#!/usr/bin/env python
"""Parsers and containers for SAMMY's Card Set 11 parameters.
This module implements parsers and containers for SAMMY's Card Set 11 miscellaneous
parameters which can appear in either the PARameter or INPut file.
Format specification from Table VI B.2:
Card Set 11 contains optional parameter sets with distinct formats:
1. DELTA - Length-dependent flight path parameters
2. ETA - Normalization coefficients for ETA data
3. FINIT - Finite-size corrections for angular distributions
4. GAMMA - Radiation width specifications
5. TZERO - Time offset parameters
...etc.
The card set starts with header "MISCEllaneous parameters follow".
Each parameter type has a specific identifier in columns 1-5 and its own fixed-width format.
Parameters can be omitted when not needed.
"""
from enum import Enum
from typing import List, Optional
from pydantic import BaseModel, Field, model_validator
from pleiades.utils.helper import VaryFlag, format_float, format_vary, safe_parse
from pleiades.utils.logger import loguru_logger
# Format definitions - column positions for each parameter type
FORMAT_SPECS = {
"DELTA": {
"identifier": slice(0, 5),
"flag1": slice(6, 7),
"flag0": slice(8, 9),
"l1_coeff": slice(10, 20),
"l1_unc": slice(20, 30),
"l0_const": slice(30, 40),
"l0_unc": slice(40, 50),
},
"ETA": {
"identifier": slice(0, 5),
"flag": slice(6, 7),
"nu_value": slice(10, 20),
"nu_unc": slice(20, 30),
"energy": slice(30, 40),
},
"FINIT": {
"identifier": slice(0, 5),
"flag_i": slice(6, 7),
"flag_o": slice(8, 9),
"attni": slice(10, 20),
"dttni": slice(20, 30),
"attno": slice(30, 40),
"dttno": slice(40, 50),
},
"GAMMA": {
"identifier": slice(0, 5),
"group": slice(5, 7),
"flag": slice(7, 9),
"width": slice(10, 20),
"uncertainty": slice(20, 30),
},
"TZERO": {
"identifier": slice(0, 5),
"flag_t0": slice(6, 7),
"flag_l0": slice(8, 9),
"t0_value": slice(10, 20),
"t0_unc": slice(20, 30),
"l0_value": slice(30, 40),
"l0_unc": slice(40, 50),
"fpl": slice(50, 60),
},
"SIABN": {
"identifier": slice(0, 5),
"flag1": slice(6, 7),
"flag2": slice(8, 9),
"flag3": slice(9, 10),
"abundance1": slice(10, 20),
"uncertainty1": slice(20, 30),
"abundance2": slice(30, 40),
"uncertainty2": slice(40, 50),
"abundance3": slice(50, 60),
"uncertainty3": slice(60, 70),
},
"SELFI": {
"identifier": slice(0, 5),
"flag_temp": slice(6, 7),
"flag_thick": slice(8, 9),
"temperature": slice(10, 20),
"temp_unc": slice(20, 30),
"thickness": slice(30, 40),
"thick_unc": slice(40, 50),
},
"EFFIC": {
"identifier": slice(0, 5),
"flag_cap": slice(6, 7),
"flag_fis": slice(8, 9),
"eff_cap": slice(10, 20),
"eff_fis": slice(20, 30),
"eff_cap_unc": slice(30, 40),
"eff_fis_unc": slice(40, 50),
},
"DELTE": {
"identifier": slice(0, 5),
"flag1": slice(6, 7),
"flag0": slice(8, 9),
"flagl": slice(9, 10),
"dele1": slice(10, 20),
"dd1": slice(20, 30),
"dele0": slice(30, 40),
"dd0": slice(40, 50),
"delel": slice(50, 60),
"ddl": slice(60, 70),
},
"DRCAP": {
"identifier": slice(0, 5),
"flag": slice(6, 7),
"nuc": slice(8, 9),
"coef": slice(10, 20),
"dcoef": slice(20, 30),
},
"NONUN": {
"identifier": slice(0, 5),
"radius": slice(20, 30),
"thickness": slice(30, 40),
"uncertainty": slice(40, 50),
},
}
[docs]
class Card11ParameterType(str, Enum):
"""Enumeration of Card 11 parameter types."""
DELTA = "DELTA"
ETA = "ETA"
FINIT = "FINIT"
GAMMA = "GAMMA"
TZERO = "TZERO"
SIABN = "SIABN"
SELFI = "SELFI"
EFFIC = "EFFIC"
DELTE = "DELTE"
DRCAP = "DRCAP"
NONUN = "NONUN"
[docs]
class Card11Parameter(BaseModel):
"""Base class for Card 11 parameter types.
This class provides common functionality for all Card 11 parameter types
including parsing and formatting of fixed-width formats.
Attributes:
type (Card11ParameterType): Parameter type identifier
"""
type: Card11ParameterType
[docs]
@classmethod
def identify_type(cls, line: str) -> Optional[Card11ParameterType]:
"""Identify parameter type from input line.
Args:
line: Input line starting with parameter identifier
Returns:
Parameter type or None if not recognized
"""
if not line or len(line) < 5:
return None
identifier = line[0:5].strip()
try:
return Card11ParameterType(identifier)
except ValueError:
return None
[docs]
@classmethod
def from_lines(cls, lines: List[str]) -> Optional["Card11Parameter"]:
"""Factory method to create appropriate parameter object.
Args:
lines: Input lines for parameter
Returns:
Parsed parameter object or None if invalid
Raises:
ValueError: If format is invalid
"""
if not lines:
return None
param_type = cls.identify_type(lines[0])
if param_type is None:
return None
# Dispatch to appropriate parameter class based on type
parameter_classes = {
Card11ParameterType.DELTA: DeltaParameters,
Card11ParameterType.ETA: EtaParameters,
Card11ParameterType.FINIT: FinitParameters,
Card11ParameterType.GAMMA: GammaParameters,
Card11ParameterType.TZERO: TzeroParameters,
Card11ParameterType.SIABN: SiabnParameters,
Card11ParameterType.SELFI: SelfiParameters,
Card11ParameterType.EFFIC: EfficParameters,
Card11ParameterType.DELTE: DelteParameters,
Card11ParameterType.DRCAP: DrcapParameters,
Card11ParameterType.NONUN: NonunParameters,
}
parser_class = parameter_classes.get(param_type)
if parser_class is None:
loguru_logger.warning(f"Parser not yet implemented for parameter type: {param_type}")
return None
return parser_class.from_lines(lines)
[docs]
def to_lines(self) -> List[str]:
"""Convert parameter to fixed-width format lines.
Returns:
List of formatted lines
Raises:
NotImplementedError: Must be implemented by subclasses
"""
raise NotImplementedError
[docs]
class DeltaParameters(Card11Parameter):
"""Container for DELTA (Length-dependent flight path) parameters.
Format specification from Table VI B.2:
Cols Format Variable Description
1-5 A "DELTA" Parameter identifier
7 I IFLAG1 Flag for L'₁ (1=vary, 3=PUP, 0=fixed)
9 I IFLAG0 Flag for L'₀
11-20 F DELL11 L'₁ coefficient of E (m/eV)
21-30 F D1 Uncertainty on L'₁ (m/eV)
31-40 F DELL00 L'₀ constant term (m)
41-50 F D0 Uncertainty on L'₀ (m)
Attributes:
l1_coefficient: L'₁ coefficient of E (m/eV)
l1_uncertainty: Uncertainty on L'₁ (m/eV)
l0_constant: L'₀ constant term (m)
l0_uncertainty: Uncertainty on L'₀ (m)
l1_flag: Flag for varying L'₁
l0_flag: Flag for varying L'₀
"""
type: Card11ParameterType = Card11ParameterType.DELTA
l1_coefficient: float = Field(..., description="L'₁ coefficient of E (m/eV)")
l1_uncertainty: Optional[float] = Field(None, description="Uncertainty on L'₁ (m/eV)")
l0_constant: float = Field(..., description="L'₀ constant term (m)")
l0_uncertainty: Optional[float] = Field(None, description="Uncertainty on L'₀ (m)")
l1_flag: VaryFlag = Field(default=VaryFlag.NO, description="Flag for L'₁")
l0_flag: VaryFlag = Field(default=VaryFlag.NO, description="Flag for L'₀")
[docs]
@classmethod
def from_lines(cls, lines: List[str]) -> "DeltaParameters":
"""Parse DELTA parameters from fixed-width format lines.
Args:
lines: List of input lines (expects single line for DELTA parameters)
Returns:
DeltaParameters: Parsed parameters
Raises:
ValueError: If format is invalid or required values missing
"""
if not lines or not lines[0].strip():
raise ValueError("No valid parameter line provided")
# Verify identifier
line = f"{lines[0]:<80}" # Pad to full width
identifier = line[FORMAT_SPECS["DELTA"]["identifier"]].strip()
if identifier != "DELTA":
raise ValueError(f"Invalid identifier: {identifier}")
# Parse flags
try:
l1_flag = VaryFlag(int(line[FORMAT_SPECS["DELTA"]["flag1"]].strip() or "0"))
l0_flag = VaryFlag(int(line[FORMAT_SPECS["DELTA"]["flag0"]].strip() or "0"))
except ValueError as e:
raise ValueError(f"Invalid flag value: {e}")
# Parse required numeric values
l1_coeff = safe_parse(line[FORMAT_SPECS["DELTA"]["l1_coeff"]])
l0_const = safe_parse(line[FORMAT_SPECS["DELTA"]["l0_const"]])
if l1_coeff is None or l0_const is None:
raise ValueError("Missing required numeric values")
# Parse optional uncertainties
l1_unc = safe_parse(line[FORMAT_SPECS["DELTA"]["l1_unc"]])
l0_unc = safe_parse(line[FORMAT_SPECS["DELTA"]["l0_unc"]])
return cls(
l1_coefficient=l1_coeff,
l1_uncertainty=l1_unc,
l0_constant=l0_const,
l0_uncertainty=l0_unc,
l1_flag=l1_flag,
l0_flag=l0_flag,
)
[docs]
def to_lines(self) -> List[str]:
"""Convert parameters to fixed-width format line.
Returns:
List containing single formatted line
"""
parts = [
"DELTA".ljust(5),
" ", # Column 6 spacing
format_vary(self.l1_flag),
" ", # Column 8 spacing
format_vary(self.l0_flag),
" ", # Column 10 spacing
format_float(self.l1_coefficient, width=10),
format_float(self.l1_uncertainty, width=10),
format_float(self.l0_constant, width=10),
format_float(self.l0_uncertainty, width=10),
]
return ["".join(parts)]
[docs]
class EtaParameters(Card11Parameter):
"""Container for ETA (normalization coefficient) parameters.
Format specification from Table VI B.2:
Cols Format Variable Description
1-5 A "ETA " Parameter identifier ("eta" + 2 spaces)
7 I IFLAGN Flag for parameter ν (0=fixed, 1=vary, 3=PUP)
11-20 F NU Normalization coefficient ν (dimensionless)
21-30 F DNU Uncertainty on NU
31-40 F ENU Energy for which this value applies (eV)
Notes:
- If a constant value of NU is wanted, the energy value can be omitted
- If more than one ETA line is present, all must be together in increasing energy order
- SAMMY will linearly interpolate to obtain values between specified energies
Attributes:
nu_value: Normalization coefficient ν (dimensionless)
nu_uncertainty: Uncertainty on ν
energy: Energy for which this value applies (eV), optional
flag: Flag for varying ν
"""
type: Card11ParameterType = Card11ParameterType.ETA
nu_value: float = Field(..., description="Normalization coefficient ν")
nu_uncertainty: Optional[float] = Field(None, description="Uncertainty on ν")
energy: Optional[float] = Field(None, description="Energy value (eV)")
flag: VaryFlag = Field(default=VaryFlag.NO, description="Flag for ν")
[docs]
@classmethod
def from_lines(cls, lines: List[str]) -> "EtaParameters":
"""Parse ETA parameters from fixed-width format lines.
Args:
lines: List of input lines (expects single line for ETA parameters)
Returns:
EtaParameters: Parsed parameters
Raises:
ValueError: If format is invalid or required values missing
"""
if not lines or not lines[0].strip():
raise ValueError("No valid parameter line provided")
line = f"{lines[0]:<80}" # Pad to full width
# Verify identifier is "ETA " (including 2 spaces)
identifier = line[FORMAT_SPECS["ETA"]["identifier"]].strip()
if identifier != "ETA":
raise ValueError(f"Invalid identifier: {identifier}")
# Parse flag
try:
flag = VaryFlag(int(line[FORMAT_SPECS["ETA"]["flag"]].strip() or "0"))
except ValueError as e:
raise ValueError(f"Invalid flag value: {e}")
# Parse required nu value
nu_value = safe_parse(line[FORMAT_SPECS["ETA"]["nu_value"]])
if nu_value is None:
raise ValueError("Missing required nu value")
# Parse optional values
nu_unc = safe_parse(line[FORMAT_SPECS["ETA"]["nu_unc"]])
energy = safe_parse(line[FORMAT_SPECS["ETA"]["energy"]])
return cls(nu_value=nu_value, nu_uncertainty=nu_unc, energy=energy, flag=flag)
[docs]
def to_lines(self) -> List[str]:
"""Convert parameters to fixed-width format line.
Returns:
List containing single formatted line
"""
parts = [
"ETA ".ljust(5), # Identifier with required 2 spaces
" ", # Column 6 spacing
format_vary(self.flag),
" ", # Columns 8-10 spacing
format_float(self.nu_value, width=10),
format_float(self.nu_uncertainty, width=10),
format_float(self.energy, width=10) if self.energy is not None else " " * 10,
]
return ["".join(parts)]
[docs]
class FinitParameters(Card11Parameter):
"""Container for FINIT (finite-size corrections) parameters.
Format specification from Table VI B.2:
Cols Format Variable Description
1-5 A "FINIT" Parameter identifier
7 I IFLAGI Flag for ATTNI
9 I IFLAGO Flag for ATTNO
11-20 F ATTNI Incident-particle attenuation (atoms/barn)
21-30 F DTTNI Uncertainty on ATTNI
31-40 F ATTNO Exit-particle attenuation (atom/b)
41-50 F DTTNO Uncertainty on ATTNO
Notes:
- Repeat once for each angle
- If only one line, same attenuations used for all angles
Attributes:
incident_attenuation: Incident-particle attenuation (atoms/barn)
incident_uncertainty: Uncertainty on incident attenuation
exit_attenuation: Exit-particle attenuation (atom/b)
exit_uncertainty: Uncertainty on exit attenuation
incident_flag: Flag for incident attenuation
exit_flag: Flag for exit attenuation
"""
type: Card11ParameterType = Card11ParameterType.FINIT
incident_attenuation: float = Field(..., description="Incident-particle attenuation (atoms/barn)")
incident_uncertainty: Optional[float] = Field(None, description="Uncertainty on incident attenuation")
exit_attenuation: float = Field(..., description="Exit-particle attenuation (atom/b)")
exit_uncertainty: Optional[float] = Field(None, description="Uncertainty on exit attenuation")
incident_flag: VaryFlag = Field(default=VaryFlag.NO, description="Flag for incident attenuation")
exit_flag: VaryFlag = Field(default=VaryFlag.NO, description="Flag for exit attenuation")
[docs]
@classmethod
def from_lines(cls, lines: List[str]) -> "FinitParameters":
"""Parse FINIT parameters from fixed-width format lines.
Args:
lines: List of input lines (expects single line for FINIT parameters)
Returns:
FinitParameters: Parsed parameters
Raises:
ValueError: If format is invalid or required values missing
"""
if not lines or not lines[0].strip():
raise ValueError("No valid parameter line provided")
line = f"{lines[0]:<80}" # Pad to full width
# Verify identifier
identifier = line[FORMAT_SPECS["FINIT"]["identifier"]].strip()
if identifier != "FINIT":
raise ValueError(f"Invalid identifier: {identifier}")
# Parse flags
try:
incident_flag = VaryFlag(int(line[FORMAT_SPECS["FINIT"]["flag_i"]].strip() or "0"))
exit_flag = VaryFlag(int(line[FORMAT_SPECS["FINIT"]["flag_o"]].strip() or "0"))
except ValueError as e:
raise ValueError(f"Invalid flag value: {e}")
# Parse required attenuations
incident_att = safe_parse(line[FORMAT_SPECS["FINIT"]["attni"]])
exit_att = safe_parse(line[FORMAT_SPECS["FINIT"]["attno"]])
if incident_att is None or exit_att is None:
raise ValueError("Missing required attenuation values")
# Parse optional uncertainties
incident_unc = safe_parse(line[FORMAT_SPECS["FINIT"]["dttni"]])
exit_unc = safe_parse(line[FORMAT_SPECS["FINIT"]["dttno"]])
return cls(
incident_attenuation=incident_att,
incident_uncertainty=incident_unc,
exit_attenuation=exit_att,
exit_uncertainty=exit_unc,
incident_flag=incident_flag,
exit_flag=exit_flag,
)
[docs]
def to_lines(self) -> List[str]:
"""Convert parameters to fixed-width format line.
Returns:
List containing single formatted line
"""
parts = [
"FINIT", # Identifier
" ", # Column 6 spacing
format_vary(self.incident_flag), # Col 7
" ", # Column 8 spacing
format_vary(self.exit_flag), # Col 9
" ", # Column 10 spacing
format_float(self.incident_attenuation, width=10),
format_float(self.incident_uncertainty, width=10),
format_float(self.exit_attenuation, width=10),
format_float(self.exit_uncertainty, width=10),
]
return ["".join(parts)]
[docs]
class GammaParameters(Card11Parameter):
"""Container for GAMMA (radiation width) parameters.
Format specification from Table VI B.2:
Cols Format Variable Description
1-5 A "GAMMA" Parameter identifier
6-7 I IG Spin group number
8-9 I IFG Flag for GAMGAM (0=fixed, 1=vary, 3=PUP)
11-20 F GAMGAM Radiation width Γγ for all resonances in spin group
21-30 F DGAM Uncertainty on GAMGAM
Notes:
- If used for any spin group, must be given for every spin group
Attributes:
spin_group: Spin group number (must be positive)
width: Radiation width Γγ for all resonances in group
uncertainty: Uncertainty on width (optional)
flag: Flag for varying width
"""
type: Card11ParameterType = Card11ParameterType.GAMMA
spin_group: int = Field(..., gt=0, description="Spin group number")
width: float = Field(..., description="Radiation width Γγ")
uncertainty: Optional[float] = Field(None, description="Uncertainty on width")
flag: VaryFlag = Field(default=VaryFlag.NO, description="Flag for width")
[docs]
@classmethod
def from_lines(cls, lines: List[str]) -> "GammaParameters":
"""Parse GAMMA parameters from fixed-width format lines.
Args:
lines: List of input lines (expects single line for GAMMA parameters)
Returns:
GammaParameters: Parsed parameters
Raises:
ValueError: If format is invalid or required values missing
"""
if not lines or not lines[0].strip():
raise ValueError("No valid parameter line provided")
line = f"{lines[0]:<80}" # Pad to full width
# Verify identifier
identifier = line[FORMAT_SPECS["GAMMA"]["identifier"]].strip()
if identifier != "GAMMA":
raise ValueError(f"Invalid identifier: {identifier}")
# Parse spin group (required)
spin_group = safe_parse(line[FORMAT_SPECS["GAMMA"]["group"]], as_int=True)
if spin_group is None:
raise ValueError("Missing or invalid spin group number")
if spin_group <= 0:
raise ValueError("Spin group must be positive")
# Parse flag
try:
flag = VaryFlag(int(line[FORMAT_SPECS["GAMMA"]["flag"]].strip() or "0"))
except ValueError as e:
raise ValueError(f"Invalid flag value: {e}")
# Parse width (required)
width = safe_parse(line[FORMAT_SPECS["GAMMA"]["width"]])
if width is None:
raise ValueError("Missing required width value")
# Parse optional uncertainty
uncertainty = safe_parse(line[FORMAT_SPECS["GAMMA"]["uncertainty"]])
return cls(spin_group=spin_group, width=width, uncertainty=uncertainty, flag=flag)
[docs]
def to_lines(self) -> List[str]:
"""Convert parameters to fixed-width format line.
Returns:
List containing single formatted line
"""
parts = [
"GAMMA", # Identifier
str(self.spin_group).rjust(2), # Spin group (cols 6-7)
format_vary(self.flag).rjust(2), # Flag (cols 8-9)
" ", # Column 10 spacing
format_float(self.width, width=10), # Width (cols 11-20)
format_float(self.uncertainty, width=10), # Uncertainty (cols 21-30)
]
return ["".join(parts)]
[docs]
class TzeroParameters(Card11Parameter):
"""Container for TZERO (time offset) parameters.
Format specification from Table VI B.2:
Cols Format Variable Description
1-5 A "TZERO" Parameter identifier
7 I IFTZER Flag for t
9 I IFLZER Flag for L
11-20 F TZERO t (μs)
21-30 F DTZERO Uncertainty on t (μs)
31-40 F LZERO L (dimensionless)
41-50 F DLZERO Uncertainty on L
51-60 F FPL Flight-path length (m)
Attributes:
t0_value: Time offset t (μs)
t0_uncertainty: Uncertainty on t (μs)
l0_value: L value (dimensionless)
l0_uncertainty: Uncertainty on L
flight_path_length: Flight path length (m), optional
t0_flag: Flag for varying t
l0_flag: Flag for varying L
"""
type: Card11ParameterType = Card11ParameterType.TZERO
t0_value: float = Field(..., description="Time offset t (μs)")
t0_uncertainty: Optional[float] = Field(None, description="Uncertainty on t (μs)")
l0_value: float = Field(..., description="L value (dimensionless)")
l0_uncertainty: Optional[float] = Field(None, description="Uncertainty on L")
flight_path_length: Optional[float] = Field(None, description="Flight path length (m)")
t0_flag: VaryFlag = Field(default=VaryFlag.NO, description="Flag for t")
l0_flag: VaryFlag = Field(default=VaryFlag.NO, description="Flag for L")
[docs]
@classmethod
def from_lines(cls, lines: List[str]) -> "TzeroParameters":
"""Parse TZERO parameters from fixed-width format lines.
Args:
lines: List of input lines (expects single line for TZERO parameters)
Returns:
TzeroParameters: Parsed parameters
Raises:
ValueError: If format is invalid or required values missing
"""
if not lines or not lines[0].strip():
raise ValueError("No valid parameter line provided")
line = f"{lines[0]:<80}" # Pad to full width
# Verify identifier
identifier = line[FORMAT_SPECS["TZERO"]["identifier"]].strip()
if identifier != "TZERO":
raise ValueError(f"Invalid identifier: {identifier}")
# Parse flags
try:
t0_flag = VaryFlag(int(line[FORMAT_SPECS["TZERO"]["flag_t0"]].strip() or "0"))
l0_flag = VaryFlag(int(line[FORMAT_SPECS["TZERO"]["flag_l0"]].strip() or "0"))
except ValueError as e:
raise ValueError(f"Invalid flag value: {e}")
# Parse required values
t0_value = safe_parse(line[FORMAT_SPECS["TZERO"]["t0_value"]])
l0_value = safe_parse(line[FORMAT_SPECS["TZERO"]["l0_value"]])
if t0_value is None or l0_value is None:
raise ValueError("Missing required t₀ or L₀ value")
# Parse optional values
t0_unc = safe_parse(line[FORMAT_SPECS["TZERO"]["t0_unc"]])
l0_unc = safe_parse(line[FORMAT_SPECS["TZERO"]["l0_unc"]])
fpl = safe_parse(line[FORMAT_SPECS["TZERO"]["fpl"]])
return cls(
t0_value=t0_value,
t0_uncertainty=t0_unc,
l0_value=l0_value,
l0_uncertainty=l0_unc,
flight_path_length=fpl,
t0_flag=t0_flag,
l0_flag=l0_flag,
)
[docs]
def to_lines(self) -> List[str]:
"""Convert parameters to fixed-width format line.
Returns:
List containing single formatted line
"""
parts = [
"TZERO", # Identifier
" ", # Column 6 spacing
format_vary(self.t0_flag), # Col 7
" ", # Column 8 spacing
format_vary(self.l0_flag), # Col 9
" ", # Column 10 spacing
format_float(self.t0_value, width=10), # t value
format_float(self.t0_uncertainty, width=10), # t uncertainty
format_float(self.l0_value, width=10), # L value
format_float(self.l0_uncertainty, width=10), # L uncertainty
format_float(self.flight_path_length, width=10), # Flight path length
]
return ["".join(parts)]
[docs]
class SiabnParameters(Card11Parameter):
"""Container for SIABN (self-indication abundance) parameters.
Format specification from Table VI B.2:
Cols Format Variable Description
1-5 A "SIABN" Parameter identifier
7 I IF1 Flag for SIABN(1)
9 I IF2 Flag for SIABN(2)
10 I IF3 Flag for SIABN(3)
11-20 F SIABN(1) Abundance for nuclide #1
21-30 F DS(1) Uncertainty on SIABN(1)
31-40 F SIABN(2) Abundance for nuclide #2
41-50 F DS(2) Uncertainty on SIABN(2)
51-60 F SIABN(3) Abundance for nuclide #3
61-70 F DS(3) Uncertainty on SIABN(3)
Notes:
- Nuclides must be defined in card set 10 before card set 11
- At least one abundance must be provided
- Number of flags must match number of abundances
"""
type: Card11ParameterType = Card11ParameterType.SIABN
abundances: List[float] = Field(..., min_length=1, max_length=3, description="Abundance values for nuclides")
uncertainties: List[Optional[float]] = Field(..., max_length=3, description="Uncertainties on abundances")
flags: List[VaryFlag] = Field(..., max_length=3, description="Flags for varying abundances")
[docs]
@model_validator(mode="after")
def validate_list_lengths(self) -> "SiabnParameters":
"""Validate that all lists have matching lengths."""
if len(self.abundances) != len(self.uncertainties) or len(self.abundances) != len(self.flags):
raise ValueError("Number of abundances, uncertainties, and flags must match")
return self
[docs]
@classmethod
def from_lines(cls, lines: List[str]) -> "SiabnParameters":
"""Parse SIABN parameters from fixed-width format lines.
Args:
lines: List of input lines (expects single line for SIABN parameters)
Returns:
SiabnParameters: Parsed parameters
Raises:
ValueError: If format is invalid or required values missing
"""
if not lines or not lines[0].strip():
raise ValueError("No valid parameter line provided")
line = f"{lines[0]:<80}" # Pad to full width
# Verify identifier
identifier = line[FORMAT_SPECS["SIABN"]["identifier"]].strip()
if identifier != "SIABN":
raise ValueError(f"Invalid identifier: {identifier}")
# Parse flags
flags = []
for i, flag_slice in enumerate(
[FORMAT_SPECS["SIABN"]["flag1"], FORMAT_SPECS["SIABN"]["flag2"], FORMAT_SPECS["SIABN"]["flag3"]]
):
flag_str = line[flag_slice].strip()
if flag_str:
try:
flags.append(VaryFlag(int(flag_str)))
except ValueError as e:
raise ValueError(f"Invalid flag value for flag {i + 1}: {e}")
# Parse abundance-uncertainty pairs
abundances = []
uncertainties = []
for i in range(1, 4): # Up to 3 pairs
abund = safe_parse(line[FORMAT_SPECS["SIABN"][f"abundance{i}"]])
uncert = safe_parse(line[FORMAT_SPECS["SIABN"][f"uncertainty{i}"]])
if abund is not None: # Found a valid abundance
abundances.append(abund)
uncertainties.append(uncert)
elif i == 1: # First abundance is required
raise ValueError("At least one abundance value is required")
else:
break # Stop parsing pairs if abundance is missing
# Trim flags to match number of abundances found
flags = flags[: len(abundances)]
if not abundances:
raise ValueError("At least one abundance value is required")
return cls(abundances=abundances, uncertainties=uncertainties, flags=flags)
[docs]
def to_lines(self) -> List[str]:
"""Convert parameters to fixed-width format line.
Returns:
List containing single formatted line
"""
parts = [
"SIABN", # Identifier
" ", # Column 6 spacing
str(self.flags[0].value), # Flag for abundance 1
" ", # Column 8 spacing
str(self.flags[1].value), # Flag for abundance 2
str(self.flags[2].value), # Flag for abundance 3
]
# Add abundance-uncertainty pairs
for i in range(3):
if i < len(self.abundances):
parts.extend(
[
format_float(self.abundances[i], width=10),
format_float(self.uncertainties[i], width=10),
]
)
else:
parts.extend([" " * 10, " " * 10]) # Pad with spaces if fewer than 3 pairs
return ["".join(parts)]
[docs]
class SelfiParameters(Card11Parameter):
"""Container for SELFI (self-indication temperature/thickness) parameters.
Format specification from Table VI B.2:
Cols Format Variable Description
1-5 A "SELFI" Parameter identifier
7 I IFTEMP Flag for temperature
9 I IFTHCK Flag for thickness
11-20 F SITEM Effective temperature (K)
21-30 F dSITEM Uncertainty on SITEM
31-40 F SITHC Thickness (atoms/barn)
41-50 F dSITHC Uncertainty on SITHC
Attributes:
temperature: Effective temperature (K) for transmission sample
temperature_uncertainty: Uncertainty on temperature
thickness: Sample thickness (atoms/barn)
thickness_uncertainty: Uncertainty on thickness
temperature_flag: Flag for varying temperature
thickness_flag: Flag for varying thickness
"""
type: Card11ParameterType = Card11ParameterType.SELFI
temperature: float = Field(..., description="Effective temperature (K)")
temperature_uncertainty: Optional[float] = Field(None, description="Uncertainty on temperature")
thickness: float = Field(..., description="Sample thickness (atoms/barn)")
thickness_uncertainty: Optional[float] = Field(None, description="Uncertainty on thickness")
temperature_flag: VaryFlag = Field(default=VaryFlag.NO, description="Flag for temperature")
thickness_flag: VaryFlag = Field(default=VaryFlag.NO, description="Flag for thickness")
[docs]
@classmethod
def from_lines(cls, lines: List[str]) -> "SelfiParameters":
"""Parse SELFI parameters from fixed-width format lines.
Args:
lines: List of input lines (expects single line for SELFI parameters)
Returns:
SelfiParameters: Parsed parameters
Raises:
ValueError: If format is invalid or required values missing
"""
if not lines or not lines[0].strip():
raise ValueError("No valid parameter line provided")
line = f"{lines[0]:<80}" # Pad to full width
# Verify identifier
identifier = line[FORMAT_SPECS["SELFI"]["identifier"]].strip()
if identifier != "SELFI":
raise ValueError(f"Invalid identifier: {identifier}")
# Parse flags
try:
temp_flag = VaryFlag(int(line[FORMAT_SPECS["SELFI"]["flag_temp"]].strip() or "0"))
thick_flag = VaryFlag(int(line[FORMAT_SPECS["SELFI"]["flag_thick"]].strip() or "0"))
except ValueError as e:
raise ValueError(f"Invalid flag value: {e}")
# Parse required values
temp = safe_parse(line[FORMAT_SPECS["SELFI"]["temperature"]])
thick = safe_parse(line[FORMAT_SPECS["SELFI"]["thickness"]])
if temp is None or thick is None:
raise ValueError("Missing required temperature or thickness value")
# Parse optional uncertainties
temp_unc = safe_parse(line[FORMAT_SPECS["SELFI"]["temp_unc"]])
thick_unc = safe_parse(line[FORMAT_SPECS["SELFI"]["thick_unc"]])
return cls(
temperature=temp,
temperature_uncertainty=temp_unc,
thickness=thick,
thickness_uncertainty=thick_unc,
temperature_flag=temp_flag,
thickness_flag=thick_flag,
)
[docs]
def to_lines(self) -> List[str]:
"""Convert parameters to fixed-width format line.
Returns:
List containing single formatted line
"""
parts = [
"SELFI", # Identifier
" ", # Column 6 spacing
format_vary(self.temperature_flag), # Col 7
" ", # Column 8 spacing
format_vary(self.thickness_flag), # Col 9
" ", # Column 10 spacing
format_float(self.temperature, width=10),
format_float(self.temperature_uncertainty, width=10),
format_float(self.thickness, width=10),
format_float(self.thickness_uncertainty, width=10),
]
return ["".join(parts)]
[docs]
class EfficParameters(Card11Parameter):
"""Container for EFFIC (detection efficiency) parameters.
Format specification from Table VI B.2:
Cols Format Variable Description
1-5 A "EFFIC" Parameter identifier
7 I IFCAPE Flag for capture efficiency
9 I IFFISE Flag for fission efficiency
11-20 F EFCAP Efficiency for detecting capture events
21-30 F EFFIS Efficiency for detecting fission events
31-40 F dEFCAP Uncertainty on EFCAP
41-50 F dEFFIS Uncertainty on EFFIS
Attributes:
capture_efficiency: Efficiency for detecting capture events
fission_efficiency: Efficiency for detecting fission events
capture_uncertainty: Uncertainty on capture efficiency
fission_uncertainty: Uncertainty on fission efficiency
capture_flag: Flag for varying capture efficiency
fission_flag: Flag for varying fission efficiency
"""
type: Card11ParameterType = Card11ParameterType.EFFIC
capture_efficiency: float = Field(..., description="Capture detection efficiency")
fission_efficiency: float = Field(..., description="Fission detection efficiency")
capture_uncertainty: Optional[float] = Field(None, description="Uncertainty on capture efficiency")
fission_uncertainty: Optional[float] = Field(None, description="Uncertainty on fission efficiency")
capture_flag: VaryFlag = Field(default=VaryFlag.NO, description="Flag for capture efficiency")
fission_flag: VaryFlag = Field(default=VaryFlag.NO, description="Flag for fission efficiency")
[docs]
@classmethod
def from_lines(cls, lines: List[str]) -> "EfficParameters":
"""Parse EFFIC parameters from fixed-width format lines.
Args:
lines: List of input lines (expects single line for EFFIC parameters)
Returns:
EfficParameters: Parsed parameters
Raises:
ValueError: If format is invalid or required values missing
"""
if not lines or not lines[0].strip():
raise ValueError("No valid parameter line provided")
line = f"{lines[0]:<80}" # Pad to full width
# Verify identifier
identifier = line[FORMAT_SPECS["EFFIC"]["identifier"]].strip()
if identifier != "EFFIC":
raise ValueError(f"Invalid identifier: {identifier}")
# Parse flags
try:
cap_flag = VaryFlag(int(line[FORMAT_SPECS["EFFIC"]["flag_cap"]].strip() or "0"))
fis_flag = VaryFlag(int(line[FORMAT_SPECS["EFFIC"]["flag_fis"]].strip() or "0"))
except ValueError as e:
raise ValueError(f"Invalid flag value: {e}")
# Parse required values
cap_eff = safe_parse(line[FORMAT_SPECS["EFFIC"]["eff_cap"]])
fis_eff = safe_parse(line[FORMAT_SPECS["EFFIC"]["eff_fis"]])
if cap_eff is None or fis_eff is None:
raise ValueError("Missing required efficiency values")
# Parse optional uncertainties
cap_unc = safe_parse(line[FORMAT_SPECS["EFFIC"]["eff_cap_unc"]])
fis_unc = safe_parse(line[FORMAT_SPECS["EFFIC"]["eff_fis_unc"]])
return cls(
capture_efficiency=cap_eff,
fission_efficiency=fis_eff,
capture_uncertainty=cap_unc,
fission_uncertainty=fis_unc,
capture_flag=cap_flag,
fission_flag=fis_flag,
)
[docs]
def to_lines(self) -> List[str]:
"""Convert parameters to fixed-width format line.
Returns:
List containing single formatted line
"""
parts = [
"EFFIC", # Identifier
" ", # Column 6 spacing
format_vary(self.capture_flag), # Col 7
" ", # Column 8 spacing
format_vary(self.fission_flag), # Col 9
" ", # Column 10 spacing
format_float(self.capture_efficiency, width=10),
format_float(self.fission_efficiency, width=10),
format_float(self.capture_uncertainty, width=10),
format_float(self.fission_uncertainty, width=10),
]
return ["".join(parts)]
[docs]
class DelteParameters(Card11Parameter):
"""Container for DELTE (energy-dependent delta E) parameters.
Format specification from Table VI B.2:
Cols Format Variable Description
1-5 A "DELTE" Parameter identifier
7 I IFLAG1 Flag for DELE1
9 I IFLAG0 Flag for DELE0
10 I IFLAGL Flag for DELEL
11-20 F DELE1 Coefficient of E (m/eV)
21-30 F DD1 Uncertainty on DELE1
31-40 F DELE0 Constant term (m)
41-50 F DD0 Uncertainty on DELE0
51-60 F DELEL Coefficient of log term (m/ln(eV))
61-70 F DDL Uncertainty on DELEL
Attributes:
e_coefficient: Coefficient of E (m/eV)
e_uncertainty: Uncertainty on E coefficient
constant_term: Constant term (m)
constant_uncertainty: Uncertainty on constant term
log_coefficient: Coefficient of log term (m/ln(eV))
log_uncertainty: Uncertainty on log coefficient
e_flag: Flag for varying E coefficient
constant_flag: Flag for varying constant term
log_flag: Flag for varying log coefficient
"""
type: Card11ParameterType = Card11ParameterType.DELTE
e_coefficient: float = Field(..., description="Coefficient of E (m/eV)")
e_uncertainty: Optional[float] = Field(None, description="Uncertainty on E coefficient")
constant_term: float = Field(..., description="Constant term (m)")
constant_uncertainty: Optional[float] = Field(None, description="Uncertainty on constant term")
log_coefficient: float = Field(..., description="Coefficient of log term (m/ln(eV))")
log_uncertainty: Optional[float] = Field(None, description="Uncertainty on log coefficient")
e_flag: VaryFlag = Field(default=VaryFlag.NO, description="Flag for E coefficient")
constant_flag: VaryFlag = Field(default=VaryFlag.NO, description="Flag for constant term")
log_flag: VaryFlag = Field(default=VaryFlag.NO, description="Flag for log coefficient")
[docs]
@classmethod
def from_lines(cls, lines: List[str]) -> "DelteParameters":
"""Parse DELTE parameters from fixed-width format lines.
Args:
lines: List of input lines (expects single line for DELTE parameters)
Returns:
DelteParameters: Parsed parameters
Raises:
ValueError: If format is invalid or required values missing
"""
if not lines or not lines[0].strip():
raise ValueError("No valid parameter line provided")
line = f"{lines[0]:<80}" # Pad to full width
# Verify identifier
identifier = line[FORMAT_SPECS["DELTE"]["identifier"]].strip()
if identifier != "DELTE":
raise ValueError(f"Invalid identifier: {identifier}")
# Parse flags
try:
e_flag = VaryFlag(int(line[FORMAT_SPECS["DELTE"]["flag1"]].strip() or "0"))
constant_flag = VaryFlag(int(line[FORMAT_SPECS["DELTE"]["flag0"]].strip() or "0"))
log_flag = VaryFlag(int(line[FORMAT_SPECS["DELTE"]["flagl"]].strip() or "0"))
except ValueError as e:
raise ValueError(f"Invalid flag value: {e}")
# Parse required values
e_coef = safe_parse(line[FORMAT_SPECS["DELTE"]["dele1"]])
const_term = safe_parse(line[FORMAT_SPECS["DELTE"]["dele0"]])
log_coef = safe_parse(line[FORMAT_SPECS["DELTE"]["delel"]])
if e_coef is None or const_term is None or log_coef is None:
raise ValueError("Missing required coefficient values")
# Parse optional uncertainties
e_unc = safe_parse(line[FORMAT_SPECS["DELTE"]["dd1"]])
const_unc = safe_parse(line[FORMAT_SPECS["DELTE"]["dd0"]])
log_unc = safe_parse(line[FORMAT_SPECS["DELTE"]["ddl"]])
return cls(
e_coefficient=e_coef,
e_uncertainty=e_unc,
constant_term=const_term,
constant_uncertainty=const_unc,
log_coefficient=log_coef,
log_uncertainty=log_unc,
e_flag=e_flag,
constant_flag=constant_flag,
log_flag=log_flag,
)
[docs]
def to_lines(self) -> List[str]:
"""Convert parameters to fixed-width format line.
Returns:
List containing single formatted line
"""
parts = [
"DELTE", # Identifier
" ", # Column 6 spacing
format_vary(self.e_flag), # Col 7
" ", # Column 8 spacing
format_vary(self.constant_flag), # Col 9
format_vary(self.log_flag), # Col 10
format_float(self.e_coefficient, width=10),
format_float(self.e_uncertainty, width=10),
format_float(self.constant_term, width=10),
format_float(self.constant_uncertainty, width=10),
format_float(self.log_coefficient, width=10),
format_float(self.log_uncertainty, width=10),
]
return ["".join(parts)]
[docs]
class DrcapParameters(Card11Parameter):
"""Container for DRCAP (direct capture component) parameters.
Format specification from Table VI B.2:
Cols Format Variable Description
1-5 A "DRCAP" Parameter identifier
7 I IFLAG1 Flag to vary COEF
9 I NUC Nuclide Number
11-20 F COEF Coefficient of DRC file value
21-30 F dCOEF Uncertainty on COEF
Notes:
- May be included multiple times, once per nuclide
- Numerical direct capture component is read from DRC file
- COEF multiplies the value from DRC file
Attributes:
coefficient: Coefficient of the DRC file value
coefficient_uncertainty: Uncertainty on coefficient
nuclide_number: Nuclide number (must be positive)
flag: Flag for varying coefficient
"""
type: Card11ParameterType = Card11ParameterType.DRCAP
coefficient: float = Field(..., description="Coefficient of DRC file value")
coefficient_uncertainty: Optional[float] = Field(None, description="Uncertainty on coefficient")
nuclide_number: int = Field(..., gt=0, description="Nuclide number")
flag: VaryFlag = Field(default=VaryFlag.NO, description="Flag for coefficient")
[docs]
@classmethod
def from_lines(cls, lines: List[str]) -> "DrcapParameters":
"""Parse DRCAP parameters from fixed-width format lines.
Args:
lines: List of input lines (expects single line for DRCAP parameters)
Returns:
DrcapParameters: Parsed parameters
Raises:
ValueError: If format is invalid or required values missing
"""
if not lines or not lines[0].strip():
raise ValueError("No valid parameter line provided")
line = f"{lines[0]:<80}" # Pad to full width
# Verify identifier
identifier = line[FORMAT_SPECS["DRCAP"]["identifier"]].strip()
if identifier != "DRCAP":
raise ValueError(f"Invalid identifier: {identifier}")
# Parse flag
try:
flag = VaryFlag(int(line[FORMAT_SPECS["DRCAP"]["flag"]].strip() or "0"))
except ValueError as e:
raise ValueError(f"Invalid flag value: {e}")
# Parse nuclide number
nuc_num = safe_parse(line[FORMAT_SPECS["DRCAP"]["nuc"]], as_int=True)
if nuc_num is None:
raise ValueError("Missing required nuclide number")
if nuc_num <= 0:
raise ValueError("Nuclide number must be positive")
# Parse coefficient
coef = safe_parse(line[FORMAT_SPECS["DRCAP"]["coef"]])
if coef is None:
raise ValueError("Missing required coefficient value")
# Parse optional uncertainty
coef_unc = safe_parse(line[FORMAT_SPECS["DRCAP"]["dcoef"]])
return cls(
coefficient=coef,
coefficient_uncertainty=coef_unc,
nuclide_number=nuc_num,
flag=flag,
)
[docs]
def to_lines(self) -> List[str]:
"""Convert parameters to fixed-width format line.
Returns:
List containing single formatted line
"""
parts = [
"DRCAP", # Identifier
" ", # Column 6 spacing
format_vary(self.flag), # Col 7
" ", # Column 8 spacing
f"{self.nuclide_number:1d}", # Col 9
" ", # Column 10 spacing
format_float(self.coefficient, width=10),
format_float(self.coefficient_uncertainty, width=10),
]
return ["".join(parts)]
[docs]
class NonunParameters(Card11Parameter):
"""Container for NONUN (non-uniform sample thickness) parameters.
Format specification from Table VI B.2:
Cols Format Variable Description
1-5 A "NONUN" Parameter identifier
21-30 F R Radius at which thickness is given
31-40 F Z Positive value for sample thickness at radius
41-50 F dZ Uncertainty on thickness
Notes:
- At least two lines must be given (center and outer edge)
- First line must have zero radius (center)
- Last line must be at outer edge
- Lines must be in increasing radius order
- No fitting parameters yet permitted
Attributes:
radii: List of radii values (must start with 0)
thicknesses: List of thickness values at each radius
uncertainties: List of uncertainties on thicknesses (optional)
"""
type: Card11ParameterType = Card11ParameterType.NONUN
radii: List[float] = Field(..., min_length=2, description="Radii values")
thicknesses: List[float] = Field(..., min_length=2, description="Thickness values")
uncertainties: List[Optional[float]] = Field(..., min_length=2, description="Uncertainties")
[docs]
@model_validator(mode="after")
def validate_arrays(self) -> "NonunParameters":
"""Validate array lengths and values."""
if len(self.radii) != len(self.thicknesses) or len(self.radii) != len(self.uncertainties):
raise ValueError("All arrays must have same length")
if self.radii[0] != 0.0:
raise ValueError("First radius must be zero (center point)")
# Check increasing radius order
for i in range(1, len(self.radii)):
if self.radii[i] <= self.radii[i - 1]:
raise ValueError("Radii must be in strictly increasing order")
return self
[docs]
@classmethod
def from_lines(cls, lines: List[str]) -> "NonunParameters":
"""Parse NONUN parameters from fixed-width format lines.
Args:
lines: List of input lines (minimum 2 lines required)
Returns:
NonunParameters: Parsed parameters
Raises:
ValueError: If format is invalid or required values missing
"""
if not lines or len(lines) < 2:
raise ValueError("At least two lines required (center and edge)")
radii = []
thicknesses = []
uncertainties = []
for line in lines:
line = f"{line:<80}" # Pad to full width
# Verify identifier
identifier = line[FORMAT_SPECS["NONUN"]["identifier"]].strip()
if identifier != "NONUN":
raise ValueError(f"Invalid identifier: {identifier}")
# Parse values
radius = safe_parse(line[FORMAT_SPECS["NONUN"]["radius"]])
thickness = safe_parse(line[FORMAT_SPECS["NONUN"]["thickness"]])
uncertainty = safe_parse(line[FORMAT_SPECS["NONUN"]["uncertainty"]])
if radius is None or thickness is None:
raise ValueError("Missing required radius or thickness value")
radii.append(radius)
thicknesses.append(thickness)
uncertainties.append(uncertainty)
return cls(
radii=radii,
thicknesses=thicknesses,
uncertainties=uncertainties,
)
[docs]
def to_lines(self) -> List[str]:
"""Convert parameters to fixed-width format lines.
Returns:
List containing formatted lines for each radius point
"""
lines = []
for i in range(len(self.radii)):
parts = [
"NONUN", # Identifier
" " * 15, # Columns 6-20 spacing
format_float(self.radii[i], width=10),
format_float(self.thicknesses[i], width=10),
format_float(self.uncertainties[i], width=10),
]
lines.append("".join(parts))
return lines
if __name__ == "__main__":
from pleiades.utils.logger import configure_logger
configure_logger(console_level="DEBUG")
logger = loguru_logger.bind(name=__name__)
logger.info("Refer to unit tests for usage examples.")