feat: add CLI options for custom CSV separator and normalize-to-one branch normalization

This commit is contained in:
2026-07-06 17:32:56 +03:00
parent 1b4d418b5f
commit 6dc1e3924d
5 changed files with 78 additions and 11 deletions
+17
View File
@@ -152,6 +152,17 @@ def _build_parser() -> argparse.ArgumentParser:
choices=["full", "concise"], choices=["full", "concise"],
help="CSV output verbosity: 'full' (all components) or 'concise' (total spectrum and normalized branch components only) (default: full)", help="CSV output verbosity: 'full' (all components) or 'concise' (total spectrum and normalized branch components only) (default: full)",
) )
log_group.add_argument(
"--csv-separator",
type=str,
default=',',
help="CSV column separator (default: ','). Use 'tab' for tab separation.",
)
log_group.add_argument(
"--normalize-to-one",
action="store_true",
help="Normalize each branch to 1, ignoring intensity. In this case, total spectrum will not be produced (only energy and branch-wise spectra).",
)
return parser return parser
@@ -253,6 +264,8 @@ def _run(args: argparse.Namespace) -> None:
neutrino_mass_MeV=args.neutrino_mass, neutrino_mass_MeV=args.neutrino_mass,
branches=branches, branches=branches,
intensity_cutoff=args.intensity_cutoff, intensity_cutoff=args.intensity_cutoff,
csv_separator=args.csv_separator,
normalize_to_one=args.normalize_to_one,
) )
else: else:
config = create_config_from_source( config = create_config_from_source(
@@ -261,6 +274,8 @@ def _run(args: argparse.Namespace) -> None:
e_step_MeV=args.e_step, e_step_MeV=args.e_step,
intensity_cutoff=args.intensity_cutoff, intensity_cutoff=args.intensity_cutoff,
neutrino_mass_MeV=args.neutrino_mass, neutrino_mass_MeV=args.neutrino_mass,
csv_separator=args.csv_separator,
normalize_to_one=args.normalize_to_one,
) )
else: else:
config = create_config_from_source( config = create_config_from_source(
@@ -269,6 +284,8 @@ def _run(args: argparse.Namespace) -> None:
e_step_MeV=args.e_step, e_step_MeV=args.e_step,
intensity_cutoff=args.intensity_cutoff, intensity_cutoff=args.intensity_cutoff,
neutrino_mass_MeV=args.neutrino_mass, neutrino_mass_MeV=args.neutrino_mass,
csv_separator=args.csv_separator,
normalize_to_one=args.normalize_to_one,
) )
else: else:
logger.info("Source: JSON file=%s", source_value) logger.info("Source: JSON file=%s", source_value)
+12
View File
@@ -471,6 +471,8 @@ def decay_info_to_config(
e_step_MeV: float = 0.001, e_step_MeV: float = 0.001,
intensity_cutoff: float = 0.0, intensity_cutoff: float = 0.0,
neutrino_mass_MeV: float = 0.0, neutrino_mass_MeV: float = 0.0,
csv_separator: str = ',',
normalize_to_one: bool = False,
) -> SpectrumConfig: ) -> SpectrumConfig:
""" """
Convert DecayInfo to a SpectrumConfig. Convert DecayInfo to a SpectrumConfig.
@@ -485,6 +487,10 @@ def decay_info_to_config(
Minimum branch intensity fraction to include. Minimum branch intensity fraction to include.
neutrino_mass_MeV : float neutrino_mass_MeV : float
Neutrino mass in MeV. Neutrino mass in MeV.
csv_separator : str
CSV column separator.
normalize_to_one : bool
If True, normalize each branch to 1, ignoring intensity.
Returns Returns
------- -------
@@ -516,6 +522,8 @@ def decay_info_to_config(
neutrino_mass_MeV=neutrino_mass_MeV, neutrino_mass_MeV=neutrino_mass_MeV,
branches=branches, branches=branches,
intensity_cutoff=intensity_cutoff, intensity_cutoff=intensity_cutoff,
csv_separator=csv_separator,
normalize_to_one=normalize_to_one,
) )
@@ -690,12 +698,16 @@ def create_config_from_source(
e_step = kwargs.get("e_step_MeV", 0.001) e_step = kwargs.get("e_step_MeV", 0.001)
intensity_cutoff = kwargs.get("intensity_cutoff", 0.0) intensity_cutoff = kwargs.get("intensity_cutoff", 0.0)
neutrino_mass = kwargs.get("neutrino_mass_MeV", 0.0) neutrino_mass = kwargs.get("neutrino_mass_MeV", 0.0)
csv_separator = kwargs.get("csv_separator", ',')
normalize_to_one = kwargs.get("normalize_to_one", False)
info = get_decay_info_from_paceENSDF(nuclide, decay_type, decay_index) info = get_decay_info_from_paceENSDF(nuclide, decay_type, decay_index)
return decay_info_to_config( return decay_info_to_config(
info, info,
e_step_MeV=e_step, e_step_MeV=e_step,
intensity_cutoff=intensity_cutoff, intensity_cutoff=intensity_cutoff,
neutrino_mass_MeV=neutrino_mass, neutrino_mass_MeV=neutrino_mass,
csv_separator=csv_separator,
normalize_to_one=normalize_to_one,
) )
elif source == "json": elif source == "json":
if not json_path: if not json_path:
+6
View File
@@ -88,6 +88,10 @@ class SpectrumConfig:
0.0 # Ignore branches below this fraction (default: 0.0 = all) 0.0 # Ignore branches below this fraction (default: 0.0 = all)
) )
# CSV output options
csv_separator: str = ',' # Default separator for CSV output
normalize_to_one: bool = False # If True, normalize each branch to 1, ignoring intensity
class BetaSpectrum: class BetaSpectrum:
""" """
@@ -321,6 +325,8 @@ class BetaSpectrum:
# Create per-branch calculators # Create per-branch calculators
branch_spectra: List[BetaSpectrum] = [] branch_spectra: List[BetaSpectrum] = []
if config.branches is None:
raise ValueError("branches must be provided for multi-branch mode")
for i, branch in enumerate(config.branches): for i, branch in enumerate(config.branches):
if logger: if logger:
logger.info( logger.info(
+40 -8
View File
@@ -731,7 +731,10 @@ class BetaSpectrumAnalyzer:
header_lines.append(f"# n_points: {len(self.energies_MeV)}") header_lines.append(f"# n_points: {len(self.energies_MeV)}")
# Add normalization mode # Add normalization mode
header_lines.append("# branch normalization: intensity") if self.config.normalize_to_one:
header_lines.append("# branch normalization: to_one (ignoring intensity)")
else:
header_lines.append("# branch normalization: intensity")
# Separate branches into active and excluded lists # Separate branches into active and excluded lists
active_branches = [] active_branches = []
@@ -771,13 +774,31 @@ class BetaSpectrumAnalyzer:
# Create data dictionary with concise column headers # Create data dictionary with concise column headers
data: Dict[str, np.ndarray] = { data: Dict[str, np.ndarray] = {
"energy_MeV": self.energies_MeV, "energy_MeV": self.energies_MeV,
"total_spectrum": total,
} }
# Only add total_spectrum if not normalizing to one
if not self.config.normalize_to_one:
data["total_spectrum"] = total
if csv_verbosity == "full": if csv_verbosity == "full":
# Per-branch spectra and components with concise branch naming # Per-branch spectra and components with concise branch naming
for i in range(self._n_branches): for i in range(self._n_branches):
data[f"branch_{i + 1}_spectrum"] = self._get_branch_spectrum(i) # Get branch spectrum
branch_spectrum = self._get_branch_spectrum(i)
if self.config.normalize_to_one:
# Normalize to 1, ignoring intensity
sum_spectrum = np.sum(branch_spectrum)
if sum_spectrum > 0:
normalized_spectrum = branch_spectrum / sum_spectrum
else:
normalized_spectrum = branch_spectrum
else:
# Use original spectrum with intensity
normalized_spectrum = branch_spectrum * self._unified_branches[i].intensity
data[f"branch_{i + 1}_spectrum"] = normalized_spectrum
for comp_name in ["PhaseSpace", "Radiative"]: for comp_name in ["PhaseSpace", "Radiative"]:
key = self._get_component_key(i, comp_name) key = self._get_component_key(i, comp_name)
if key in components: if key in components:
@@ -799,14 +820,25 @@ class BetaSpectrumAnalyzer:
data[f"branch_{i + 1}"] = np.zeros_like(branch_spectrum) data[f"branch_{i + 1}"] = np.zeros_like(branch_spectrum)
else: else:
# Branch is valid, normalize as usual # Branch is valid, normalize as usual
sum_spectrum = np.sum(branch_spectrum) if self.config.normalize_to_one:
if sum_spectrum > 0: # Normalize to 1, ignoring intensity
data[f"branch_{i + 1}"] = branch_spectrum / sum_spectrum sum_spectrum = np.sum(branch_spectrum)
if sum_spectrum > 0:
data[f"branch_{i + 1}"] = branch_spectrum / sum_spectrum
else:
data[f"branch_{i + 1}"] = branch_spectrum
else: else:
data[f"branch_{i + 1}"] = branch_spectrum # Normalize as usual with intensity
sum_spectrum = np.sum(branch_spectrum)
if sum_spectrum > 0:
data[f"branch_{i + 1}"] = branch_spectrum / sum_spectrum * self._unified_branches[i].intensity
else:
data[f"branch_{i + 1}"] = branch_spectrum
df = pd.DataFrame(data) df = pd.DataFrame(data)
df.to_csv(filename, index=False, float_format="%.4e", sep='\t') # Determine separator
separator = '\t' if self.config.csv_separator == 'tab' else self.config.csv_separator
df.to_csv(filename, index=False, float_format="%.4e", sep=separator)
with open(filename, "r") as f: with open(filename, "r") as f:
content = f.read() content = f.read()
+3 -3
View File
@@ -460,8 +460,8 @@ class TestMultiBranchCSVExport:
import pandas as pd import pandas as pd
# Read CSV (skip comment headers, use tab delimiter) # Read CSV (skip comment headers, use comma delimiter by default)
df = pd.read_csv(csv_path, comment="#", sep='\t') df = pd.read_csv(csv_path, comment="#", sep=',')
# Should have total spectrum column # Should have total spectrum column
assert "total_spectrum" in df.columns assert "total_spectrum" in df.columns
@@ -500,7 +500,7 @@ class TestMultiBranchCSVExport:
import pandas as pd import pandas as pd
df = pd.read_csv(csv_path, comment="#", sep='\t') df = pd.read_csv(csv_path, comment="#", sep=',')
assert "total_spectrum" in df.columns assert "total_spectrum" in df.columns
assert "branch_1_spectrum" in df.columns assert "branch_1_spectrum" in df.columns