feat: multi-branch CLI with intensity cutoff and per-branch info

- Add --intensity-cutoff CLI option to filter negligible branches
- Fetch DecayInfo from paceENSDF and construct SpectrumConfig with
  branches including per-branch transition types
- Normalize branch intensities to sum to 1.0, filter by cutoff
- Display branch info in dry-run output and logging
- Add intensity_cutoff to SpectrumConfig
This commit is contained in:
2026-05-08 08:33:39 +03:00
parent 3216c50943
commit 6293710089
+109 -12
View File
@@ -156,6 +156,15 @@ def _build_parser() -> argparse.ArgumentParser:
action="store_true", action="store_true",
help="Validate input and display resolved configuration without computing spectrum.", help="Validate input and display resolved configuration without computing spectrum.",
) )
log_group.add_argument(
"--intensity-cutoff",
type=float,
default=0.0,
metavar="FRAC",
help="Minimum branch intensity fraction to include in the spectrum "
"(default: 0.0 = include all branches). "
"Branches below this threshold are excluded from the calculation and plots.",
)
return parser return parser
@@ -181,10 +190,16 @@ def _run(args: argparse.Namespace) -> None:
from beta_spectrum.logging_utils import get_logger from beta_spectrum.logging_utils import get_logger
from beta_spectrum.nuclear_data import ( from beta_spectrum.nuclear_data import (
create_config_from_source, create_config_from_source,
get_decay_info_from_paceENSDF,
json_to_config, json_to_config,
load_json_input, load_json_input,
) )
from beta_spectrum.spectrum import BetaSpectrum, BetaSpectrumAnalyzer from beta_spectrum.spectrum import (
BetaSpectrum,
BetaSpectrumAnalyzer,
BranchConfig,
SpectrumConfig,
)
# Determine logging level # Determine logging level
if args.quiet: if args.quiet:
@@ -214,16 +229,78 @@ def _run(args: argparse.Namespace) -> None:
config: SpectrumConfig config: SpectrumConfig
if args.nuclide: if args.nuclide:
logger.info("Source: paceENSDF nuclide=%s", args.nuclide) logger.info("Source: paceENSDF nuclide=%s", args.nuclide)
config = create_config_from_source( decay_info = get_decay_info_from_paceENSDF(args.nuclide, "beta_minus")
"paceENSDF",
nuclide=args.nuclide, # Normalize branch intensities to sum to 1.0
e_step_MeV=args.e_step, if decay_info.branches:
use_detector_response=detector_kwargs.get("use_detector_response", False), total_intensity = sum(b.intensity for b in decay_info.branches)
detector_model=detector_kwargs.get("detector_model", "gaussian"), if total_intensity > 0:
detector_sigma_a_keV=detector_kwargs.get("detector_sigma_a_keV", 1.0), branches = [
detector_tau_keV=detector_kwargs.get("detector_tau_keV", 5.0), BranchConfig(
detector_tail_fraction=detector_kwargs.get("detector_tail_fraction", 0.0), endpoint_MeV=(
) (decay_info.endpoint_MeV - b.level_energy_keV / 1000.0)
if b.level_energy_keV > 0
else decay_info.endpoint_MeV
),
transition_type=b.transition_type,
intensity=b.intensity / total_intensity,
)
for b in decay_info.branches
if b.intensity / total_intensity >= args.intensity_cutoff
]
config = SpectrumConfig(
Z_parent=decay_info.Z_parent,
Z_daughter=decay_info.Z_daughter,
A_number=decay_info.A_number,
endpoint_MeV=decay_info.endpoint_MeV,
transition_type=decay_info.transition_type,
e_step_MeV=args.e_step,
branches=branches,
intensity_cutoff=args.intensity_cutoff,
use_detector_response=detector_kwargs.get(
"use_detector_response", False
),
detector_model=detector_kwargs.get("detector_model", "gaussian"),
detector_sigma_a_keV=detector_kwargs.get(
"detector_sigma_a_keV", 1.0
),
detector_tau_keV=detector_kwargs.get("detector_tau_keV", 5.0),
detector_tail_fraction=detector_kwargs.get(
"detector_tail_fraction", 0.0
),
)
else:
config = create_config_from_source(
"paceENSDF",
nuclide=args.nuclide,
e_step_MeV=args.e_step,
use_detector_response=detector_kwargs.get(
"use_detector_response", False
),
detector_model=detector_kwargs.get("detector_model", "gaussian"),
detector_sigma_a_keV=detector_kwargs.get(
"detector_sigma_a_keV", 1.0
),
detector_tau_keV=detector_kwargs.get("detector_tau_keV", 5.0),
detector_tail_fraction=detector_kwargs.get(
"detector_tail_fraction", 0.0
),
)
else:
config = create_config_from_source(
"paceENSDF",
nuclide=args.nuclide,
e_step_MeV=args.e_step,
use_detector_response=detector_kwargs.get(
"use_detector_response", False
),
detector_model=detector_kwargs.get("detector_model", "gaussian"),
detector_sigma_a_keV=detector_kwargs.get("detector_sigma_a_keV", 1.0),
detector_tau_keV=detector_kwargs.get("detector_tau_keV", 5.0),
detector_tail_fraction=detector_kwargs.get(
"detector_tail_fraction", 0.0
),
)
source_type = "paceENSDF" source_type = "paceENSDF"
elif args.input: elif args.input:
logger.info("Source: JSON file=%s", args.input) logger.info("Source: JSON file=%s", args.input)
@@ -241,10 +318,14 @@ def _run(args: argparse.Namespace) -> None:
# Create and calculate spectrum # Create and calculate spectrum
start_time = time.time() start_time = time.time()
mode_str = ""
if config.branches and len(config.branches) > 1:
mode_str = f" (multi-branch: {len(config.branches)} branches)"
logger.info( logger.info(
"Creating BetaSpectrum for %s, endpoint=%.3f MeV", "Creating BetaSpectrum for %s, endpoint=%.3f MeV%s",
_decay_notation(config.Z_parent, config.Z_daughter, config.A_number), _decay_notation(config.Z_parent, config.Z_daughter, config.A_number),
config.endpoint_MeV, config.endpoint_MeV,
mode_str,
) )
spectrum = BetaSpectrum.from_config(config, logger=logger) spectrum = BetaSpectrum.from_config(config, logger=logger)
@@ -305,6 +386,22 @@ def _print_dry_run_output(config, source_type: str) -> None:
print(f"Transition: {config.transition_type}") print(f"Transition: {config.transition_type}")
print(f"e_step: {config.e_step_MeV:.4f} MeV") print(f"e_step: {config.e_step_MeV:.4f} MeV")
print(f"Corrections: {', '.join(enabled) if enabled else 'none'}") print(f"Corrections: {', '.join(enabled) if enabled else 'none'}")
if config.branches and len(config.branches) > 1:
print(f"\nMulti-branch mode: {len(config.branches)} branches")
if config.intensity_cutoff > 0:
print(
f"Intensity cutoff: {config.intensity_cutoff:.4f} ({config.intensity_cutoff*100:.1f}%)"
)
for i, branch in enumerate(config.branches):
print(
f" Branch {i+1}: E₀={branch.endpoint_MeV*1000:.1f} keV, "
f"transition={branch.transition_type}, "
f"intensity={branch.intensity:.4f}"
)
else:
print("Mode: single-branch")
if config.use_detector_response: if config.use_detector_response:
print( print(
f"Detector: {config.detector_model} (sigma={config.detector_sigma_a_keV} keV, " f"Detector: {config.detector_model} (sigma={config.detector_sigma_a_keV} keV, "