Source code for pleiades.sammy.parameters.user_resolution

#!/usr/bin/env python
"""Parser for SAMMY's Card Set 16 (User-Defined Resolution Function) parameters.

Format specification from Table VI B.2:
Card Set 16 defines user-defined resolution functions with the following components:

1. Header line ("USER-Defined resolution function")
2. Optional BURST line for burst width parameters
3. Optional CHANN lines for channel-dependent parameters
4. Required FILE= lines specifying data files

Each section has specific fixed-width format requirements.
"""

from enum import Enum
from typing import List, Optional

from pydantic import BaseModel, Field

from pleiades.utils.helper import VaryFlag, format_float, format_vary, safe_parse

# Format definitions - column positions for each parameter type
FORMAT_SPECS = {
    "BURST": {
        "identifier": slice(0, 5),  # "BURST"
        "flag": slice(6, 7),  # IFBRST
        "width": slice(10, 20),  # BURST value
        "uncertainty": slice(20, 30),  # dBURST
    },
    "CHANN": {
        "identifier": slice(0, 5),  # "CHANN"
        "flag": slice(6, 7),  # ICH flag
        "energy": slice(10, 20),  # ECRNCH
        "width": slice(20, 30),  # CH value
        "uncertainty": slice(30, 40),  # dCH
    },
    "FILE": {
        "identifier": slice(0, 5),  # "FILE="
        "name": slice(5, 75),  # Filename
    },
}


[docs] class Card16ParameterType(str, Enum): """Enumeration of Card 16 parameter types.""" USER = "USER" # Header identifier BURST = "BURST" CHANN = "CHANN" FILE = "FILE="
[docs] class UserResolutionParameters(BaseModel): """Container for User-Defined Resolution Function parameters. Attributes: type: Parameter type identifier (always "USER") burst_width: Square burst width value (ns), optional burst_uncertainty: Uncertainty on burst width, optional burst_flag: Flag for varying burst width channel_energies: List of energies for channel widths channel_widths: List of channel width values channel_uncertainties: List of uncertainties on channel widths channel_flags: List of flags for varying channel widths filenames: List of data file names """ type: Card16ParameterType = Card16ParameterType.USER burst_width: Optional[float] = Field(None, description="Square burst width (ns)") burst_uncertainty: Optional[float] = Field(None, description="Uncertainty on burst width") burst_flag: VaryFlag = Field(default=VaryFlag.NO, description="Flag for burst width") channel_energies: List[float] = Field(default_factory=list, description="Energies for channels") channel_widths: List[float] = Field(default_factory=list, description="Channel width values") channel_uncertainties: List[Optional[float]] = Field(default_factory=list, description="Channel uncertainties") channel_flags: List[VaryFlag] = Field(default_factory=list, description="Channel flags") filenames: List[str] = Field(default_factory=list, description="Resolution function filenames")
[docs] @classmethod def from_lines(cls, lines: List[str]) -> "UserResolutionParameters": """Parse user resolution parameters from fixed-width format lines.""" if not lines: raise ValueError("No lines provided") # Verify header line header = lines[0].strip() if not header.startswith("USER-Defined"): raise ValueError(f"Invalid header: {header}") # Initialize parameters params = cls() # Process remaining lines current_line = 1 while current_line < len(lines): line = f"{lines[current_line]:<80}" # Pad to full width # Skip blank lines if not line.strip(): current_line += 1 continue # Parse BURST line if line.startswith("BURST"): # Verify identifier identifier = line[FORMAT_SPECS["BURST"]["identifier"]].strip() if identifier != "BURST": raise ValueError(f"Invalid BURST identifier: {identifier}") # Parse flag try: burst_flag = VaryFlag(int(line[FORMAT_SPECS["BURST"]["flag"]].strip() or "0")) except ValueError as e: raise ValueError(f"Invalid BURST flag value: {e}") # Parse width and uncertainty burst_width = safe_parse(line[FORMAT_SPECS["BURST"]["width"]]) if burst_width is None: raise ValueError("Missing required burst width value") burst_uncertainty = safe_parse(line[FORMAT_SPECS["BURST"]["uncertainty"]]) params.burst_width = burst_width params.burst_uncertainty = burst_uncertainty params.burst_flag = burst_flag elif line.startswith("CHANN"): # Verify identifier identifier = line[FORMAT_SPECS["CHANN"]["identifier"]].strip() if identifier != "CHANN": raise ValueError(f"Invalid CHANN identifier: {identifier}") # Parse flag try: flag = VaryFlag(int(line[FORMAT_SPECS["CHANN"]["flag"]].strip() or "0")) except ValueError as e: raise ValueError(f"Invalid CHANN flag value: {e}") # Parse required values energy = safe_parse(line[FORMAT_SPECS["CHANN"]["energy"]]) width = safe_parse(line[FORMAT_SPECS["CHANN"]["width"]]) if energy is None or width is None: raise ValueError("Missing required energy or width value") # Parse optional uncertainty uncertainty = safe_parse(line[FORMAT_SPECS["CHANN"]["uncertainty"]]) # Append values to lists params.channel_energies.append(energy) params.channel_widths.append(width) params.channel_uncertainties.append(uncertainty) params.channel_flags.append(flag) elif line.startswith("FILE="): # Verify identifier and format identifier = line[FORMAT_SPECS["FILE"]["identifier"]].strip() if identifier != "FILE=": raise ValueError(f"Invalid FILE identifier: {identifier}") # Get raw filename by removing "FILE=" prefix raw_filename = line[5:].strip() # Everything after FILE= if not raw_filename: raise ValueError("Missing filename") # Check raw filename length before column slicing if len(raw_filename) > 70: raise ValueError(f"Filename length ({len(raw_filename)}) exceeds maximum length (70 characters)") # Now we can safely use the column slice knowing it won't truncate valid data filename = line[FORMAT_SPECS["FILE"]["name"]].strip() params.filenames.append(filename) else: raise ValueError(f"Invalid line type: {line.strip()}") current_line += 1 return params
[docs] def to_lines(self) -> List[str]: """Convert parameters to fixed-width format lines.""" lines = ["USER-Defined resolution function"] # Add BURST line if parameters present if self.burst_width is not None: burst_parts = [ "BURST", # Identifier " ", # Column 6 spacing format_vary(self.burst_flag), # Col 7 " ", # Columns 8-10 spacing format_float(self.burst_width, width=10), format_float(self.burst_uncertainty, width=10), ] lines.append("".join(burst_parts)) # Add CHANN lines if parameters present for i in range(len(self.channel_energies)): channel_parts = [ "CHANN", # Identifier " ", # Column 6 spacing format_vary(self.channel_flags[i]), # Col 7 " ", # Columns 8-10 spacing format_float(self.channel_energies[i], width=10), format_float(self.channel_widths[i], width=10), format_float(self.channel_uncertainties[i], width=10), ] lines.append("".join(channel_parts)) # Add FILE lines if present for filename in self.filenames: lines.append(f"FILE={filename}") # Add required blank line at end per spec lines.append("") return lines
if __name__ == "__main__": print("Refer to unit tests for usage examples.")