Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,801 changes: 922 additions & 879 deletions pixi.lock

Large diffs are not rendered by default.

131 changes: 131 additions & 0 deletions src/pleiades/sammy/io/card_formats/inp02_element.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/usr/bin/env python
"""
Card Set 2 (Element Information) for SAMMY INP files.

This module provides the Card02 class for parsing and generating the element
information line in SAMMY input files. This card appears early in the INP file
and defines the sample element, atomic weight, and energy range.

Format specification (Card Set 2):
Cols Format Variable Description
1-10 A ELMNT Sample element's name (left-aligned)
11-20 F AW Atomic weight (amu)
21-30 F EMIN Minimum energy for dataset (eV)
31-40 F EMAX Maximum energy (eV)

Example:
Si 27.976928 300000. 1800000.
"""

from typing import List

from pydantic import BaseModel, Field

from pleiades.utils.logger import loguru_logger

logger = loguru_logger.bind(name=__name__)

# Format specification for Card Set 2
CARD02_FORMAT = {
"element": slice(0, 10),
"atomic_weight": slice(10, 20),
"min_energy": slice(20, 30),
"max_energy": slice(30, 40),
}


class ElementInfo(BaseModel):
"""Pydantic model for element information in Card Set 2.

Attributes:
element: Sample element's name (up to 10 characters)
atomic_weight: Atomic weight in amu
min_energy: Minimum energy for dataset in eV
max_energy: Maximum energy in eV
"""

element: str = Field(..., description="Sample element's name", max_length=10)
atomic_weight: float = Field(..., description="Atomic weight (amu)", gt=0)
min_energy: float = Field(..., description="Minimum energy (eV)", ge=0)
max_energy: float = Field(..., description="Maximum energy (eV)", gt=0)

def model_post_init(self, __context) -> None:
"""Validate that max_energy > min_energy."""
if self.max_energy <= self.min_energy:
raise ValueError(f"max_energy ({self.max_energy}) must be greater than min_energy ({self.min_energy})")


class Card02(BaseModel):
"""
Class representing Card Set 2 (element information) in SAMMY INP files.

This card defines the sample element, atomic weight, and energy range for the analysis.
"""

@classmethod
def from_lines(cls, lines: List[str]) -> ElementInfo:
"""Parse element information from Card Set 2 line.

Args:
lines: List of input lines (expects single line for Card 2)

Returns:
ElementInfo: Parsed element information

Raises:
ValueError: If format is invalid or required values missing
"""
if not lines or not lines[0].strip():
message = "No valid Card 2 line provided"
logger.error(message)
raise ValueError(message)

line = lines[0]
if len(line) < 40:
line = f"{line:<40}"

try:
element = line[CARD02_FORMAT["element"]].strip()
atomic_weight = float(line[CARD02_FORMAT["atomic_weight"]].strip())
min_energy = float(line[CARD02_FORMAT["min_energy"]].strip())
max_energy = float(line[CARD02_FORMAT["max_energy"]].strip())
except (ValueError, IndexError) as e:
message = f"Failed to parse Card 2 line: {e}"
logger.error(message)
raise ValueError(message)

if not element:
message = "Element name cannot be empty"
logger.error(message)
raise ValueError(message)

return ElementInfo(
element=element,
atomic_weight=atomic_weight,
min_energy=min_energy,
max_energy=max_energy,
)

@classmethod
def to_lines(cls, element_info: ElementInfo) -> List[str]:
"""Convert element information to Card Set 2 formatted line.

Args:
element_info: ElementInfo object containing element data

Returns:
List containing single formatted line for Card Set 2
"""
if not isinstance(element_info, ElementInfo):
message = "element_info must be an instance of ElementInfo"
logger.error(message)
raise ValueError(message)

line = (
f"{element_info.element:<10s}"
f"{element_info.atomic_weight:10.6f}"
f"{element_info.min_energy:10.3f}"
f"{element_info.max_energy:10.1f}"
)

return [line]
123 changes: 123 additions & 0 deletions src/pleiades/sammy/io/card_formats/inp03_constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/usr/bin/env python
"""
Card Set 3 (Physical Constants) for SAMMY INP files.

This module provides the Card03 class for parsing and generating the physical
constants line in SAMMY input files. This card appears after the element information
and defines temperature, flight path, and resolution parameters.

Format specification (Card Set 3 - Physical Constants):
The line contains five floating-point values with variable spacing:
- TEMP: Temperature (K)
- FPL: Flight path length (m)
- DELTAL: Spread in flight-path length (m)
- DELTAG: Gaussian resolution width (μs)
- DELTAE: e-folding width of exponential resolution (μs)

Example:
300. 200.0000 0.182233 0.0 0.002518
"""

from typing import List

from pydantic import BaseModel, Field

from pleiades.utils.logger import loguru_logger

logger = loguru_logger.bind(name=__name__)


class PhysicalConstants(BaseModel):
"""Pydantic model for physical constants in Card Set 3.

Attributes:
temperature: Temperature in Kelvin
flight_path_length: Flight path length in meters
delta_l: Spread in flight-path length in meters
delta_g: Gaussian resolution width in microseconds
delta_e: e-folding width of exponential resolution in microseconds
"""

temperature: float = Field(..., description="Temperature (K)", gt=0)
flight_path_length: float = Field(..., description="Flight path length (m)", gt=0)
delta_l: float = Field(default=0.0, description="Spread in flight-path length (m)", ge=0)
delta_g: float = Field(default=0.0, description="Gaussian resolution width (μs)", ge=0)
delta_e: float = Field(default=0.0, description="e-folding width of exponential resolution (μs)", ge=0)


class Card03(BaseModel):
"""
Class representing Card Set 3 (physical constants) in SAMMY INP files.

This card defines temperature, flight path, and resolution parameters for the analysis.
"""

@classmethod
def from_lines(cls, lines: List[str]) -> PhysicalConstants:
"""Parse physical constants from Card Set 3 line.

Args:
lines: List of input lines (expects single line for Card 3)

Returns:
PhysicalConstants: Parsed physical constants

Raises:
ValueError: If format is invalid or required values missing
"""
if not lines or not lines[0].strip():
message = "No valid Card 3 line provided"
logger.error(message)
raise ValueError(message)

line = lines[0].strip()
fields = line.split()

if len(fields) < 2:
message = f"Card 3 line must have at least 2 fields (TEMP, FPL), got {len(fields)}"
logger.error(message)
raise ValueError(message)

try:
temperature = float(fields[0])
flight_path_length = float(fields[1])
delta_l = float(fields[2]) if len(fields) > 2 else 0.0
delta_g = float(fields[3]) if len(fields) > 3 else 0.0
delta_e = float(fields[4]) if len(fields) > 4 else 0.0
except (ValueError, IndexError) as e:
message = f"Failed to parse Card 3 line: {e}"
logger.error(message)
raise ValueError(message)

return PhysicalConstants(
temperature=temperature,
flight_path_length=flight_path_length,
delta_l=delta_l,
delta_g=delta_g,
delta_e=delta_e,
)

@classmethod
def to_lines(cls, constants: PhysicalConstants) -> List[str]:
"""Convert physical constants to Card Set 3 formatted line.

Args:
constants: PhysicalConstants object containing parameter data

Returns:
List containing single formatted line for Card Set 3
"""
if not isinstance(constants, PhysicalConstants):
message = "constants must be an instance of PhysicalConstants"
logger.error(message)
raise ValueError(message)

line = (
f" {constants.temperature:5.1f} "
f"{constants.flight_path_length:8.4f} "
f"{constants.delta_l:8.6f} "
f"{constants.delta_g:3.1f} "
f"{constants.delta_e:8.6f}"
)

return [line]
102 changes: 102 additions & 0 deletions src/pleiades/sammy/io/card_formats/inp03_density.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env python
"""
Sample Density for SAMMY INP files.

This module provides the Card03Density class for parsing and generating the sample
density line in SAMMY input files. This line appears after the physical constants
and defines the material density and number density.

Format specification (Sample Density):
The line contains two floating-point values:
- Density: Material density (g/cm³)
- Number density: Number density (atoms/barn-cm)

Example:
4.20000 0.347162
"""

from typing import List

from pydantic import BaseModel, Field

from pleiades.utils.logger import loguru_logger

logger = loguru_logger.bind(name=__name__)


class SampleDensity(BaseModel):
"""Pydantic model for sample density parameters.

Attributes:
density: Material density in g/cm³
number_density: Number density in atoms/barn-cm
"""

density: float = Field(..., description="Material density (g/cm³)", gt=0)
number_density: float = Field(..., description="Number density (atoms/barn-cm)", gt=0)


class Card03Density(BaseModel):
"""
Class representing sample density line in SAMMY INP files.

This line defines the material density and number density for the sample.
"""

@classmethod
def from_lines(cls, lines: List[str]) -> SampleDensity:
"""Parse sample density from density line.

Args:
lines: List of input lines (expects single line)

Returns:
SampleDensity: Parsed sample density parameters

Raises:
ValueError: If format is invalid or required values missing
"""
if not lines or not lines[0].strip():
message = "No valid density line provided"
logger.error(message)
raise ValueError(message)

line = lines[0].strip()
fields = line.split()

if len(fields) < 2:
message = f"Density line must have 2 fields (density, number_density), got {len(fields)}"
logger.error(message)
raise ValueError(message)

try:
density = float(fields[0])
number_density = float(fields[1])
except (ValueError, IndexError) as e:
message = f"Failed to parse density line: {e}"
logger.error(message)
raise ValueError(message)

return SampleDensity(
density=density,
number_density=number_density,
)

@classmethod
def to_lines(cls, sample_density: SampleDensity) -> List[str]:
"""Convert sample density to formatted line.

Args:
sample_density: SampleDensity object containing density data

Returns:
List containing single formatted line
"""
if not isinstance(sample_density, SampleDensity):
message = "sample_density must be an instance of SampleDensity"
logger.error(message)
raise ValueError(message)

line = f" {sample_density.density:8.6f} {sample_density.number_density:.6e}"

return [line]
Loading