fix: resolve 4 fatal bugs — json_to_config, create_config_from_source, Am-241 constant, CLI positional args
This commit is contained in:
+50
-23
@@ -22,6 +22,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from beta_spectrum.logging_utils import LoggingConfig, setup_logging
|
||||
from beta_spectrum.spectrum import SpectrumConfig
|
||||
@@ -52,8 +53,18 @@ def _build_parser() -> argparse.ArgumentParser:
|
||||
version=f"%(prog)s {__import__('beta_spectrum').__version__}",
|
||||
)
|
||||
|
||||
# Source selection (mutually exclusive)
|
||||
source_group = parser.add_mutually_exclusive_group(required=True)
|
||||
# Source selection (positional or flag)
|
||||
parser.add_argument(
|
||||
"source",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="Nuclide symbol (e.g. 'Tc99') or JSON file path. "
|
||||
"Can also use --nuclide <symbol> or --input <file>. "
|
||||
"Auto-detected: .json extension → JSON input, otherwise → paceENSDF.",
|
||||
)
|
||||
|
||||
# Source selection flags (backward compatible, mutually exclusive)
|
||||
source_group = parser.add_mutually_exclusive_group(required=False)
|
||||
source_group.add_argument(
|
||||
"--nuclide",
|
||||
type=str,
|
||||
@@ -171,12 +182,36 @@ def _run(args: argparse.Namespace) -> None:
|
||||
logger.info("bs_pnpi starting...")
|
||||
logger.debug("Verbosity: %s | Log file: %s", log_level, args.log_file or "none")
|
||||
|
||||
# Resolve source: positional arg takes precedence, then flags
|
||||
source_value = args.source
|
||||
if source_value is None and args.nuclide:
|
||||
source_value = args.nuclide
|
||||
elif source_value is None and args.input:
|
||||
source_value = args.input
|
||||
elif source_value is not None:
|
||||
# Auto-detect from positional argument
|
||||
if source_value.endswith(".json") or Path(source_value).exists():
|
||||
source_value = source_value
|
||||
# else: keep as-is (nuclide symbol)
|
||||
|
||||
if source_value is None:
|
||||
logger.error(
|
||||
"Required: specify nuclide or JSON file "
|
||||
"(e.g., 'bs_pnpi Tc99' or 'bs_pnpi --nuclide Tc99')"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Determine source type
|
||||
if source_value.endswith(".json"):
|
||||
source_type = "json"
|
||||
else:
|
||||
source_type = "paceENSDF"
|
||||
|
||||
# Build config
|
||||
source_type: str
|
||||
config: SpectrumConfig
|
||||
if args.nuclide:
|
||||
logger.info("Source: paceENSDF nuclide=%s", args.nuclide)
|
||||
decay_info = get_decay_info_from_paceENSDF(args.nuclide, "beta_minus")
|
||||
if source_type == "paceENSDF":
|
||||
logger.info("Source: paceENSDF nuclide=%s", source_value)
|
||||
decay_info = get_decay_info_from_paceENSDF(source_value, "beta_minus")
|
||||
|
||||
# Normalize branch intensities to sum to 1.0
|
||||
if decay_info.branches:
|
||||
@@ -208,24 +243,21 @@ def _run(args: argparse.Namespace) -> None:
|
||||
else:
|
||||
config = create_config_from_source(
|
||||
"paceENSDF",
|
||||
nuclide=args.nuclide,
|
||||
nuclide=source_value,
|
||||
e_step_MeV=args.e_step,
|
||||
intensity_cutoff=args.intensity_cutoff,
|
||||
)
|
||||
else:
|
||||
config = create_config_from_source(
|
||||
"paceENSDF",
|
||||
nuclide=args.nuclide,
|
||||
nuclide=source_value,
|
||||
e_step_MeV=args.e_step,
|
||||
intensity_cutoff=args.intensity_cutoff,
|
||||
)
|
||||
source_type = "paceENSDF"
|
||||
elif args.input:
|
||||
logger.info("Source: JSON file=%s", args.input)
|
||||
data = load_json_input(args.input)
|
||||
config = json_to_config(data)
|
||||
source_type = "json"
|
||||
else:
|
||||
logger.error("No source specified (use --nuclide or --input)")
|
||||
sys.exit(1)
|
||||
logger.info("Source: JSON file=%s", source_value)
|
||||
data = load_json_input(source_value)
|
||||
config = json_to_config(data)
|
||||
|
||||
# Dry run: display resolved config and exit
|
||||
if args.dry_run:
|
||||
@@ -318,13 +350,8 @@ def _print_dry_run_output(config, source_type: str) -> None:
|
||||
else:
|
||||
print("Mode: single-branch")
|
||||
|
||||
if config.use_detector_response:
|
||||
print(
|
||||
f"Detector: {config.detector_model} (sigma={config.detector_sigma_a_keV} keV, "
|
||||
f"tail={config.detector_tail_fraction})"
|
||||
)
|
||||
else:
|
||||
print("Detector: disabled")
|
||||
# Detector response is a Phase 4 feature — not yet implemented
|
||||
print("Detector: not configured (Phase 4)")
|
||||
print("========================================")
|
||||
|
||||
|
||||
|
||||
@@ -272,7 +272,7 @@ def _get_decay_mode_symbol(
|
||||
}
|
||||
if decay_type not in mapping:
|
||||
raise ValueError(
|
||||
f"Unknown decay type '{decay_type}'. " f"Supported: {list(mapping.keys())}"
|
||||
f"Unknown decay type '{decay_type}'. Supported: {list(mapping.keys())}"
|
||||
)
|
||||
return mapping[decay_type]
|
||||
|
||||
@@ -638,9 +638,6 @@ def json_to_config(data: Dict[str, Any]) -> SpectrumConfig:
|
||||
endpoint_MeV=float(data["endpoint_MeV"]),
|
||||
transition_type=str(data.get("transition_type", "A")),
|
||||
e_step_MeV=float(data.get("e_step_MeV", 0.001)),
|
||||
detector_model=str(data.get("detector_model", "gaussian")),
|
||||
detector_channel_energy_range=tuple(
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -685,8 +682,12 @@ def create_config_from_source(
|
||||
decay_type = kwargs.get("decay_type", "beta_minus")
|
||||
decay_index = kwargs.get("decay_index", None)
|
||||
e_step = kwargs.get("e_step_MeV", 0.001)
|
||||
intensity_cutoff = kwargs.get("intensity_cutoff", 0.0)
|
||||
info = get_decay_info_from_paceENSDF(nuclide, decay_type, decay_index)
|
||||
return decay_info_to_config(
|
||||
info,
|
||||
e_step_MeV=e_step,
|
||||
intensity_cutoff=intensity_cutoff,
|
||||
)
|
||||
elif source == "json":
|
||||
if not json_path:
|
||||
|
||||
@@ -312,7 +312,7 @@ class EnergyCalibrator:
|
||||
filepath : str or Path
|
||||
Path to the Am-241 binary calibration file (CAM1.DAT or CAM2.DAT).
|
||||
known_energies : list of float, optional
|
||||
Known Am-241 line energies in keV. Defaults to [13.8, 17.8, 26.3].
|
||||
Known Am-241 line energies in keV. Defaults to [13.9, 17.8, 26.3, 59.54].
|
||||
n_channels : int
|
||||
Number of channels (default 4096).
|
||||
|
||||
@@ -352,8 +352,11 @@ class EnergyCalibrator:
|
||||
peak_counts = counts_search[peaks]
|
||||
|
||||
# Known Am-241 gamma lines (keV)
|
||||
# Am-241 decays to Np-237; Np K X-rays at 13.9 keV (Kα), 17.8 keV (Kβ),
|
||||
# 20.8 keV (L-series); Np gamma at 26.3 keV; Np/Am gamma at 59.54 keV.
|
||||
# Source: LNHb Table de Radionucléides, IAEA nuclear data.
|
||||
if known_energies is None:
|
||||
known_energies = [13.0, 17.8, 26.3, 59.54]
|
||||
known_energies = [13.9, 17.8, 26.3, 59.54]
|
||||
|
||||
# Match peaks to known energies using a simpler, more robust
|
||||
# approach: find the two strongest peaks that span the largest
|
||||
|
||||
@@ -151,7 +151,7 @@ def calibrate_from_am241(
|
||||
Path to the Am-241 binary file (CAM1.DAT or CAM2.DAT).
|
||||
known_peaks
|
||||
Mapping of energy_keV -> expected channel. If None, auto-detects
|
||||
Am-241 peaks at 13.8, 17.8, 26.3 keV.
|
||||
Am-241 peaks at 13.9, 17.8, 26.3 keV.
|
||||
n_channels
|
||||
Number of channels.
|
||||
|
||||
@@ -175,7 +175,7 @@ def calibrate_from_am241(
|
||||
_peak_heights = props["peak_heights"] # noqa: F841
|
||||
|
||||
# Known Am-241 gamma lines (keV)
|
||||
am241_lines = {13.8: None, 17.8: None, 26.3: None}
|
||||
am241_lines = {13.9: None, 17.8: None, 26.3: None}
|
||||
if known_peaks:
|
||||
am241_lines = known_peaks
|
||||
else:
|
||||
@@ -220,7 +220,7 @@ def read_am241_calibration(
|
||||
"""
|
||||
filepath = Path(filepath)
|
||||
|
||||
# Am-241 known peaks (keV): 13.8, 17.8, 26.3, 59.54
|
||||
# Am-241 known peaks (keV): 13.9, 17.8, 26.3, 59.54
|
||||
# The spectrum typically has a few prominent peaks
|
||||
with open(filepath, "rb") as f:
|
||||
_header = f.read(128)
|
||||
@@ -232,7 +232,7 @@ def read_am241_calibration(
|
||||
metadata = {
|
||||
"source": "Am241",
|
||||
"calibration_type": "energy",
|
||||
"known_peaks_keV": [13.8, 17.8, 26.3, 59.54],
|
||||
"known_peaks_keV": [13.9, 17.8, 26.3, 59.54],
|
||||
}
|
||||
|
||||
return ExpSpectrum(
|
||||
|
||||
@@ -29,7 +29,7 @@ from exp_data.raw_reader import (
|
||||
)
|
||||
|
||||
# Energy calibration from Am-241 source: E = offset + gain * channel
|
||||
# Determined automatically from known Am-241 gamma lines (13.8, 17.8, 26.3 keV)
|
||||
# Determined automatically from known Am-241 gamma lines (13.9, 17.8, 26.3 keV)
|
||||
CALIBRATION_FILE = "data/raw/CAM1.DAT"
|
||||
_calib_offset, _calib_gain = calibrate_from_am241(CALIBRATION_FILE)
|
||||
ENERGY_OFFSET = _calib_offset
|
||||
@@ -234,7 +234,9 @@ def main():
|
||||
# Save results
|
||||
results_file = output_dir / "rate_analysis.csv"
|
||||
with open(results_file, "w") as f:
|
||||
f.write("run,total_counts,low_energy_counts,high_energy_counts,low_fraction,endpoint_keV\n")
|
||||
f.write(
|
||||
"run,total_counts,low_energy_counts,high_energy_counts,low_fraction,endpoint_keV\n"
|
||||
)
|
||||
for i in range(len(run_ids)):
|
||||
f.write(
|
||||
f"{run_ids[i]},{total_rates[i]:.0f},{low_rates[i]:.0f},"
|
||||
@@ -265,7 +267,9 @@ def main():
|
||||
# Endpoint
|
||||
axes[1, 0].plot(run_ids, endpoints, "r-", lw=1.5)
|
||||
axes[1, 0].axhline(
|
||||
TC99_ENDPOINT, color="gray", ls="--",
|
||||
TC99_ENDPOINT,
|
||||
color="gray",
|
||||
ls="--",
|
||||
label=f"Tc-99 Q ≈ {TC99_ENDPOINT} keV",
|
||||
)
|
||||
axes[1, 0].set_xlabel("Run number")
|
||||
|
||||
@@ -202,7 +202,8 @@ class TestDecayInfoToConfig:
|
||||
assert abs(config.endpoint_MeV - 0.294) < 1e-6
|
||||
assert config.transition_type == "A"
|
||||
|
||||
def test_with_detector_response(self):
|
||||
def test_with_e_step_and_cutoff(self):
|
||||
"""Verify e_step_MeV and intensity_cutoff kwargs are passed through."""
|
||||
info = DecayInfo(
|
||||
parent_symbol="Co60",
|
||||
Z_parent=27,
|
||||
@@ -215,13 +216,30 @@ class TestDecayInfoToConfig:
|
||||
parent_J=0.0,
|
||||
parent_pi=1,
|
||||
half_life="y",
|
||||
branches=[],
|
||||
branches=[
|
||||
BranchInfo(
|
||||
level_index=0,
|
||||
level_energy_keV=0.0,
|
||||
intensity=0.5,
|
||||
log_ft=5.0,
|
||||
transition_type="A",
|
||||
),
|
||||
BranchInfo(
|
||||
level_index=1,
|
||||
level_energy_keV=10.0,
|
||||
intensity=0.3,
|
||||
log_ft=6.0,
|
||||
transition_type="A",
|
||||
),
|
||||
],
|
||||
)
|
||||
config = decay_info_to_config(
|
||||
info, use_detector_response=True, e_step_MeV=0.005
|
||||
)
|
||||
assert config.use_detector_response is True
|
||||
config = decay_info_to_config(info, e_step_MeV=0.005, intensity_cutoff=0.4)
|
||||
assert config.e_step_MeV == 0.005
|
||||
assert config.intensity_cutoff == 0.4
|
||||
# Only branch with intensity >= 0.4 should be included
|
||||
assert config.branches is not None
|
||||
assert len(config.branches) == 1
|
||||
assert config.branches[0].intensity == 0.5
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user