"""
Module for managing SAMMY input (.inp) files.
This module provides the InpManager class to generate and write SAMMY input files
based on FitOptions configurations. It supports different modes (ENDF, fitting, custom)
through the refactored FitOptions class and its factory methods.
"""
from pathlib import Path
from typing import Dict, List, Optional
from pleiades.sammy.fitting.options import FitOptions
from pleiades.sammy.io.card_formats.inp02_element import Card02, ElementInfo
from pleiades.sammy.io.card_formats.inp03_constants import Card03, PhysicalConstants
from pleiades.sammy.io.card_formats.inp03_density import Card03Density, SampleDensity
from pleiades.utils.logger import loguru_logger
logger = loguru_logger.bind(name=__name__)
# Default constants for SAMMY parameters
DEFAULT_DENSITY = 9.000000 # Default material density (g/cm³)
DEFAULT_NUMBER_DENSITY = 1.797e-03 # Default number density (atoms/barn-cm)
# TZERO default parameters
DEFAULT_T0_VALUE = 0.86000000 # Time offset t₀ (μs)
DEFAULT_T0_UNCERTAINTY = 0.00200000 # Uncertainty on t₀ (μs)
DEFAULT_L0_VALUE = 1.0020000 # L₀ value (dimensionless)
DEFAULT_L0_UNCERTAINTY = 2.00000e-5 # Uncertainty on L₀
[docs]
class InpManager:
"""
Manages creation and writing of SAMMY input (.inp) files.
This class takes a FitOptions object and generates a SAMMY input file
with the appropriate commands based on the selected options.
Attributes:
options (FitOptions): Configuration options for SAMMY
title (str, optional): Title line for the inp file
isotope_info (Dict, optional): Dictionary with isotope information
physical_constants (Dict, optional): Dictionary with physical constants
reaction_type (str, optional): Reaction type (transmission, capture, etc.)
"""
[docs]
def __init__(
self,
options: FitOptions = None,
title: str = None,
isotope_info: Optional[Dict] = None,
physical_constants: Optional[Dict] = None,
reaction_type: str = None,
):
"""
Initialize with optional FitOptions and section information.
Args:
options: FitOptions instance containing SAMMY configuration
title: Title/description for the inp file
isotope_info: Isotope information (name, mass, energy range)
physical_constants: Physical constants (temperature, flight path, etc.)
reaction_type: Reaction type (transmission, capture, etc.)
"""
self.options = options or FitOptions()
self.title = title
self.isotope_info = isotope_info
self.physical_constants = physical_constants
self.reaction_type = reaction_type
[docs]
def set_options(self, options: FitOptions) -> None:
"""
Set FitOptions after initialization.
Args:
options: FitOptions instance to use
"""
self.options = options
[docs]
def generate_commands(self) -> List[str]:
"""
Generate SAMMY input commands from options.
Returns:
List[str]: List of SAMMY alphanumeric commands
"""
return self.options.get_alphanumeric_commands()
[docs]
def generate_title_section(self) -> str:
"""
Generate the title section of the inp file.
Returns:
str: Title line (defaults to "SAMMY analysis" if not provided)
"""
if self.title:
return self.title
return "SAMMY analysis"
[docs]
def generate_isotope_section(self) -> str:
"""
Generate the isotope information section of the inp file using Card Set 2 format.
Returns:
str: Properly formatted Card Set 2 element information line
"""
if self.isotope_info:
element = self.isotope_info.get("element", "Sample")
atomic_mass = self.isotope_info.get("atomic_mass_amu", 1.0)
min_energy = self.isotope_info.get("min_energy_eV", 0.001)
max_energy = self.isotope_info.get("max_energy_eV", 1000.0)
element_info = ElementInfo(
element=element,
atomic_weight=atomic_mass,
min_energy=min_energy,
max_energy=max_energy,
)
else:
element_info = ElementInfo(
element="Sample",
atomic_weight=1.0,
min_energy=0.001,
max_energy=1000.0,
)
lines = Card02.to_lines(element_info)
return lines[0]
[docs]
def generate_physical_constants_section(self, material_properties: Dict = None) -> str:
"""
Generate the physical constants section for multi-isotope mode.
Args:
material_properties: Dict with material properties
Returns:
str: Physical constants line
"""
if material_properties:
temperature = material_properties.get("temperature_K", 293.6)
flight_path = material_properties.get("flight_path_m", 25.0)
delta_l = material_properties.get("delta_l", 0.0)
delta_g = material_properties.get("delta_g", 0.0)
delta_e = material_properties.get("delta_e", 0.0)
constants = PhysicalConstants(
temperature=temperature,
flight_path_length=flight_path,
delta_l=delta_l,
delta_g=delta_g,
delta_e=delta_e,
)
else:
constants = PhysicalConstants(
temperature=293.6,
flight_path_length=25.0,
delta_l=0.0,
delta_g=0.0,
delta_e=0.0,
)
lines = Card03.to_lines(constants)
return "\n" + lines[0]
[docs]
def generate_sample_density_section(self, material_properties: Dict = None) -> str:
"""
Generate the sample density section.
Args:
material_properties: Dict with material properties
Returns:
str: Sample density line with density (g/cm3) and number density (atoms/barn)
"""
if material_properties:
from pleiades.utils.units import calculate_number_density
density = material_properties.get("density_g_cm3", 9.0)
thickness_mm = material_properties.get("thickness_mm", 5.0)
atomic_mass = material_properties.get("atomic_mass_amu", 28.0)
number_density = calculate_number_density(density, thickness_mm, atomic_mass)
sample_density = SampleDensity(
density=density,
number_density=number_density,
)
else:
sample_density = SampleDensity(
density=DEFAULT_DENSITY,
number_density=DEFAULT_NUMBER_DENSITY,
)
lines = Card03Density.to_lines(sample_density)
return lines[0]
[docs]
def generate_reaction_type_section(self) -> str:
"""
Generate the reaction type section of the inp file.
Returns:
str: Reaction type (defaults to "transmission" if not provided)
"""
if self.reaction_type:
return self.reaction_type
return "transmission"
[docs]
def generate_card_set_2_element_info(self, material_properties: Dict = None) -> str:
"""
Generate Card Set 2 (element information) according to SAMMY documentation.
This generates the second line with element name, atomic weight, and energy range
according to SAMMY Card Set 2 specification.
Args:
material_properties: Dict with material properties including element info
Returns:
str: Properly formatted Card Set 2 element information line
"""
if material_properties:
element = material_properties.get("element", "Au")
mass_number = material_properties.get("mass_number", 197)
atomic_mass = material_properties.get("atomic_mass_amu", 196.966569)
min_energy = material_properties.get("min_energy_eV", 0.001)
max_energy = material_properties.get("max_energy_eV", 1000.0)
element_name = f"{element}{mass_number}"
element_info = ElementInfo(
element=element_name,
atomic_weight=atomic_mass,
min_energy=min_energy,
max_energy=max_energy,
)
else:
element_info = ElementInfo(
element="Au197",
atomic_weight=196.96657,
min_energy=0.001,
max_energy=1000.0,
)
lines = Card02.to_lines(element_info)
return lines[0]
[docs]
def generate_broadening_parameters_section(self, material_properties: Dict = None) -> str:
"""
Generate broadening parameters section for multi-isotope mode.
Args:
material_properties: Dict with material properties for calculations
Returns:
str: Broadening parameters section with required blank line before it
"""
if material_properties:
from pleiades.experimental.models import BroadeningParameters
from pleiades.sammy.fitting.config import FitConfig
from pleiades.sammy.io.card_formats.par04_broadening import Card04
from pleiades.utils.helper import VaryFlag
from pleiades.utils.units import calculate_number_density
# Extract and validate material properties
density = material_properties.get("density_g_cm3")
thickness = material_properties.get("thickness_mm", 5.0)
atomic_mass = material_properties.get("atomic_mass_amu")
temperature = material_properties.get("temperature_K", 293.6)
if density is None or atomic_mass is None:
raise ValueError("material_properties must contain 'density_g_cm3' and 'atomic_mass_amu'")
# Calculate number density
number_density = calculate_number_density(density, thickness, atomic_mass)
# Create FitConfig with broadening parameters using proper Card04
fit_config = FitConfig()
# Create BroadeningParameters object
broadening_params = BroadeningParameters(
crfn=8.0, # Matching radius
temp=temperature, # Temperature
thick=number_density, # Calculated number density
deltal=0.0, # Flight path spread
deltag=0.0, # Gaussian resolution
deltae=0.0, # Exponential resolution
flag_thick=VaryFlag.YES, # Allow SAMMY to vary thickness
)
# Add to fit_config
fit_config.physics_params.broadening_parameters = broadening_params
# Generate proper Card04 output with required blank line before it
lines = [""] + Card04.to_lines(fit_config) # Add blank line before broadening section
return "\n".join(lines)
return "" # Return empty string when no broadening parameters
[docs]
def generate_misc_parameters_section(self, flight_path_m: float = 25.0) -> str:
"""
Generate miscellaneous parameters (TZERO) section.
Args:
flight_path_m: Flight path length in meters (default 25.0 for VENUS)
Returns:
str: Miscellaneous parameters section
"""
from pleiades.sammy.parameters.misc import TzeroParameters
from pleiades.utils.helper import VaryFlag
# Create TzeroParameters with proper values
# TZERO values (rounded uncertainties required - SAMMY cannot use zero uncertainty)
tzero_params = TzeroParameters(
t0_value=DEFAULT_T0_VALUE, # Time offset t₀ (μs)
t0_uncertainty=DEFAULT_T0_UNCERTAINTY, # Uncertainty on t₀ (μs)
l0_value=DEFAULT_L0_VALUE, # L₀ value (dimensionless)
l0_uncertainty=DEFAULT_L0_UNCERTAINTY, # Uncertainty on L₀
flight_path_length=flight_path_m, # Flight path (m)
t0_flag=VaryFlag.YES, # Allow SAMMY to vary t₀
l0_flag=VaryFlag.YES, # Allow SAMMY to vary L₀
)
# Generate proper TZERO output with header
lines = [
"", # Empty line
"MISCEllaneous parameters follow",
] + tzero_params.to_lines()
return "\n".join(lines)
[docs]
def generate_normalization_parameters_section(self) -> str:
"""
Generate normalization parameters section.
Returns:
str: Normalization parameters section with required blank line before it
"""
from pleiades.experimental.models import NormalizationParameters
from pleiades.sammy.fitting.config import FitConfig
from pleiades.sammy.io.card_formats.par06_normalization import Card06
from pleiades.utils.helper import VaryFlag
# Create FitConfig with normalization parameters using proper Card06
fit_config = FitConfig()
# Create NormalizationParameters object
# NORM values (non-zero uncertainties required - SAMMY cannot fit parameters with zero uncertainty)
norm_params = NormalizationParameters(
anorm=1.0, # Normalization factor
backa=0.01000000, # Constant background
backb=0.02000000, # Background ∝ 1/E
backc=0.00100000, # Background ∝ √E
backd=0.0, # Exponential background coefficient
backf=0.0, # Exponential decay constant
flag_anorm=VaryFlag.YES, # Allow SAMMY to vary normalization
flag_backa=VaryFlag.YES, # Allow SAMMY to vary constant background
flag_backb=VaryFlag.YES, # Allow SAMMY to vary 1/E background
flag_backc=VaryFlag.YES, # Allow SAMMY to vary √E background
flag_backd=VaryFlag.NO, # Don't vary exponential coefficient
flag_backf=VaryFlag.NO, # Don't vary exponential constant
)
# Add to fit_config
fit_config.physics_params.normalization_parameters = norm_params
# Generate proper Card06 output with required blank line before it
lines = [""] + Card06.to_lines(fit_config) # Add blank line before normalization section
return "\n".join(lines)
[docs]
def generate_resolution_function_section(self, resolution_file: str = "venus_resolution.dat") -> str:
"""
Generate user-defined resolution function section.
Args:
resolution_file: Name of resolution function file, or None to disable
Returns:
str: Resolution function section or empty string if disabled
"""
if resolution_file is None:
return ""
from pleiades.sammy.parameters.user_resolution import UserResolutionParameters
user_res = UserResolutionParameters(filenames=[resolution_file])
lines = user_res.to_lines()
return "\n" + "\n".join(lines)
[docs]
def generate_multi_isotope_inp_content(
self, material_properties: Dict = None, resolution_file_path: Path = None
) -> str:
"""
Generate complete multi-isotope INP content with parameter sections.
Args:
material_properties: Dict with material properties for parameter calculations
resolution_file_path: Optional absolute path to resolution function file
Returns:
str: Complete multi-isotope INP file content
"""
sections = [
self.generate_title_section(),
self.generate_card_set_2_element_info(material_properties), # Use Card Set 2 for element info
"\n".join(self.generate_commands()),
self.generate_physical_constants_section(material_properties),
self.generate_sample_density_section(material_properties),
self.generate_reaction_type_section(),
self.generate_broadening_parameters_section(material_properties),
self.generate_misc_parameters_section(),
self.generate_normalization_parameters_section(),
self.generate_resolution_function_section(
str(resolution_file_path.resolve()) if resolution_file_path else None
),
]
return "\n".join(sections)
[docs]
def generate_inp_content(self) -> str:
"""
Generate full content for SAMMY input file.
Returns:
str: Complete content for SAMMY input file
"""
sections = [
self.generate_title_section(),
self.generate_isotope_section(),
"\n".join(self.generate_commands()),
"", # Empty line for readability
self.generate_physical_constants_section(),
self.generate_reaction_type_section(),
]
return "\n".join(sections)
[docs]
def write_inp_file(self, file_path: Path) -> Path:
"""
Write a SAMMY input file to disk.
Args:
file_path: Path where the input file should be written
Returns:
Path: Path to the created file
Raises:
IOError: If file cannot be written
"""
try:
content = self.generate_inp_content()
file_path = Path(file_path)
file_path.parent.mkdir(parents=True, exist_ok=True)
with open(file_path, "w") as f:
f.write(content)
logger.info(f"Successfully wrote SAMMY input file to {file_path}")
return file_path
except Exception as e:
logger.error(f"Failed to write SAMMY input file: {str(e)}")
raise IOError(f"Failed to write SAMMY input file: {str(e)}")
[docs]
@classmethod
def create_endf_inp(cls, output_path: Path, title: str = None) -> Path:
"""
Create input file for ENDF mode.
Args:
output_path: Path to write the input file
title: Optional title for the inp file
Returns:
Path: Path to the created file
"""
options = FitOptions.from_endf_config()
manager = cls(options, title=title or "ENDF extraction mode")
return manager.write_inp_file(output_path)
[docs]
@classmethod
def create_fitting_inp(cls, output_path: Path, title: str = None) -> Path:
"""
Create input file for fitting mode.
Args:
output_path: Path to write the input file
title: Optional title for the inp file
Returns:
Path: Path to the created file
"""
options = FitOptions.from_fitting_config()
manager = cls(options, title=title or "Bayesian fitting mode")
return manager.write_inp_file(output_path)
[docs]
@classmethod
def create_multi_isotope_inp(
cls, output_path: Path, title: str = None, material_properties: Dict = None, resolution_file_path: Path = None
) -> Path:
"""
Create input file for multi-isotope JSON mode fitting.
This method generates a complete INP file for multi-isotope fitting that includes
both static alphanumeric commands and calculated parameter sections.
Args:
output_path: Path to write the input file
title: Optional title for the inp file
material_properties: Optional dict with material properties for parameter calculations
resolution_file_path: Optional absolute path to resolution function file
Returns:
Path: Path to the created file
"""
options = FitOptions.from_multi_isotope_config()
manager = cls(options, title=title or "Multi-isotope JSON mode fitting", reaction_type="transmission")
# Use specialized multi-isotope content generation
try:
content = manager.generate_multi_isotope_inp_content(material_properties, resolution_file_path)
output_path = Path(output_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w") as f:
f.write(content)
logger.info(f"Successfully wrote multi-isotope SAMMY input file to {output_path}")
return output_path
except Exception as e:
logger.error(f"Failed to write multi-isotope SAMMY input file: {str(e)}")
raise