#!/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)
41-45 I NEPNTS Points in artificial energy grid
46-50 I ITMAX Maximum iterations for Bayes' solution
51-52 I ICORR Correlation threshold x100
53-55 I NXTRA Extra points between experimental points
56-57 I IPTDOP Grid enhancement for Doppler broadening
59-60 I IPTWID Grid enhancement for resonance tails
61-70 I IXXCHN Channel skip or ZA for ENDF output
71-72 I NDIGIT Digits for compact covariance output
73-74 I IDROPP Percent threshold for zeroing covariances
75-80 I MATNUM ENDF material number
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),
"nepnts": slice(40, 45),
"itmax": slice(45, 50),
"icorr": slice(50, 52),
"nxtra": slice(52, 55),
"iptdop": slice(55, 57),
"iptwid": slice(58, 60),
"ixxchn": slice(60, 70),
"ndigit": slice(70, 72),
"idropp": slice(72, 74),
"matnum": slice(74, 80),
}
[docs]
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
nepnts: Points in artificial energy grid
itmax: Maximum iterations for Bayes' solution
icorr: Correlation threshold x100
nxtra: Extra points between experimental points
iptdop: Grid enhancement for Doppler broadening
iptwid: Grid enhancement for resonance tails
ixxchn: Channel skip or ZA for ENDF output
ndigit: Digits for compact covariance output
idropp: Percent threshold for zeroing covariances
matnum: ENDF material number
"""
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)
nepnts: int | None = Field(default=None, description="Points in artificial energy grid")
itmax: int | None = Field(default=None, description="Maximum iterations for Bayes' solution")
icorr: int | None = Field(default=None, description="Correlation threshold x100")
nxtra: int | None = Field(default=None, description="Extra points between experimental points")
iptdop: int | None = Field(default=None, description="Grid enhancement for Doppler broadening")
iptwid: int | None = Field(default=None, description="Grid enhancement for resonance tails")
ixxchn: int | None = Field(default=None, description="Channel skip or ZA for ENDF output")
ndigit: int | None = Field(default=None, description="Digits for compact covariance output")
idropp: int | None = Field(default=None, description="Percent threshold for zeroing covariances")
matnum: int | None = Field(default=None, description="ENDF material number")
[docs]
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})")
[docs]
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.
"""
[docs]
@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) < 80:
line = f"{line:<80}"
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())
def parse_int(value: str) -> int | None:
stripped = value.strip()
return int(stripped) if stripped else None
nepnts = parse_int(line[CARD02_FORMAT["nepnts"]])
itmax = parse_int(line[CARD02_FORMAT["itmax"]])
icorr = parse_int(line[CARD02_FORMAT["icorr"]])
nxtra = parse_int(line[CARD02_FORMAT["nxtra"]])
iptdop = parse_int(line[CARD02_FORMAT["iptdop"]])
iptwid = parse_int(line[CARD02_FORMAT["iptwid"]])
ixxchn = parse_int(line[CARD02_FORMAT["ixxchn"]])
ndigit = parse_int(line[CARD02_FORMAT["ndigit"]])
idropp = parse_int(line[CARD02_FORMAT["idropp"]])
matnum = parse_int(line[CARD02_FORMAT["matnum"]])
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,
nepnts=nepnts,
itmax=itmax,
icorr=icorr,
nxtra=nxtra,
iptdop=iptdop,
iptwid=iptwid,
ixxchn=ixxchn,
ndigit=ndigit,
idropp=idropp,
matnum=matnum,
)
[docs]
@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}{element_info.atomic_weight:10.6f}"
def format_energy(value: float, fixed_format: str) -> str:
if value != 0.0 and abs(value) < 1.0e-3:
return f"{value:10.3E}"
return format(value, fixed_format)
line += format_energy(element_info.min_energy, "10.3f") + format_energy(element_info.max_energy, "10.1f")
extra_fields = (
element_info.nepnts,
element_info.itmax,
element_info.icorr,
element_info.nxtra,
element_info.iptdop,
element_info.iptwid,
element_info.ixxchn,
element_info.ndigit,
element_info.idropp,
element_info.matnum,
)
if any(value is not None for value in extra_fields):
def fmt_int(value: int | None, width: int) -> str:
if value is None:
return " " * width
return f"{value:>{width}d}"
line += (
f"{fmt_int(element_info.nepnts, 5)}"
f"{fmt_int(element_info.itmax, 5)}"
f"{fmt_int(element_info.icorr, 2)}"
f"{fmt_int(element_info.nxtra, 3)}"
f"{fmt_int(element_info.iptdop, 2)}"
f" "
f"{fmt_int(element_info.iptwid, 2)}"
f"{fmt_int(element_info.ixxchn, 10)}"
f"{fmt_int(element_info.ndigit, 2)}"
f"{fmt_int(element_info.idropp, 2)}"
f"{fmt_int(element_info.matnum, 6)}"
)
return [line]