feat: v0.4.0 package restructuring — split into 4 modules
Separate monolithic beta_spectrum package into:
beta_spectrum — pure theory (components, spectrum, CLI, visualize)
exp_data — experimental data handling (spectrum, calibration,
fitters, corrections)
fitter — analysis framework (model, fit_engine, extractor,
result)
beta_spectrum.visualize — plotting (BetaSpectrumAnalyzer)
Phase 1 — exp_data module:
ExpSpectrum dataclass with metadata, normalization, serialization
EnergyCalibrator (linear/quadratic calibration)
GaussianFitter + PeakFitter for calibration sources
DeadTimeCorrection, PileUpCorrection, BackgroundSubtractor
43 new tests, ruff + mypy clean
Phase 2 — fitter module:
SpectrumModel wraps BetaSpectrum + DetectorResponse with convolution
SpectrumFitter with χ² minimization, covariance, profile likelihood,
confidence intervals, robust loss functions
CWExtractor + GVAExtractor for C(W) and g_V/g_A extraction
AnalysisResult + FitSummary for physics-specific results
CurveFitter general-purpose callable model fitter
34 new tests, ruff + mypy clean
Phase 3 — clean up beta_spectrum:
Remove cw_extractor.py and fitter.py (moved to fitter/)
Clean __init__.py to export only theory classes
Add backward-compat methods to CWExtractor
Fix BetaSpectrumAnalyzer._get_branch_spectrum returning BetaSpectrum
objects instead of numpy arrays (plot fix)
Phase 4 — package structure:
Auto-discovered by setuptools (4 packages)
All imports updated across 26 source files
Black formatting applied
322 tests pass (262 original + 60 new)
ruff + mypy clean on all source files
Bug fixes:
- Fix multi-branch spectrum plotting: universal component keys
(Fermi, Screening, etc.) don't have per-branch prefix; fixed
fallback lookup.
- Fix negative phase space beyond endpoint: BetaSpectrum returns
negative values for W > W0; clamp branch spectra to zero.
This commit is contained in:
@@ -163,7 +163,14 @@ class BetaSpectrumAnalyzer:
|
||||
def _get_branch_spectrum(self, i: int) -> np.ndarray:
|
||||
"""Get the spectrum array for branch i."""
|
||||
if i < len(self.spectrum.branch_spectra):
|
||||
return self.spectrum.branch_spectra[i]
|
||||
bs_obj = self.spectrum.branch_spectra[i]
|
||||
# branch_spectra stores BetaSpectrum objects — call them to get arrays
|
||||
if hasattr(bs_obj, "__call__"):
|
||||
val = bs_obj(self.W)
|
||||
# Clamp to zero beyond endpoint (phase space goes negative)
|
||||
W0 = T_to_W(self._unified_branches[i].endpoint_MeV)
|
||||
return np.where(self.W <= W0, val, 0.0)
|
||||
return np.asarray(bs_obj)
|
||||
# Single-branch: the spectrum itself is the only branch
|
||||
return self.spectrum(self.W)
|
||||
|
||||
@@ -276,15 +283,19 @@ class BetaSpectrumAnalyzer:
|
||||
|
||||
# Compute branch contributions (universal * intensity * branch_spectrum)
|
||||
branch_contribs = []
|
||||
# Universal components are shared (no per-branch key prefix)
|
||||
universal_names = [
|
||||
"Fermi", "Screening", "FiniteSizeL0",
|
||||
"ChargeDistributionU", "Exchange",
|
||||
]
|
||||
for i in range(self._n_branches):
|
||||
universal_product = np.ones_like(self.W)
|
||||
for name in [
|
||||
"Fermi", "Screening", "FiniteSizeL0",
|
||||
"ChargeDistributionU", "Exchange",
|
||||
]:
|
||||
for name in universal_names:
|
||||
key = self._get_component_key(i, name)
|
||||
if key in components:
|
||||
universal_product *= components[key]
|
||||
elif name in components: # fallback: universal components
|
||||
universal_product *= components[name]
|
||||
bs = self._get_branch_spectrum(i)
|
||||
contrib = universal_product * self._unified_branches[i].intensity * bs
|
||||
branch_contribs.append(contrib)
|
||||
@@ -348,15 +359,18 @@ class BetaSpectrumAnalyzer:
|
||||
|
||||
# Panel 1: Total spectrum + per-branch spectra
|
||||
branch_contribs = []
|
||||
universal_names = [
|
||||
"Fermi", "Screening", "FiniteSizeL0",
|
||||
"ChargeDistributionU", "Exchange",
|
||||
]
|
||||
for i in range(n_branches):
|
||||
universal_product = np.ones_like(self.W)
|
||||
for name in [
|
||||
"Fermi", "Screening", "FiniteSizeL0",
|
||||
"ChargeDistributionU", "Exchange",
|
||||
]:
|
||||
for name in universal_names:
|
||||
key = self._get_component_key(i, name)
|
||||
if key in components:
|
||||
universal_product *= components[key]
|
||||
elif name in components:
|
||||
universal_product *= components[name]
|
||||
bs = self._get_branch_spectrum(i)
|
||||
contrib = universal_product * self._unified_branches[i].intensity * bs
|
||||
branch_contribs.append(contrib)
|
||||
|
||||
Reference in New Issue
Block a user