feat(cli): propagate export options through JSON/paceENSDF flows

- main(argv) signature enables in-process CLI invocation and testing
- parser defaults for --e-step/--neutrino-mass/--intensity-cutoff/
  --csv-separator/--normalize-to-one now None = 'not provided'
- JSON flow applies file values by default and CLI flags as overrides
  (previously these five options were silently discarded)
- paceENSDF flow resolves None flags to literal defaults
- new tests/integration/test_cli_flows.py: JSON propagation,
  degenerate-mode non-zero exit with no file (JSON + paceENSDF)
- adapted two neutrino-mass default tests to None-parsing semantics
This commit is contained in:
2026-10-02 05:46:08 +03:00
parent 62bf7fdac2
commit 18a46ad02f
4 changed files with 235 additions and 29 deletions
+58 -25
View File
@@ -81,14 +81,18 @@ def _build_parser() -> argparse.ArgumentParser:
parser.add_argument(
"--e-step",
type=float,
default=0.001,
help="Energy step size in MeV (default: 0.001). All energies are in MeV throughout the toolkit.",
default=None,
help="Energy step size in MeV (default: 0.001 for nuclide input; "
"for JSON input the file's e_step_MeV is used when the flag is "
"omitted). All energies are in MeV throughout the toolkit.",
)
parser.add_argument(
"--neutrino-mass",
type=float,
default=0.0,
help="Neutrino mass in MeV (default: 0.0). Set to non-zero value to include neutrino mass effects in phase space calculation.",
default=None,
help="Neutrino mass in MeV (default: 0.0, or the JSON file's "
"neutrino_mass_MeV when omitted). Set to non-zero value to include "
"neutrino mass effects in phase space calculation.",
)
# Output
@@ -138,7 +142,7 @@ def _build_parser() -> argparse.ArgumentParser:
log_group.add_argument(
"--intensity-cutoff",
type=float,
default=0.0,
default=None,
metavar="FRAC",
help="Minimum branch intensity fraction to include in the spectrum "
"(default: 0.0 = include all branches). "
@@ -154,12 +158,13 @@ def _build_parser() -> argparse.ArgumentParser:
log_group.add_argument(
"--csv-separator",
type=str,
default=",",
default=None,
help="CSV column separator (default: ','). Use 'tab' for tab separation.",
)
log_group.add_argument(
"--normalize-to-one",
action="store_true",
default=None,
help="Normalize each branch to 1, ignoring intensity. In this case, total spectrum will not be produced (only energy and branch-wise spectra).",
)
@@ -230,6 +235,17 @@ def _run(args: argparse.Namespace) -> None:
else:
source_type = "paceENSDF"
# Resolve optional CLI flags: None means "not provided". For paceENSDF
# they fall back to literal defaults; for JSON they let the file's own
# values through unless explicitly overridden.
e_step_MeV = 0.001 if args.e_step is None else float(args.e_step)
neutrino_mass_MeV = 0.0 if args.neutrino_mass is None else float(args.neutrino_mass)
intensity_cutoff = (
0.0 if args.intensity_cutoff is None else float(args.intensity_cutoff)
)
csv_separator = "," if args.csv_separator is None else args.csv_separator
normalize_to_one = bool(args.normalize_to_one)
# Build config
config: SpectrumConfig
if source_type == "paceENSDF":
@@ -251,7 +267,7 @@ def _run(args: argparse.Namespace) -> None:
intensity=b.intensity / total_intensity,
)
for b in decay_info.branches
if b.intensity / total_intensity >= args.intensity_cutoff
if b.intensity / total_intensity >= intensity_cutoff
]
config = SpectrumConfig(
Z_parent=decay_info.Z_parent,
@@ -259,37 +275,48 @@ def _run(args: argparse.Namespace) -> None:
A_number=decay_info.A_number,
endpoint_MeV=decay_info.endpoint_MeV,
transition_type=decay_info.transition_type,
e_step_MeV=args.e_step,
neutrino_mass_MeV=args.neutrino_mass,
e_step_MeV=e_step_MeV,
neutrino_mass_MeV=neutrino_mass_MeV,
branches=branches,
intensity_cutoff=args.intensity_cutoff,
csv_separator=args.csv_separator,
normalize_to_one=args.normalize_to_one,
intensity_cutoff=intensity_cutoff,
csv_separator=csv_separator,
normalize_to_one=normalize_to_one,
)
else:
config = create_config_from_source(
"paceENSDF",
nuclide=source_value,
e_step_MeV=args.e_step,
intensity_cutoff=args.intensity_cutoff,
neutrino_mass_MeV=args.neutrino_mass,
csv_separator=args.csv_separator,
normalize_to_one=args.normalize_to_one,
e_step_MeV=e_step_MeV,
intensity_cutoff=intensity_cutoff,
neutrino_mass_MeV=neutrino_mass_MeV,
csv_separator=csv_separator,
normalize_to_one=normalize_to_one,
)
else:
config = create_config_from_source(
"paceENSDF",
nuclide=source_value,
e_step_MeV=args.e_step,
intensity_cutoff=args.intensity_cutoff,
neutrino_mass_MeV=args.neutrino_mass,
csv_separator=args.csv_separator,
normalize_to_one=args.normalize_to_one,
e_step_MeV=e_step_MeV,
intensity_cutoff=intensity_cutoff,
neutrino_mass_MeV=neutrino_mass_MeV,
csv_separator=csv_separator,
normalize_to_one=normalize_to_one,
)
else:
logger.info("Source: JSON file=%s", source_value)
data = load_json_input(source_value)
config = json_to_config(data)
# CLI flags override the JSON file values only when explicitly
# provided; unset flags keep the file's own values.
if args.e_step is not None:
config.e_step_MeV = e_step_MeV
if args.neutrino_mass is not None:
config.neutrino_mass_MeV = neutrino_mass_MeV
if args.intensity_cutoff is not None:
config.intensity_cutoff = intensity_cutoff
if args.csv_separator is not None:
config.csv_separator = csv_separator
config.normalize_to_one = normalize_to_one
# Dry run: display resolved config and exit
if args.dry_run:
@@ -516,10 +543,16 @@ def _decay_notation(Z_parent: int, Z_daughter: int, A: int) -> str:
return f"{_nuclide_symbol(Z_parent, A)} -> {_nuclide_symbol(Z_daughter, A)}"
def main() -> None:
"""Main entry point for the CLI."""
def main(argv: list[str] | None = None) -> None:
"""Main entry point for the CLI.
Parameters
----------
argv : list[str], optional
Explicit argument vector (for tests/embedding); None reads sys.argv.
"""
parser = _build_parser()
args = parser.parse_args()
args = parser.parse_args(argv)
try:
_run(args)