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:
@@ -52,4 +52,4 @@ __all__ = [
|
||||
"DEFAULT_JSON_SCHEMA",
|
||||
]
|
||||
|
||||
__version__ = "0.4.0"
|
||||
__version__ = "0.4.1"
|
||||
|
||||
+19
-6
@@ -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'}")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]:
|
||||
|
||||
Reference in New Issue
Block a user