Skip to content
Merged
725 changes: 725 additions & 0 deletions examples/Notebooks/ornl_venus/au_analysis.ipynb

Large diffs are not rendered by default.

773 changes: 773 additions & 0 deletions examples/Notebooks/ornl_venus/hf_analysis.ipynb

Large diffs are not rendered by default.

706 changes: 706 additions & 0 deletions examples/Notebooks/ornl_venus/ta_analysis.ipynb

Large diffs are not rendered by default.

761 changes: 0 additions & 761 deletions examples/Notebooks/pleiades_venus_demo.ipynb

This file was deleted.

3 changes: 2 additions & 1 deletion src/pleiades/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@


def main():
print("Placeholder for PLEIADES main function")
# TODO: Implement PLEIADES CLI
raise NotImplementedError("PLEIADES CLI not yet implemented")


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion src/pleiades/nuclear/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def clear_cache(self, method: Optional[DataRetrievalMethod] = None, library: Opt
# Call unlink directly on the file path
file.unlink()
logger.info(f"Deleted cached file: {file}")
except Exception as e:
except (OSError, PermissionError) as e:
logger.error(f"Failed to delete {file}: {str(e)}")

def _get_data_from_direct(
Expand Down
4 changes: 2 additions & 2 deletions src/pleiades/results/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ class AbundanceInfo(BaseModel):

class BackgroundInfo(BaseModel):
# TODO: Add attributes and methods for background information
pass
...


class NormalizationInfo(BaseModel):
# TODO: Add attributes and methods for normalization information
pass
...


class PixelInfo(BaseModel):
Expand Down
55 changes: 22 additions & 33 deletions src/pleiades/sammy/backends/local.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
#!/usr/bin/env python
"""Local backend implementation for SAMMY execution."""

import shlex
import os
import subprocess
import textwrap
from datetime import datetime
from pathlib import Path
from typing import List, Union
Expand All @@ -22,14 +21,6 @@

logger = loguru_logger.bind(name=__name__)

# Known SAMMY output file patterns
SAMMY_OUTPUT_FILES = {
"SAMMY.LPT", # Log file
"SAMMIE.ODF", # Output data file
"SAMNDF.PAR", # Updated parameter file
"SAMRESOLVED.PAR", # Additional parameter file
}


class LocalSammyRunner(SammyRunner):
"""Implementation of SAMMY runner for local installation."""
Expand Down Expand Up @@ -111,36 +102,34 @@ def execute_sammy(self, files: Union[SammyFiles, SammyFilesMultiMode]) -> SammyE
logger.debug(f"Working directory: {self.config.working_dir}")

# Generate command based on file type
# Prepare input text for SAMMY based on mode
if isinstance(files, SammyFilesMultiMode):
# JSON mode command format
sammy_command = textwrap.dedent(f"""\
{self.config.sammy_executable} <<EOF
{shlex.quote(files.input_file.name)}
#file {shlex.quote(files.json_config_file.name)}
{shlex.quote(files.data_file.name)}

EOF""")
logger.debug("Using JSON mode command format")
# JSON mode input format
sammy_input = f"{files.input_file.name}\n#file {files.json_config_file.name}\n{files.data_file.name}\n\n"
logger.debug("Using JSON mode input format")
else:
# Traditional mode command format
sammy_command = textwrap.dedent(f"""\
{self.config.sammy_executable} <<EOF
{shlex.quote(files.input_file.name)}
{shlex.quote(files.parameter_file.name)}
{shlex.quote(files.data_file.name)}

EOF""")
logger.debug("Using traditional mode command format")
# Traditional mode input format
sammy_input = f"{files.input_file.name}\n{files.parameter_file.name}\n{files.data_file.name}\n\n"
logger.debug("Using traditional mode input format")

try:
# Ensure libcrypto.so.1.1 is found by adding /usr/lib64 to LD_LIBRARY_PATH
env = dict(os.environ)
env.update(self.config.env_vars)
if "LD_LIBRARY_PATH" in env:
env["LD_LIBRARY_PATH"] = f"/usr/lib64:{env['LD_LIBRARY_PATH']}"
else:
env["LD_LIBRARY_PATH"] = "/usr/lib64"

# Use safer subprocess call without shell
process = subprocess.run(
sammy_command,
shell=True,
executable=str(self.config.shell_path),
env=self.config.env_vars,
[str(self.config.sammy_executable)],
input=sammy_input,
shell=False,
text=True,
env=env,
cwd=str(self.config.working_dir),
capture_output=True,
text=True,
)

end_time = datetime.now()
Expand Down
59 changes: 51 additions & 8 deletions src/pleiades/sammy/data/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@

logger = loguru_logger.bind(name=__name__)

# Plot constants
RESIDUAL_YLIM = (-1, 1) # Y-axis limits for residual plots
HISTOGRAM_BIN_RANGE = (-1, 1, 0.01) # Range and step for histogram bins


class DataTypeOptions(str, Enum):
TRANSMISSION = "TRANSMISSION"
Expand Down Expand Up @@ -108,21 +112,44 @@ def validate_columns(self):
if col in self.data.columns:
raise ValueError(f"Unexpected transmission column for cross-section data: {col}")

def plot_transmission(self, show_diff=False, plot_uncertainty=False):
def plot_transmission(
self,
show_diff=False,
plot_uncertainty=False,
figsize=None,
title=None,
xscale="linear",
yscale="linear",
data_color="#433E3F",
final_color="#ff6361",
show=True,
):
"""
Plot the transmission data and optionally the residuals.

Args:
show_diff (bool): If True, plot the residuals.
plot_uncertainty (bool): (Unused, for compatibility)
figsize (tuple): Figure size (width, height) in inches.
title (str): Plot title.
xscale (str): X-axis scale ('linear' or 'log').
yscale (str): Y-axis scale ('linear' or 'log').
data_color (str): Color for experimental data points.
final_color (str): Color for fitted theoretical curve.
show (bool): If True, display the plot. If False, return figure object.

Returns:
matplotlib.figure.Figure: The figure object if show=False, None otherwise.
"""
if self.data is None:
raise ValueError("No data loaded to plot.")

data = self.data
data_color = "#433E3F"
initial_color = "#003f5c"
final_color = "#ff6361"

# Use provided figsize or default
if figsize is None:
figsize = (8, 6)

# Column name mapping for compatibility
col_exp = "Experimental transmission (dimensionless)"
Expand All @@ -135,12 +162,12 @@ def plot_transmission(self, show_diff=False, plot_uncertainty=False):
2,
2,
sharey=False,
figsize=(8, 6),
figsize=figsize,
gridspec_kw={"width_ratios": [5, 1], "height_ratios": [5, 2]},
)
ax = np.ravel(ax)
else:
fig, ax = plt.subplots(figsize=(8, 6))
fig, ax = plt.subplots(figsize=figsize)
ax = [ax]

# Plot experimental transmission as scatter with error bars if available
Expand All @@ -165,11 +192,20 @@ def plot_transmission(self, show_diff=False, plot_uncertainty=False):
color=final_color,
lw=1,
)
# Apply scale settings first
ax[0].set_xscale(xscale)
ax[0].set_yscale(yscale)

# Then remove x-axis labels and ticks (for show_diff layout)
ax[0].set_xlabel("")
ax[0].set_xticks([])
ax[0].legend(["data", "final fit"])
ax[0].set_ylabel("transmission")

# Apply title if provided
if title:
ax[0].set_title(title)

# Determine y-axis limits
max_y = data[col_exp].max()
min_y = data[col_exp].min()
Expand Down Expand Up @@ -202,13 +238,15 @@ def plot_transmission(self, show_diff=False, plot_uncertainty=False):
)
ax[2].set_ylabel("residuals\n(fit-data)/err [σ]")
ax[2].set_xlabel("energy [eV]")
ax[2].set_ylim(-1, 1)
ax[2].set_ylim(*RESIDUAL_YLIM)
# Apply same x-scale to residual plot
ax[2].set_xscale(xscale)

# Plot histograms of residuals
if "residual_initial" in data.columns:
data.plot.hist(
y=["residual_initial"],
bins=np.arange(-1, 1, 0.01),
bins=np.arange(*HISTOGRAM_BIN_RANGE),
ax=ax[3],
orientation="horizontal",
legend=False,
Expand All @@ -235,7 +273,12 @@ def plot_transmission(self, show_diff=False, plot_uncertainty=False):
ax[3].spines["left"].set_visible(False)

plt.subplots_adjust(wspace=0.003, hspace=0.03)
plt.show()

if show:
plt.show()
return None
else:
return fig

def plot_cross_section(self, show_diff=False, plot_uncertainty=False):
"""Plot the cross-section data."""
Expand Down
20 changes: 0 additions & 20 deletions src/pleiades/sammy/fitting/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,3 @@ def append_isotope_from_string(self, isotope_string: str) -> None:
self.nuclear_params.isotopes.append(isotope_info)
else:
logger.error(f"Could not append Isotope {isotope_string}.")


# example usage
if __name__ == "__main__":
example_config = FitConfig(
fit_title="Example SAMMY Fit",
tolerance=1e-5,
max_iterations=100,
i_correlation=10,
max_cpu_time=3600.0,
max_wall_time=7200.0,
max_memory=8.0,
max_disk=100.0,
nuclear_params=nuclearParameters(),
physics_params=PhysicsParameters(),
data_params=SammyData(),
options_and_routines=FitOptions(),
)

print(example_config)
11 changes: 1 addition & 10 deletions src/pleiades/sammy/interface.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/env/bin python
#!/usr/bin/env python
"""
Interface definitions for SAMMY execution system.

Expand Down Expand Up @@ -396,15 +396,6 @@ def cleanup(self, files: SammyFiles) -> None:
"""
raise NotImplementedError

async def __aenter__(self) -> "SammyRunner":
"""Allow usage as async context manager."""
return self

async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
"""Ensure cleanup on context exit."""
if hasattr(self, "files"):
await self.cleanup(self.files)

@abstractmethod
def validate_config(self) -> bool:
"""
Expand Down
70 changes: 52 additions & 18 deletions src/pleiades/sammy/io/data_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
experimental transmission data.
"""

import csv
from pathlib import Path
from typing import Union

Expand All @@ -21,7 +20,7 @@ def convert_csv_to_sammy_twenty(csv_file: Union[str, Path], twenty_file: Union[s
"""
Convert transmission spectra from CSV to SAMMY twenty format.

This function supports both tab- and comma-separated CSV files, with either two columns
This function supports tab-, comma-, and space-separated files, with either two columns
(energy, transmission) or three columns (energy, transmission, uncertainty).
If only two columns are present, the uncertainty column will be filled with 0.0.

Expand All @@ -30,11 +29,13 @@ def convert_csv_to_sammy_twenty(csv_file: Union[str, Path], twenty_file: Union[s
twenty_file: Path to output SAMMY twenty format file

File Formats:
Input CSV (tab or comma separated):
Input CSV (tab, comma, or space separated):
"energy_eV,transmission,uncertainty\n6.673,0.932,0.272\n"
or
"energy_eV\ttransmission\tuncertainty\n6.673\t0.932\t0.272\n"
or
"# Energy(eV) Transmission Uncertainty\n6.673240e+00 1.003460e+00 7.242967e-03\n"
or
"energy_eV,transmission\n6.673,0.932\n"
Output twenty:
" 6.6732397079 0.9323834777 0.2727669477\n"
Expand All @@ -51,21 +52,54 @@ def convert_csv_to_sammy_twenty(csv_file: Union[str, Path], twenty_file: Union[s
"""
logger.info(f"Converting {csv_file} to SAMMY twenty format: {twenty_file}")

# Use csv.Sniffer to detect delimiter
with open(csv_file, "r", newline="") as f:
sample = f.read(2048) # Read a sample of the file for delimiter detection
f.seek(0) # Reset file pointer to start

sniffer = csv.Sniffer()
dialect = sniffer.sniff(sample, delimiters=[",", "\t"]) # Detect comma or tab delimiter
delimiter = dialect.delimiter

# Create a CSV reader with the detected delimiter
reader = csv.reader(f, delimiter=delimiter)
header = next(reader) # Skip header row

# Read the remaining rows, skipping empty lines
data = [row for row in reader if row and any(field.strip() for field in row)]
data = []

with open(csv_file, "r") as f:
lines = f.readlines()

# Skip header lines (comments starting with # or containing non-numeric first field)
for line in lines:
# Strip whitespace and skip empty lines
line = line.strip()
if not line:
continue

# Skip comment lines
if line.startswith("#"):
continue

# Try to parse the line with different delimiters
# First try splitting by whitespace (most common for scientific data)
fields = line.split()

# If that doesn't give us 2 or 3 fields, try comma
if len(fields) not in [2, 3]:
fields = line.split(",")

# If still not right, try tab
if len(fields) not in [2, 3]:
fields = line.split("\t")

# Skip lines that don't have the right number of fields
if len(fields) not in [2, 3]:
# Check if this might be a header line
try:
float(fields[0])
except (ValueError, IndexError):
continue # Skip header lines
logger.warning(f"Skipping line with {len(fields)} fields: {line[:50]}...")
continue

# Try to convert to floats
try:
numeric_fields = [float(field) for field in fields]
data.append(numeric_fields)
except ValueError:
# This is likely a header line, skip it
continue

if not data:
raise ValueError(f"No valid data found in {csv_file}")

# Convert data to numpy array of floats
data = np.array(data, dtype=float)
Expand Down
Loading