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:
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
Analysis result container for beta spectrum fitting.
|
||||
|
||||
Wraps fit results with physics-specific metadata (nuclide, extracted
|
||||
C(W) values, g_V, g_A, etc.).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from beta_spectrum.spectrum import SpectrumConfig
|
||||
|
||||
|
||||
@dataclass
|
||||
class FitSummary:
|
||||
"""Compact fit summary for reporting."""
|
||||
|
||||
nuclide: str
|
||||
"""Nuclide identifier (e.g. 'Tc99')."""
|
||||
|
||||
converged: bool
|
||||
"""Whether the fit converged."""
|
||||
|
||||
chi2_per_dof: float
|
||||
"""Reduced χ²."""
|
||||
|
||||
p_value: float
|
||||
"""Fit p-value."""
|
||||
|
||||
parameters: Dict[str, Tuple[float, float]]
|
||||
"""Parameter names → (value, error)."""
|
||||
|
||||
def __str__(self) -> str:
|
||||
lines = [
|
||||
f"Fit Summary: {self.nuclide}",
|
||||
f" Converged: {self.converged}",
|
||||
f" χ²/ndof: {self.chi2_per_dof:.3f}",
|
||||
f" p-value: {self.p_value:.4f}",
|
||||
" Parameters:",
|
||||
]
|
||||
for name, (val, err) in self.parameters.items():
|
||||
lines.append(f" {name:20s} = {val:12.6f} ± {err:10.6f}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnalysisResult:
|
||||
"""Complete analysis result from spectrum fitting.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fit_result : FitResult
|
||||
Low-level fit result from SpectrumFitter.
|
||||
nuclide : str, optional
|
||||
Nuclide identifier.
|
||||
config : SpectrumConfig, optional
|
||||
Original spectrum configuration.
|
||||
cw_values : np.ndarray, optional
|
||||
Extracted C(W) values at each energy bin.
|
||||
cw_errors : np.ndarray, optional
|
||||
Uncertainties on C(W).
|
||||
gv_eff : float, optional
|
||||
Effective vector coupling constant.
|
||||
ga_eff : float, optional
|
||||
Effective axial coupling constant.
|
||||
endpoint_keV : float, optional
|
||||
Extracted endpoint energy.
|
||||
metadata : dict, optional
|
||||
Additional analysis metadata.
|
||||
"""
|
||||
|
||||
fit_result: Any # FitResult from SpectrumFitter
|
||||
nuclide: str = ""
|
||||
config: Optional["SpectrumConfig"] = None
|
||||
cw_values: Optional[np.ndarray] = None
|
||||
cw_errors: Optional[np.ndarray] = None
|
||||
gv_eff: Optional[float] = None
|
||||
ga_eff: Optional[float] = None
|
||||
endpoint_keV: Optional[float] = None
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def summary(self) -> FitSummary:
|
||||
"""Get a FitSummary from this result."""
|
||||
fr = self.fit_result
|
||||
param_names = self.metadata.get("param_names", None)
|
||||
params = {}
|
||||
if hasattr(fr, "parameters_with_errors"):
|
||||
params = fr.parameters_with_errors
|
||||
elif hasattr(fr, "parameters"):
|
||||
errors = np.sqrt(np.maximum(np.diag(fr.covariance), 0.0))
|
||||
for i in range(len(fr.parameters)):
|
||||
name = (
|
||||
param_names[i] if param_names and i < len(param_names) else f"p{i}"
|
||||
)
|
||||
params[name] = (float(fr.parameters[i]), float(errors[i]))
|
||||
|
||||
return FitSummary(
|
||||
nuclide=self.nuclide,
|
||||
converged=fr.success if hasattr(fr, "success") else False,
|
||||
chi2_per_dof=fr.chi2_per_dof if hasattr(fr, "chi2_per_dof") else 0.0,
|
||||
p_value=fr.p_value if hasattr(fr, "p_value") else 0.0,
|
||||
parameters=params,
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Serialize to dictionary."""
|
||||
return {
|
||||
"nuclide": self.nuclide,
|
||||
"converged": (
|
||||
self.fit_result.success
|
||||
if hasattr(self.fit_result, "success")
|
||||
else False
|
||||
),
|
||||
"chi2_per_dof": (
|
||||
self.fit_result.chi2_per_dof
|
||||
if hasattr(self.fit_result, "chi2_per_dof")
|
||||
else 0.0
|
||||
),
|
||||
"p_value": (
|
||||
self.fit_result.p_value if hasattr(self.fit_result, "p_value") else 0.0
|
||||
),
|
||||
"gv_eff": self.gv_eff,
|
||||
"ga_eff": self.ga_eff,
|
||||
"endpoint_keV": self.endpoint_keV,
|
||||
"metadata": self.metadata,
|
||||
}
|
||||
Reference in New Issue
Block a user