feat(cli): separate-branches rename, short aliases, clean dry-run

- --normalize-to-one renamed to --separate-branches (no alias; the
  name now describes the output choice: total spectrum with
  intensity-normalized branches vs separate branches, each
  normalized to unit area with intensities left to the data user).
  SpectrumConfig.normalize_to_one -> separate_branches, CSV header
  word to_one -> separate.
- every long option gains a single-letter alias: -N -i -s -m -o -p
  -L -d -c -V -S -b (only -v/-q existed before)
- dry-run drops the top-level Transition: line — the per-branch
  list below already reports each branch's transition type
- version bumped to 0.4.1 (pyproject, __version__, CSV semantics
  docstring, TODO roadmap entry)
This commit is contained in:
2026-10-03 01:08:51 +03:00
parent 7f47a876ae
commit 2788ea0683
12 changed files with 210 additions and 53 deletions
+1 -1
View File
@@ -52,4 +52,4 @@ __all__ = [
"DEFAULT_JSON_SCHEMA",
]
__version__ = "0.4.0"
__version__ = "0.4.1"
+19 -6
View File
@@ -65,12 +65,14 @@ def _build_parser() -> argparse.ArgumentParser:
# Source selection flags (backward compatible, mutually exclusive)
source_group = parser.add_mutually_exclusive_group(required=False)
source_group.add_argument(
"-N",
"--nuclide",
type=str,
help="Nuclide symbol for paceENSDF lookup (e.g. 'Tc99', 'Co60'). "
"Retrieves decay parameters from the ENSDF database via paceENSDF.",
)
source_group.add_argument(
"-i",
"--input",
type=str,
metavar="FILE",
@@ -79,6 +81,7 @@ def _build_parser() -> argparse.ArgumentParser:
# Common parameters
parser.add_argument(
"-s",
"--e-step",
type=float,
default=None,
@@ -87,6 +90,7 @@ def _build_parser() -> argparse.ArgumentParser:
"omitted). All energies are in MeV throughout the toolkit.",
)
parser.add_argument(
"-m",
"--neutrino-mass",
type=float,
default=None,
@@ -97,6 +101,7 @@ def _build_parser() -> argparse.ArgumentParser:
# Output
parser.add_argument(
"-o",
"--output",
type=str,
metavar="FILE",
@@ -105,6 +110,7 @@ def _build_parser() -> argparse.ArgumentParser:
# Plotting
parser.add_argument(
"-p",
"--plot",
type=str,
metavar="FILE",
@@ -128,6 +134,7 @@ def _build_parser() -> argparse.ArgumentParser:
help="Suppress all terminal output (only errors shown).",
)
log_group.add_argument(
"-L",
"--log-file",
type=str,
metavar="PATH",
@@ -135,11 +142,13 @@ def _build_parser() -> argparse.ArgumentParser:
"Log file is always opened in append mode at DEBUG level.",
)
log_group.add_argument(
"-d",
"--dry-run",
action="store_true",
help="Validate input and display resolved configuration without computing spectrum.",
)
log_group.add_argument(
"-c",
"--intensity-cutoff",
type=float,
default=None,
@@ -149,6 +158,7 @@ def _build_parser() -> argparse.ArgumentParser:
"Branches below this threshold are excluded from the calculation and plots.",
)
log_group.add_argument(
"-V",
"--csv-verbosity",
type=str,
default="full",
@@ -156,16 +166,20 @@ def _build_parser() -> argparse.ArgumentParser:
help="CSV output verbosity: 'full' (all components) or 'concise' (total spectrum and normalized branch components only) (default: full)",
)
log_group.add_argument(
"-S",
"--csv-separator",
type=str,
default=None,
help="CSV column separator (default: ','). Use 'tab' for tab separation.",
)
log_group.add_argument(
"--normalize-to-one",
"-b",
"--separate-branches",
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).",
help="Emit separate branch spectra, each normalized to integrate to "
"1 (intensities ignored; no total_spectrum column). Default: total "
"spectrum with intensity-normalized branch columns.",
)
return parser
@@ -239,7 +253,7 @@ def _run(args: argparse.Namespace) -> None:
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)
separate_branches = bool(args.separate_branches)
# Build config — a single delegated path for every data source.
config: SpectrumConfig
@@ -252,7 +266,7 @@ def _run(args: argparse.Namespace) -> None:
intensity_cutoff=intensity_cutoff,
neutrino_mass_MeV=neutrino_mass_MeV,
csv_separator=csv_separator,
normalize_to_one=normalize_to_one,
separate_branches=separate_branches,
)
else:
logger.info("Source: JSON file=%s", source_value)
@@ -268,7 +282,7 @@ def _run(args: argparse.Namespace) -> 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
config.separate_branches = separate_branches
# Dry run: display resolved config and exit
if args.dry_run:
@@ -352,7 +366,6 @@ def _print_dry_run_output(config, source_type: str) -> None:
f"Nuclide: {_decay_notation(config.Z_parent, config.Z_daughter, config.A_number)}"
)
print(f"Endpoint: {config.endpoint_MeV * 1000:.1f} keV")
print(f"Transition: {config.transition_type}")
print(f"e_step: {config.e_step_MeV:.4f} MeV")
print(f"neutrino_mass_MeV: {config.neutrino_mass_MeV:.6f}")
print(f"Corrections: {', '.join(enabled) if enabled else 'none'}")
+7 -6
View File
@@ -477,7 +477,7 @@ def decay_info_to_config(
intensity_cutoff: float = 0.0,
neutrino_mass_MeV: float = 0.0,
csv_separator: str = ",",
normalize_to_one: bool = False,
separate_branches: bool = False,
) -> SpectrumConfig:
"""
Convert DecayInfo to a SpectrumConfig.
@@ -494,8 +494,9 @@ def decay_info_to_config(
Neutrino mass in MeV.
csv_separator : str
CSV column separator.
normalize_to_one : bool
If True, normalize each branch to 1, ignoring intensity.
separate_branches : bool
If True, emit separate branch spectra, each normalized to
integrate to 1 (intensity ignored; no total spectrum column).
Returns
-------
@@ -535,7 +536,7 @@ def decay_info_to_config(
branches=branches,
intensity_cutoff=intensity_cutoff,
csv_separator=csv_separator,
normalize_to_one=normalize_to_one,
separate_branches=separate_branches,
)
@@ -708,7 +709,7 @@ def create_config_from_source(
intensity_cutoff = kwargs.get("intensity_cutoff", 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)
separate_branches = kwargs.get("separate_branches", False)
info = get_decay_info_from_paceENSDF(nuclide, decay_type, decay_index)
return decay_info_to_config(
info,
@@ -716,7 +717,7 @@ def create_config_from_source(
intensity_cutoff=intensity_cutoff,
neutrino_mass_MeV=neutrino_mass,
csv_separator=csv_separator,
normalize_to_one=normalize_to_one,
separate_branches=separate_branches,
)
elif source == "json":
if not json_path:
+7 -7
View File
@@ -1,7 +1,7 @@
"""
CSV export module — CSV export functionality for BetaSpectrumAnalyzer.
Column semantics (v0.4.0 onward):
Column semantics (v0.4.1 onward):
- The basic spectrum is ALWAYS core = phase_space x fermi; the Fermi function
is a core spectrum component, never an optional correction. Higher-order
corrections (finite size, charge distribution, screening, exchange,
@@ -12,7 +12,7 @@ Column semantics (v0.4.0 onward):
(the total column itself is normalized to integrate to 1). In the legacy
export branch columns contained only the per-branch phase-space x
radiative product WITHOUT the Fermi function — physically wrong.
- ``normalize_to_one`` mode: each branch column is normalized to
- ``separate_branches`` mode: each branch column is normalized to
integrate to 1 individually, intensity is ignored, no total column.
"""
@@ -62,7 +62,7 @@ class CSVEmitter:
"""
components = analyzer.components
config = analyzer.config
to_one = config.normalize_to_one
separate = config.separate_branches
n_branches = analyzer._n_branches
# Physical branch spectra: core(phase_space x fermi) x corrections.
@@ -127,9 +127,9 @@ class CSVEmitter:
header_lines.append(f"# e_step: {config.e_step_MeV:.4f} MeV")
header_lines.append(f"# n_points: {len(analyzer.energies_MeV)}")
if to_one:
if separate:
header_lines.append(
"# branch normalization: to_one "
"# branch normalization: separate "
"(each branch integrates to 1, intensity ignored)"
)
else:
@@ -189,7 +189,7 @@ class CSVEmitter:
"is degenerate (check neutrino_mass vs branch endpoints "
"and intensity_cutoff)"
)
if not to_one:
if not separate:
data["total_spectrum"] = total_raw / integral
for i in range(n_branches):
@@ -199,7 +199,7 @@ class CSVEmitter:
else:
col_name = f"branch_{key_i}_spectrum"
if to_one:
if separate:
area = float(np.trapezoid(physical[i], analyzer.energies_MeV))
if area > 0.0:
data[col_name] = physical[i] / area
+3 -5
View File
@@ -57,9 +57,6 @@ class SpectrumConfig:
"""
Configuration for beta spectrum calculation.
Supports declarative detector response specification:
to enable automatic detector smearing via convolve_with_detector().
Supports multi-branch decay via the `branches` field. When branches are
provided, the total spectrum is the intensity-weighted sum of individual
branch spectra. Each branch has its own endpoint energy and transition type.
@@ -90,9 +87,10 @@ class SpectrumConfig:
# 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
separate_branches: bool = (
False # If True: emit separate branch spectra, each normalized to
)
# integrate to 1 (intensity ignored); no total spectrum column
def _effective_single_fields(config: SpectrumConfig) -> Tuple[float, str]: