refactor: remove stale detector schema keys and single-mode duplicates
- DEFAULT_JSON_SCHEMA: drop the 8 detector_* keys left over from the removed detector_response module; CLI dry-run drops the dead 'Detector: not configured (Phase 4)' line - CSV export: single-mode no longer writes branch_1_PhaseSpace / branch_1_Radiative duplicates next to the bare PhaseSpace/Radiative columns; per-branch core factors only in multi-branch mode - Visualizer: debug panels fall back to the unprefixed core-factor columns for single-mode CSVs instead of skipping them - Spectrum ylabel now states the actual units: normalized spectrum [MeV^-1], not 'Normalized Counts / keV' - tests: defaults assert detector keys gone; column layout pinned for single vs multi; panel and ylabel assertions
This commit is contained in:
@@ -372,8 +372,6 @@ def _print_dry_run_output(config, source_type: str) -> None:
|
|||||||
else:
|
else:
|
||||||
print("Mode: single-branch")
|
print("Mode: single-branch")
|
||||||
|
|
||||||
# Detector response is a Phase 4 feature — not yet implemented
|
|
||||||
print("Detector: not configured (Phase 4)")
|
|
||||||
print("========================================")
|
print("========================================")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -554,14 +554,6 @@ DEFAULT_JSON_SCHEMA = {
|
|||||||
"transition_type": "F2",
|
"transition_type": "F2",
|
||||||
"e_step_MeV": 0.001,
|
"e_step_MeV": 0.001,
|
||||||
"neutrino_mass_MeV": 0.0,
|
"neutrino_mass_MeV": 0.0,
|
||||||
"detector_model": "gaussian",
|
|
||||||
"detector_sigma_a_keV": 1.0,
|
|
||||||
"detector_sigma_b": 0.0,
|
|
||||||
"detector_tail_fraction": 0.0,
|
|
||||||
"detector_tau_keV": 5.0,
|
|
||||||
"detector_fano_factor": 0.12,
|
|
||||||
"detector_n_channels": 4096,
|
|
||||||
"detector_channel_energy_range": [0.0, 0.35],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -211,6 +211,11 @@ class CSVEmitter:
|
|||||||
|
|
||||||
if csv_verbosity == "full":
|
if csv_verbosity == "full":
|
||||||
# Per-branch component columns (factorized ingredients).
|
# Per-branch component columns (factorized ingredients).
|
||||||
|
# Only multi-branch mode carries per-branch factors: in single
|
||||||
|
# mode the bare PhaseSpace/Radiative columns below already
|
||||||
|
# describe the single spectrum, and prefixed duplicates would
|
||||||
|
# silently disagree with them once renamed.
|
||||||
|
if analyzer._is_multi:
|
||||||
for i in range(n_branches):
|
for i in range(n_branches):
|
||||||
for comp_name in ["PhaseSpace", "Radiative"]:
|
for comp_name in ["PhaseSpace", "Radiative"]:
|
||||||
key = analyzer._get_component_key(i, comp_name)
|
key = analyzer._get_component_key(i, comp_name)
|
||||||
@@ -218,7 +223,8 @@ class CSVEmitter:
|
|||||||
data[f"branch_{i + 1}_{comp_name}"] = components[key]
|
data[f"branch_{i + 1}_{comp_name}"] = components[key]
|
||||||
|
|
||||||
# Universal components (no prefix): Fermi plus universal
|
# Universal components (no prefix): Fermi plus universal
|
||||||
# higher-order corrections.
|
# higher-order corrections. In single mode the core factors
|
||||||
|
# (PhaseSpace, Radiative) also appear here unprefixed.
|
||||||
for name, values in components.items():
|
for name, values in components.items():
|
||||||
if not name.startswith("branch_"):
|
if not name.startswith("branch_"):
|
||||||
data[name] = values
|
data[name] = values
|
||||||
|
|||||||
@@ -235,7 +235,6 @@ def _plot_spectra_view_from_df(
|
|||||||
) -> Figure:
|
) -> Figure:
|
||||||
"""Plot total spectrum and per-branch spectra from CSV columns."""
|
"""Plot total spectrum and per-branch spectra from CSV columns."""
|
||||||
energies = df["energy_MeV"].to_numpy()
|
energies = df["energy_MeV"].to_numpy()
|
||||||
e_step_keV = (meta["e_step_MeV"] or 0.0) * 1000.0
|
|
||||||
|
|
||||||
fig, ax = plt.subplots(figsize=(12, 7))
|
fig, ax = plt.subplots(figsize=(12, 7))
|
||||||
|
|
||||||
@@ -266,7 +265,7 @@ def _plot_spectra_view_from_df(
|
|||||||
)
|
)
|
||||||
|
|
||||||
ax.set_xlabel(r"Electron kinetic energy $E$ [MeV]", fontsize=12)
|
ax.set_xlabel(r"Electron kinetic energy $E$ [MeV]", fontsize=12)
|
||||||
ax.set_ylabel(f"Normalized Counts / {e_step_keV:.1f} keV", fontsize=12)
|
ax.set_ylabel(r"Normalized spectrum [MeV$^{-1}$]", fontsize=12)
|
||||||
fig.suptitle(_spectrum_title(meta), fontsize=14, fontweight="bold")
|
fig.suptitle(_spectrum_title(meta), fontsize=14, fontweight="bold")
|
||||||
ax.set_yscale("log")
|
ax.set_yscale("log")
|
||||||
ax.grid(True, alpha=0.3)
|
ax.grid(True, alpha=0.3)
|
||||||
@@ -288,7 +287,6 @@ def _plot_debug_view_from_df(
|
|||||||
) -> Figure:
|
) -> Figure:
|
||||||
"""Debug view (4 panels) built entirely from CSV columns."""
|
"""Debug view (4 panels) built entirely from CSV columns."""
|
||||||
energies = df["energy_MeV"].to_numpy()
|
energies = df["energy_MeV"].to_numpy()
|
||||||
e_step_keV = (meta["e_step_MeV"] or 0.0) * 1000.0
|
|
||||||
active = _active_branches(meta)
|
active = _active_branches(meta)
|
||||||
|
|
||||||
fig, axes = plt.subplots(4, 1, figsize=(12, 16), sharex=True)
|
fig, axes = plt.subplots(4, 1, figsize=(12, 16), sharex=True)
|
||||||
@@ -318,7 +316,7 @@ def _plot_debug_view_from_df(
|
|||||||
label=f"Br. {b['index']} ({b['intensity'] * 100:.1f}%)",
|
label=f"Br. {b['index']} ({b['intensity'] * 100:.1f}%)",
|
||||||
)
|
)
|
||||||
ax.set_yscale("log")
|
ax.set_yscale("log")
|
||||||
ax.set_ylabel(f"Normalized Counts / {e_step_keV:.1f} keV", fontsize=10)
|
ax.set_ylabel(r"Normalized spectrum [MeV$^{-1}$]", fontsize=10)
|
||||||
ax.set_title("Spectrum", fontsize=11)
|
ax.set_title("Spectrum", fontsize=11)
|
||||||
ax.grid(True, alpha=0.3)
|
ax.grid(True, alpha=0.3)
|
||||||
ax.legend(fontsize=8, loc="best", ncol=2)
|
ax.legend(fontsize=8, loc="best", ncol=2)
|
||||||
@@ -331,6 +329,9 @@ def _plot_debug_view_from_df(
|
|||||||
ax.plot(energies, fermi_scaled, "r-", lw=1.5, label="Fermi", alpha=0.8)
|
ax.plot(energies, fermi_scaled, "r-", lw=1.5, label="Fermi", alpha=0.8)
|
||||||
for color_i, b in enumerate(active):
|
for color_i, b in enumerate(active):
|
||||||
col = f"branch_{b['index']}_PhaseSpace"
|
col = f"branch_{b['index']}_PhaseSpace"
|
||||||
|
if col not in df.columns:
|
||||||
|
# Single-branch CSVs carry the core factors unprefixed.
|
||||||
|
col = "PhaseSpace"
|
||||||
if col not in df.columns:
|
if col not in df.columns:
|
||||||
continue
|
continue
|
||||||
ps_vals = df[col].to_numpy()
|
ps_vals = df[col].to_numpy()
|
||||||
@@ -374,6 +375,9 @@ def _plot_debug_view_from_df(
|
|||||||
ax = axes[3]
|
ax = axes[3]
|
||||||
for color_i, b in enumerate(active):
|
for color_i, b in enumerate(active):
|
||||||
col = f"branch_{b['index']}_Radiative"
|
col = f"branch_{b['index']}_Radiative"
|
||||||
|
if col not in df.columns:
|
||||||
|
# Single-branch CSVs carry the core factors unprefixed.
|
||||||
|
col = "Radiative"
|
||||||
if col not in df.columns:
|
if col not in df.columns:
|
||||||
continue
|
continue
|
||||||
ax.plot(
|
ax.plot(
|
||||||
|
|||||||
@@ -605,9 +605,7 @@ class TestNormalizationModeMatrix:
|
|||||||
)
|
)
|
||||||
assert np.isclose(area, 1.0, rtol=5e-4)
|
assert np.isclose(area, 1.0, rtol=5e-4)
|
||||||
|
|
||||||
def test_intensity_concise_branch_columns_sum_to_total(
|
def test_intensity_concise_branch_columns_sum_to_total(self, tmp_path: Any) -> None:
|
||||||
self, tmp_path: Any
|
|
||||||
) -> None:
|
|
||||||
config = make_multi_config()
|
config = make_multi_config()
|
||||||
spectrum = BetaSpectrum.from_config(config)
|
spectrum = BetaSpectrum.from_config(config)
|
||||||
analyzer = BetaSpectrumAnalyzer(spectrum, config)
|
analyzer = BetaSpectrumAnalyzer(spectrum, config)
|
||||||
@@ -622,10 +620,7 @@ class TestNormalizationModeMatrix:
|
|||||||
)
|
)
|
||||||
total = df["total_spectrum"].to_numpy()
|
total = df["total_spectrum"].to_numpy()
|
||||||
nonzero = total > 1e-12
|
nonzero = total > 1e-12
|
||||||
assert np.all(
|
assert np.all(np.abs(summed[nonzero] - total[nonzero]) <= total[nonzero] * 5e-4)
|
||||||
np.abs(summed[nonzero] - total[nonzero])
|
|
||||||
<= total[nonzero] * 5e-4
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_plot_from_csv_to_one_plots_branches_without_total(
|
def test_plot_from_csv_to_one_plots_branches_without_total(
|
||||||
self, tmp_path: Any
|
self, tmp_path: Any
|
||||||
@@ -635,9 +630,7 @@ class TestNormalizationModeMatrix:
|
|||||||
analyzer = BetaSpectrumAnalyzer(spectrum, config)
|
analyzer = BetaSpectrumAnalyzer(spectrum, config)
|
||||||
path, df = export_and_read(analyzer, tmp_path, "to_one_plot.csv")
|
path, df = export_and_read(analyzer, tmp_path, "to_one_plot.csv")
|
||||||
|
|
||||||
fig = Visualizer.plot_from_csv(
|
fig = Visualizer.plot_from_csv(path, save_path=None, show_components=True)
|
||||||
path, save_path=None, show_components=True
|
|
||||||
)
|
|
||||||
assert len(fig.axes) == 4
|
assert len(fig.axes) == 4
|
||||||
# Panel 1 has exactly the 3 branch curves — no total spectrum
|
# Panel 1 has exactly the 3 branch curves — no total spectrum
|
||||||
# exists in to_one mode.
|
# exists in to_one mode.
|
||||||
@@ -646,9 +639,7 @@ class TestNormalizationModeMatrix:
|
|||||||
assert len(curves) == 3
|
assert len(curves) == 3
|
||||||
energies = df["energy_MeV"].to_numpy()
|
energies = df["energy_MeV"].to_numpy()
|
||||||
assert np.allclose(curves[0][:, 0], energies)
|
assert np.allclose(curves[0][:, 0], energies)
|
||||||
assert np.allclose(
|
assert np.allclose(curves[0][:, 1], df["branch_1_spectrum"].to_numpy())
|
||||||
curves[0][:, 1], df["branch_1_spectrum"].to_numpy()
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_plot_analysis_to_one_returns_figure(self, tmp_path: Any) -> None:
|
def test_plot_analysis_to_one_returns_figure(self, tmp_path: Any) -> None:
|
||||||
config = make_multi_config(normalize_to_one=True)
|
config = make_multi_config(normalize_to_one=True)
|
||||||
@@ -669,11 +660,70 @@ class TestNormalizationModeMatrix:
|
|||||||
analyzer, tmp_path, "to_one_concise2.csv", csv_verbosity="concise"
|
analyzer, tmp_path, "to_one_concise2.csv", csv_verbosity="concise"
|
||||||
)
|
)
|
||||||
|
|
||||||
fig = Visualizer.plot_from_csv(
|
fig = Visualizer.plot_from_csv(path, save_path=None, show_components=True)
|
||||||
path, save_path=None, show_components=True
|
|
||||||
)
|
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert "Warning" in captured.out
|
assert "Warning" in captured.out
|
||||||
# Simple spectra view: one axis, 3 branch curves, no total curve.
|
# Simple spectra view: one axis, 3 branch curves, no total curve.
|
||||||
assert len(fig.axes) == 1
|
assert len(fig.axes) == 1
|
||||||
assert len(fig.axes[0].lines) == 3
|
assert len(fig.axes[0].lines) == 3
|
||||||
|
|
||||||
|
|
||||||
|
class TestSingleModeColumnCleanup:
|
||||||
|
"""Single-mode CSVs carry the factorized components once, unprefixed;
|
||||||
|
multi-mode CSVs carry them per branch; the visualizer reads whichever
|
||||||
|
naming the file actually uses."""
|
||||||
|
|
||||||
|
def test_single_mode_full_export_has_unprefixed_components_only(
|
||||||
|
self, tmp_path: Any
|
||||||
|
) -> None:
|
||||||
|
config = make_single_config(use_radiative=True)
|
||||||
|
spectrum = BetaSpectrum.from_config(config)
|
||||||
|
analyzer = BetaSpectrumAnalyzer(spectrum, config)
|
||||||
|
path, df = export_and_read(analyzer, tmp_path, "single_components.csv")
|
||||||
|
|
||||||
|
assert "PhaseSpace" in df.columns
|
||||||
|
assert "Radiative" in df.columns
|
||||||
|
assert "Fermi" in df.columns
|
||||||
|
assert "branch_1_spectrum" in df.columns
|
||||||
|
assert "branch_1_PhaseSpace" not in df.columns
|
||||||
|
assert "branch_1_Radiative" not in df.columns
|
||||||
|
|
||||||
|
def test_multi_mode_full_export_has_prefixed_core_factors(
|
||||||
|
self, tmp_path: Any
|
||||||
|
) -> None:
|
||||||
|
config = make_multi_config(use_radiative=True)
|
||||||
|
spectrum = BetaSpectrum.from_config(config)
|
||||||
|
analyzer = BetaSpectrumAnalyzer(spectrum, config)
|
||||||
|
path, df = export_and_read(analyzer, tmp_path, "multi_components.csv")
|
||||||
|
|
||||||
|
assert "branch_1_PhaseSpace" in df.columns
|
||||||
|
assert "branch_2_Radiative" in df.columns
|
||||||
|
assert "Fermi" in df.columns
|
||||||
|
# The bare core-factor names belong to single mode only.
|
||||||
|
assert "PhaseSpace" not in df.columns
|
||||||
|
assert "Radiative" not in df.columns
|
||||||
|
|
||||||
|
def test_debug_view_single_csv_includes_core_panels(self, tmp_path: Any) -> None:
|
||||||
|
config = make_single_config(use_radiative=True)
|
||||||
|
spectrum = BetaSpectrum.from_config(config)
|
||||||
|
analyzer = BetaSpectrumAnalyzer(spectrum, config)
|
||||||
|
path, _df = export_and_read(analyzer, tmp_path, "single_plot.csv")
|
||||||
|
|
||||||
|
fig = Visualizer.plot_from_csv(path, None, show_components=True)
|
||||||
|
assert len(fig.axes) == 4
|
||||||
|
# Panel 2: Fermi + the single branch's phase space.
|
||||||
|
assert len(fig.axes[1].lines) == 2
|
||||||
|
labels = [ln.get_label() for ln in fig.axes[1].lines]
|
||||||
|
assert "Fermi" in labels
|
||||||
|
assert any("PS Br" in str(lbl) for lbl in labels)
|
||||||
|
# Panel 4: the radiative correction for the single branch.
|
||||||
|
assert len(fig.axes[3].lines) == 1
|
||||||
|
|
||||||
|
def test_ylabel_uses_per_MeV_units(self, tmp_path: Any) -> None:
|
||||||
|
config = make_multi_config()
|
||||||
|
spectrum = BetaSpectrum.from_config(config)
|
||||||
|
analyzer = BetaSpectrumAnalyzer(spectrum, config)
|
||||||
|
path, _df = export_and_read(analyzer, tmp_path, "ylabel.csv")
|
||||||
|
|
||||||
|
fig = Visualizer.plot_from_csv(path, None, show_components=True)
|
||||||
|
assert "MeV$^{-1}$" in fig.axes[0].get_ylabel()
|
||||||
|
|||||||
@@ -161,9 +161,12 @@ class TestJsonInput:
|
|||||||
path = self._write_json(data)
|
path = self._write_json(data)
|
||||||
try:
|
try:
|
||||||
loaded = load_json_input(str(path))
|
loaded = load_json_input(str(path))
|
||||||
# Defaults should be filled in
|
# Defaults should be filled in. The detector_* schema keys were
|
||||||
assert loaded["detector_model"] == "gaussian"
|
# removed together with the detector_response module; JSON
|
||||||
assert loaded["detector_sigma_a_keV"] == 1.0
|
# defaults carry spectrum physics only.
|
||||||
|
assert "detector_model" not in loaded
|
||||||
|
assert loaded["neutrino_mass_MeV"] == 0.0
|
||||||
|
assert loaded["transition_type"] == "F2"
|
||||||
finally:
|
finally:
|
||||||
path.unlink()
|
path.unlink()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user