Files
beta_shape_pnpi/beta_spectrum/cli.py
T
unzhakov 1269244b93 feat: add paceENSDF integration and CLI interface (Track B1)
- Add nuclear_data.py module for paceENSDF integration and JSON input
- Add DecayInfo and BranchInfo dataclasses for parsed nuclear data
- Implement get_decay_info_from_paceENSDF() to retrieve decay parameters
- Implement load_json_input() and json_to_config() for custom JSON input
- Add create_config_from_source() unified interface for both sources
- Add bs_pnpi CLI with --nuclide, --input, --output, --plot, and detector options
- Add 23 comprehensive tests for nuclear_data module
- Add sample JSON input file
- Update __init__.py and pyproject.toml with new exports and entry point
- Add paceENSDF as optional dependency
2026-05-02 22:09:25 +03:00

216 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Command-line interface for beta-spectrum calculations.
Usage examples:
# From paceENSDF nuclear data
bs_pnpi --nuclide Tc99 --output spectrum.csv
# From custom JSON input
bs_pnpi --input custom_input.json --output spectrum.csv
# Full analysis with plotting
bs_pnpi --nuclide Co60 --mode BM --plot analysis.png --output spectrum.csv
# With detector response
bs_pnpi --nuclide Tc99 --detector sigma=1.0 --output convolved.csv
"""
from __future__ import annotations
import argparse
import sys
from typing import Any, Dict
def _build_parser() -> argparse.ArgumentParser:
"""Build the argument parser."""
parser = argparse.ArgumentParser(
prog="bs_pnpi",
description="Beta spectrum calculation toolkit — "
"calculate high-precision beta decay energy spectra.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Examples:\n"
" bs_pnpi --nuclide Tc99 --output spectrum.csv\n"
" bs_pnpi --input custom_input.json --output spectrum.csv\n"
" bs_pnpi --nuclide Tc99 --plot spectrum.png --output spectrum.csv\n"
" bs_pnpi --nuclide Tc99 --detector sigma=1.0 --output convolved.csv\n"
),
)
# Source selection (mutually exclusive)
source_group = parser.add_mutually_exclusive_group(required=True)
source_group.add_argument(
"--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(
"--input",
type=str,
metavar="FILE",
help="Path to a JSON input file with custom parameters.",
)
# Common parameters
parser.add_argument(
"--mode",
type=str,
default="beta_minus",
choices=["beta_minus", "beta_plus", "ec"],
help="Decay mode for paceENSDF source (default: beta_minus).",
)
parser.add_argument(
"--decay-index",
type=int,
default=None,
help="Decay index for paceENSDF (0 = ground state, default: auto-select).",
)
parser.add_argument(
"--transition-type",
type=str,
default=None,
choices=["A", "F1", "F1U", "F2", "F2U", "F3", "F3U", "F4"],
help="Override transition type (forbiddenness classification).",
)
parser.add_argument(
"--e-step",
type=float,
default=0.001,
help="Energy step size in MeV (default: 0.001).",
)
# Output
parser.add_argument(
"--output",
type=str,
metavar="FILE",
help="Output CSV file path.",
)
# Plotting
parser.add_argument(
"--plot",
type=str,
metavar="FILE",
help="Save analysis plot to the specified path.",
)
# Detector response
detector_group = parser.add_argument_group("detector response")
detector_group.add_argument(
"--detector",
type=str,
default=None,
metavar="MODEL",
help="Enable detector response convolution. "
"Specify model: 'gaussian', 'gaussian_tail', 'tikhonov', 'tabulated'.",
)
detector_group.add_argument(
"--sigma",
type=float,
default=1.0,
metavar="KEV",
help="Resolution parameter sigma in keV (default: 1.0).",
)
detector_group.add_argument(
"--tau",
type=float,
default=5.0,
metavar="KEV",
help="Tail decay constant tau in keV (default: 5.0).",
)
detector_group.add_argument(
"--tail-fraction",
type=float,
default=0.0,
metavar="FRAC",
help="Low-energy tail fraction (0.0–1.0, default: 0.0).",
)
return parser
def _parse_detector_args(args: argparse.Namespace) -> Dict[str, Any]:
"""Parse detector-related arguments into a config dict."""
if not args.detector:
return {}
return {
"use_detector_response": True,
"detector_model": args.detector,
"detector_sigma_a_keV": args.sigma,
"detector_tau_keV": args.tau,
"detector_tail_fraction": args.tail_fraction,
}
def _run(args: argparse.Namespace) -> None:
"""Execute the CLI command."""
from beta_spectrum.nuclear_data import (
create_config_from_source,
json_to_config,
load_json_input,
)
from beta_spectrum.spectrum import BetaSpectrum, BetaSpectrumAnalyzer
# Build config
detector_kwargs = _parse_detector_args(args)
if args.nuclide:
config = create_config_from_source(
"paceENSDF",
nuclide=args.nuclide,
decay_type=args.mode,
decay_index=args.decay_index,
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),
)
elif args.input:
data = load_json_input(args.input)
if args.transition_type:
data["transition_type"] = args.transition_type
config = json_to_config(data)
# Override transition type if explicitly provided
if args.transition_type:
config.transition_type = args.transition_type
# Create and calculate spectrum
spectrum = BetaSpectrum.from_config(config)
_W, _kinetic_MeV = spectrum.get_energy_grid(config)
_ = spectrum(_W)
# Export to CSV
if args.output:
analyzer = BetaSpectrumAnalyzer(spectrum, config)
analyzer.export_to_csv(args.output)
print(f"Spectrum exported to {args.output}")
# Generate plot
if args.plot:
analyzer = BetaSpectrumAnalyzer(spectrum, config)
analyzer.plot_analysis(save_path=args.plot)
print(f"Analysis plot saved to {args.plot}")
def main() -> None:
"""Main entry point for the CLI."""
parser = _build_parser()
args = parser.parse_args()
try:
_run(args)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()