refactor: refine CLI options and enhance debug logging

- Remove --mode, --transition-type, and --decay-index CLI flags
  (parameters now come from ENSDF data or custom JSON input)
- Clarify energy units in all CLI help text (MeV throughout, keV for detector params)
- Enhance logging verbosity:
  - -v (INFO): shows source, component initialization, workflow steps
  - -vv (DEBUG): shows detailed internals of all spectrum components
    (parameters, evaluation counts, input/output ranges)
- Improve CSV headers with element notation (e.g. Tc99 -> Ru99)
- Add CONVENTIONS.md documenting energy unit and logging conventions
- Update TODO.md with completion status
This commit is contained in:
2026-05-05 09:08:08 +03:00
parent eb52c3b918
commit 330ebddaef
14 changed files with 470 additions and 78 deletions
+36 -3
View File
@@ -1,5 +1,7 @@
from pathlib import Path
from typing import Dict
from typing import Dict, Optional
import logging
import numpy as np
@@ -11,7 +13,14 @@ class ExchangeCorrection(SpectrumComponent):
Atomic xchange correction using Hayen 2018 Table X coefficients (App. G)
"""
def __init__(self, Z: int, filename: str | None = None, eps: float = 1e-6):
def __init__(
self,
Z: int,
filename: Optional[str] = None,
eps: float = 1e-6,
logger: Optional[logging.Logger] = None,
):
super().__init__(logger=logger)
self.Z = Z
self.eps = eps
@@ -20,6 +29,13 @@ class ExchangeCorrection(SpectrumComponent):
self.coeffs = self._load_coeffs(filename)
if self._logger:
self._logger.debug(
"ExchangeCorrection: Z=%d, coefficients loaded from %s",
Z,
filename,
)
def _load_coeffs(self, filename: str) -> Dict[str, float]:
data = np.genfromtxt(filename, delimiter=",", names=True)
@@ -42,6 +58,14 @@ class ExchangeCorrection(SpectrumComponent):
return Path(__file__).resolve().parent.parent / "data" / "exchange_coeff.csv"
def __call__(self, W: np.ndarray) -> np.ndarray:
if self._logger:
self._logger.debug(
"ExchangeCorrection: evaluating at %d points, W range=[%.4f, %.4f]",
len(W),
W.min(),
W.max(),
)
W = np.asarray(W)
# Define physical cutoff
@@ -71,4 +95,13 @@ class ExchangeCorrection(SpectrumComponent):
X_safe = 1.0 + term1 + term2 + term3 + term4
return np.asarray(X_safe, dtype=np.float64)
result = np.asarray(X_safe, dtype=np.float64)
if self._logger:
self._logger.debug(
"ExchangeCorrection: output range=[%.6e, %.6e]",
result.min(),
result.max(),
)
return result
+30 -2
View File
@@ -1,4 +1,5 @@
# components/fermi.py
import logging
import numpy as np
from scipy.special import loggamma
@@ -8,14 +9,32 @@ from ..utils import nuclear_radius
class FermiFunction(SpectrumComponent):
def __init__(self, Z: int, A: int):
def __init__(self, Z: int, A: int, logger: logging.Logger | None = None):
super().__init__(logger=logger)
self.Z = Z
self.A = A
self.alphaZ = ALPHA * Z
self.gamma = np.sqrt(1.0 - self.alphaZ**2)
self.R = nuclear_radius(A)
if self._logger:
self._logger.debug(
"FermiFunction: Z=%d, A=%d, alpha*Z=%.4f, gamma=%.6f, R=%.4f",
Z,
A,
self.alphaZ,
self.gamma,
self.R,
)
def __call__(self, W: np.ndarray) -> np.ndarray:
if self._logger:
self._logger.debug(
"FermiFunction: evaluating at %d points, W range=[%.4f, %.4f]",
len(W),
W.min(),
W.max(),
)
p = np.sqrt(W**2 - 1.0)
# Avoid division by zero
@@ -43,4 +62,13 @@ class FermiFunction(SpectrumComponent):
logF = log_prefactor + log_power + log_gamma_abs_sq - log_gamma_denom + log_exp
return np.asarray(np.exp(logF), dtype=np.float64)
result = np.asarray(np.exp(logF), dtype=np.float64)
if self._logger:
self._logger.debug(
"FermiFunction: output range=[%.6e, %.6e]",
result.min(),
result.max(),
)
return result
+61 -4
View File
@@ -1,3 +1,5 @@
import logging
import numpy as np
from beta_spectrum.constants import ALPHA
@@ -13,7 +15,8 @@ class FiniteSizeL0(SpectrumComponent):
Includes (1 + gamma)/2 prefactor
"""
def __init__(self, Z: int, A: int):
def __init__(self, Z: int, A: int, logger: logging.Logger | None = None):
super().__init__(logger=logger)
self.Z = Z
self.A = A
@@ -25,20 +28,74 @@ class FiniteSizeL0(SpectrumComponent):
# beta-minus decay
self.sign = -1
if self._logger:
self._logger.debug(
"FiniteSizeL0: Z=%d, A=%d, R=%.4f, alphaZ=%.4f, gamma=%.6f",
Z,
A,
self.R,
self.alphaZ,
self.gamma,
)
def __call__(self, W: np.ndarray) -> np.ndarray:
if self._logger:
self._logger.debug(
"FiniteSizeL0: evaluating at %d points, W range=[%.4f, %.4f]",
len(W),
W.min(),
W.max(),
)
# Terms from expansion
term1 = self.sign * self.alphaZ * W * self.R
term2 = (13 / 60) * self.alphaZ**2
term3 = -0.5 * (self.alphaZ * self.R) / W
return 1 + term1 + term2 + term3
result = 1 + term1 + term2 + term3
if self._logger:
self._logger.debug(
"FiniteSizeL0: output range=[%.6e, %.6e]",
result.min(),
result.max(),
)
return result
class ChargeDistributionU(SpectrumComponent):
def __init__(self, Z: int, A: int):
def __init__(self, Z: int, A: int, logger: logging.Logger | None = None):
super().__init__(logger=logger)
self.Z = Z
self.R = nuclear_radius(A)
self.alphaZ = ALPHA * Z
if self._logger:
self._logger.debug(
"ChargeDistributionU: Z=%d, A=%d, R=%.4f, alphaZ=%.4f",
Z,
A,
self.R,
self.alphaZ,
)
def __call__(self, W: np.ndarray) -> np.ndarray:
return 1.0 + (1 / 5) * (self.alphaZ * W * self.R) ** 2
if self._logger:
self._logger.debug(
"ChargeDistributionU: evaluating at %d points, W range=[%.4f, %.4f]",
len(W),
W.min(),
W.max(),
)
result = 1.0 + (1 / 5) * (self.alphaZ * W * self.R) ** 2
if self._logger:
self._logger.debug(
"ChargeDistributionU: output range=[%.6e, %.6e]",
result.min(),
result.max(),
)
return result
+3
View File
@@ -15,6 +15,7 @@ class PhaseSpace(SpectrumComponent):
transition_type: str = "A",
m_e: float = 1.0,
m_nu: float = 0.0,
logger=None,
):
"""
Create instance of phase space with given decay parameters.
@@ -25,7 +26,9 @@ class PhaseSpace(SpectrumComponent):
transition_type: allowed or N-order forbidden, should be [A, F1, F1U, F2, F2U, F3, F3U, F4]
m_e: 1st body mass (electron by default: m_e = 1)
m_nu:2nd body mass (massless neutrino by default: m_nu = 0)
logger: Optional logger for debug/info output
"""
super().__init__(logger=logger)
self.tr_type = transition_type
if self.tr_type not in ["A", "F1", "F1U", "F2", "F2U", "F3", "F3U", "F4"]:
raise ValueError(
+34 -1
View File
@@ -1,4 +1,6 @@
# components/radiative.py
import logging
import numpy as np
from scipy.special import spence
@@ -41,6 +43,7 @@ class RadiativeCorrection(SpectrumComponent):
A: int | None = None,
use_endpoint_resummation: bool = True,
delta_cut: float = 1e-3,
logger: logging.Logger | None = None,
):
"""
Args:
@@ -49,7 +52,9 @@ class RadiativeCorrection(SpectrumComponent):
A: Mass number (optional, affects nuclear model correction)
use_endpoint_resummation: Apply Eq.(53) to handle ln(W0 - W) divergence
delta_cut: Only apply resummation when (W0 - W) < delta_cut (spec Section 3.4)
logger: Optional logger for debug/info output
"""
super().__init__(logger=logger)
self.W0 = W0
self.Z = Z
self.A = A
@@ -65,6 +70,16 @@ class RadiativeCorrection(SpectrumComponent):
# Precompute O(Z*alpha^2) energy-independent nuclear part if A is given
self._delta_F = self._compute_nuclear_model_correction(Z, A)
if self._logger:
self._logger.debug(
"RadiativeCorrection: W0=%.4f, Z=%d, A=%s, delta_cut=%.2e, resummation=%s",
W0,
Z,
A,
delta_cut,
use_endpoint_resummation,
)
def __call__(self, W: np.ndarray) -> np.ndarray:
"""
Calculate R(W, W0) = 1 + delta_1(W, W0) + delta_2(Z, W).
@@ -75,6 +90,15 @@ class RadiativeCorrection(SpectrumComponent):
Returns:
Radiative correction factor array
"""
if self._logger:
self._logger.debug(
"RadiativeCorrection: evaluating at %d points, W range=[%.4f, %.4f], W0=%.4f",
len(W),
W.min(),
W.max(),
self.W0,
)
W = np.asarray(W, dtype=float)
# Avoid numerical issues exactly at endpoint W0
@@ -95,7 +119,16 @@ class RadiativeCorrection(SpectrumComponent):
delta_R_finite = delta_1 + delta_2
delta_R[mask_finite] = delta_R_finite
return np.asarray(1.0 + delta_R, dtype=np.float64)
result = np.asarray(1.0 + delta_R, dtype=np.float64)
if self._logger:
self._logger.debug(
"RadiativeCorrection: output range=[%.6e, %.6e]",
result.min(),
result.max(),
)
return result
def _delta_1(self, W: np.ndarray) -> np.ndarray:
"""
+31 -1
View File
@@ -1,5 +1,7 @@
from typing import Any
import logging
import numpy as np
from beta_spectrum.base import SpectrumComponent
@@ -26,6 +28,7 @@ class ScreeningCorrection(SpectrumComponent):
Delta: float = 0.01,
eps: float = 1e-6,
C: float = 0.015,
logger: logging.Logger | None = None,
):
"""
Parameters
@@ -39,6 +42,7 @@ class ScreeningCorrection(SpectrumComponent):
Delta : switching width
eps : numerical floor for W_tilde
"""
super().__init__(logger=logger)
self.fermi = fermi_function
self.Z = fermi_function.Z
self.Ws = Ws
@@ -47,10 +51,27 @@ class ScreeningCorrection(SpectrumComponent):
self._C = C
self.V0 = V0 if V0 is not None else self._estimate_V0()
if self._logger:
self._logger.debug(
"ScreeningCorrection: Z=%d, V0=%.6f, Ws=%.4f, Delta=%.4f",
self.Z,
self.V0,
self.Ws,
self.Delta,
)
# --------------------
# Core evaluation
# --------------------
def __call__(self, W: np.ndarray) -> np.ndarray:
if self._logger:
self._logger.debug(
"ScreeningCorrection: evaluating at %d points, W range=[%.4f, %.4f]",
len(W),
W.min(),
W.max(),
)
W = np.asarray(W)
# Energy shift by screening potential V0
@@ -70,7 +91,16 @@ class ScreeningCorrection(SpectrumComponent):
# Smooth switching
f = self._switching_function(W)
return np.asarray(1.0 + f * (S_raw - 1.0), dtype=np.float64)
result = np.asarray(1.0 + f * (S_raw - 1.0), dtype=np.float64)
if self._logger:
self._logger.debug(
"ScreeningCorrection: output range=[%.6e, %.6e]",
result.min(),
result.max(),
)
return result
# --------------------
# Helpers