Compare commits
18
Commits
ee7aedc80a
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
410e8b6524 | ||
|
|
dc8e26403b | ||
|
|
6aced1d8ac | ||
|
|
e8b73eb20f | ||
|
|
62042ae92a | ||
|
|
bc287e42a6 | ||
|
|
57d5c7334a | ||
|
|
abde581146 | ||
|
|
67de4e3baa | ||
|
|
2788ea0683 | ||
|
|
7f47a876ae | ||
|
|
7e31ac4b4e | ||
|
|
36174b5d25 | ||
|
|
3590ca91a1 | ||
|
|
9c3dd80b5c | ||
|
|
d166532779 | ||
|
|
18a46ad02f | ||
|
|
62bf7fdac2 |
@@ -1,6 +1,6 @@
|
|||||||
# Agent Instructions — beta-spectrum
|
# Agent Instructions — beta-spectrum
|
||||||
|
|
||||||
High-precision beta decay spectrum calculator. Extracts shape factor C(W) from ⁹⁹Tc beta spectrum to parametrize g_V coupling.
|
High-precision beta decay spectrum shape calculator. Since v0.5.0 the repository is dedicated solely to spectrum shape calculation: detector response, calibration, fitting and C(W)/g_V/g_A extraction live in separate, dedicated projects.
|
||||||
|
|
||||||
## Stack & Commands
|
## Stack & Commands
|
||||||
|
|
||||||
@@ -68,7 +68,6 @@ After quality gates pass, run:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
bs_pnpi --nuclide Tc99 -vv --output ./output/verification.csv --plot ./output/verification.png --log-file ./output/debug.log 2>&1 | tee ./output/debug_stdout.log
|
bs_pnpi --nuclide Tc99 -vv --output ./output/verification.csv --plot ./output/verification.png --log-file ./output/debug.log 2>&1 | tee ./output/debug_stdout.log
|
||||||
bs_exp --help # verify experimental data CLI
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Verify parameter consistency across all components. Save artifacts to `./output/`.
|
Verify parameter consistency across all components. Save artifacts to `./output/`.
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ m_e = ℏ = c = 1
|
|||||||
|
|
||||||
**External API:** human-readable MeV/keV. Check the unit suffix in parameter names:
|
**External API:** human-readable MeV/keV. Check the unit suffix in parameter names:
|
||||||
- `_MeV` suffix → value is in MeV
|
- `_MeV` suffix → value is in MeV
|
||||||
- `_keV` suffix → value is in keV (detector params only)
|
- `_keV` suffix → value is in keV
|
||||||
- No suffix on energy → value is in m_e units (internal)
|
- No suffix on energy → value is in m_e units (internal)
|
||||||
|
|
||||||
### Conversion
|
### Conversion
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
A modular Python package for calculating high-precision **beta decay energy spectra**. It models the spectral shape of nuclear transitions by combining well-established physical corrections into a coherent, configurable pipeline.
|
A modular Python package for calculating high-precision **beta decay energy spectra**. It models the spectral shape of nuclear transitions by combining well-established physical corrections into a coherent, configurable pipeline.
|
||||||
|
|
||||||
|
Since v0.5.0 the project scope is the spectrum-shape **calculation** alone: detector response modeling, data calibration and fitting/extraction live in separate, dedicated projects.
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
|
|
||||||
- Numerical evaluation from near-zero kinetic energies up to the endpoint
|
- Numerical evaluation from near-zero kinetic energies up to the endpoint
|
||||||
@@ -11,6 +13,8 @@ A modular Python package for calculating high-precision **beta decay energy spec
|
|||||||
- Normal mode: single spectrum plot with nuclear data header
|
- Normal mode: single spectrum plot with nuclear data header
|
||||||
- Debug mode (`-vv`): 4-panel view with all correction factors
|
- Debug mode (`-vv`): 4-panel view with all correction factors
|
||||||
- All plots include commit hash and UTC timestamp for traceability
|
- All plots include commit hash and UTC timestamp for traceability
|
||||||
|
- Multi-branch decays with intensity weighting and per-branch endpoints
|
||||||
|
- Nuclear data retrieval from ENSDF via paceENSDF (`bs_pnpi --nuclide Tc99`)
|
||||||
- Comprehensive scientific reference in [`docs/`](docs/)
|
- Comprehensive scientific reference in [`docs/`](docs/)
|
||||||
- Debug verification workflow: `bs_pnpi -vv` validates all parameters end-to-end
|
- Debug verification workflow: `bs_pnpi -vv` validates all parameters end-to-end
|
||||||
|
|
||||||
@@ -22,7 +26,7 @@ The beta spectrum is constructed as a product of independent correction factors:
|
|||||||
|
|
||||||
$$N(W) \\propto p W (W_0 - W)^2 \\times \\prod_i C_i(W)$$
|
$$N(W) \\propto p W (W_0 - W)^2 \\times \\prod_i C_i(W)$$
|
||||||
|
|
||||||
Each $C_i(W)$ is an independently implemented and toggleable component.
|
Each $C_i(W)$ is an independently implemented and toggleable component. The phase space and the Fermi function are the unconditional core: every spectrum carries their product, and the higher-order corrections multiply on top of it.
|
||||||
|
|
||||||
______________________________________________________________________
|
______________________________________________________________________
|
||||||
|
|
||||||
@@ -31,68 +35,46 @@ ______________________________________________________________________
|
|||||||
```
|
```
|
||||||
beta_shape_pnpi/
|
beta_shape_pnpi/
|
||||||
├── pyproject.toml
|
├── pyproject.toml
|
||||||
├── docs/ # Obsidian knowledge base (physics theory & references)
|
├── docs/ ← Obsidian knowledge base (physics theory)
|
||||||
│ ├── README.md # Vault overview + navigation index
|
│ ├── README.md ← Vault overview + navigation index
|
||||||
│ ├── 00-beta-spectrum-overview.md # Master equation, all corrections table
|
│ ├── 00-beta-spectrum-overview.md
|
||||||
│ ├── 01-fermi-function.md # Fermi function F₀(Z,W)
|
│ ├── 02-phase-space.md
|
||||||
│ ├── 02-phase-space.md # Phase space p·W·(W₀−W)²
|
│ └── corrections/ ← One note per correction component
|
||||||
│ ├── 03-finite-size.md # L₀, U, DFS corrections
|
├── beta_spectrum/ ← theory-only core package
|
||||||
│ ├── 04-screening-correction.md # Atomic screening S(Z,W)
|
│ ├── __init__.py ← Public API
|
||||||
│ ├── 05-exchange-correction.md # Electron exchange X(Z,W)
|
│ ├── base.py ← Abstract SpectrumComponent base class
|
||||||
│ ├── 06-radiative-corrections.md # Outer radiative δᵣ(W,W₀)
|
│ ├── constants.py ← Physical constants (natural units, m_e = 1)
|
||||||
│ ├── 07-recoil-effects.md # Weak magnetism, recoil kinematics
|
│ ├── utils.py ← T<->W conversion, momentum, nuclear radius
|
||||||
│ ├── 08-detector-response.md # Detector response models
|
│ ├── spectrum.py ← BetaSpectrum + SpectrumConfig + BranchConfig
|
||||||
│ ├── 10-nuclear-structure.md # Shape factor C(Z,W), BB vs HS formalisms
|
│ ├── nuclear_data.py ← paceENSDF integration + JSON input support
|
||||||
│ ├── 12-atomic-overlap.md # Bahcall correction r(Z,W)
|
│ ├── cli.py ← Command-line interface (bs_pnpi)
|
||||||
│ ├── 13-chemical-effects.md # Molecular environment effects
|
│ ├── logging_utils.py ← Structured logging helpers
|
||||||
├── beta_spectrum/ ← pure theory core
|
│ ├── components/
|
||||||
│ ├── __init__.py # Public API: constants, utilities, classes
|
│ │ ├── phase_space.py ← Phase space shape (p·W·(W₀−W)²)
|
||||||
│ ├── base.py # Abstract SpectrumComponent base class
|
│ │ ├── fermi.py ← Coulomb correction (loggamma for stability)
|
||||||
│ ├── constants.py # Physical constants (natural units, m_e = 1)
|
│ │ ├── finite_size.py ← L0 expansion + charge distribution U term
|
||||||
│ ├── utils.py # Helpers: T<->W conversion, momentum, nuclear radius
|
│ │ ├── screening.py ← Atomic electron screening (ratio method)
|
||||||
│ ├── spectrum.py # BetaSpectrum + SpectrumConfig + BranchConfig
|
│ │ ├── exchange.py ← Hayen-2018 empirical fit coefficients
|
||||||
│ ├── nuclear_data.py # paceENSDF integration + JSON input support
|
│ │ └── radiative.py ← Outer radiative corrections, soft-photon resummation
|
||||||
│ ├── cli.py # Command-line interface (bs_pnpi)
|
│ ├── data/
|
||||||
│ ├── logging_utils.py # Structured logging helpers
|
│ │ └── exchange_coeff.csv ← Tabulated coefficients for X(Z,W), Z=2..120
|
||||||
│ └── components/
|
│ └── output/ ← visualization, CSV export
|
||||||
│ │ ├── phase_space.py ✓ Phase space shape (p·W·(W₀−W)²)
|
│ ├── core.py ← BetaSpectrumAnalyzer
|
||||||
│ │ ├── fermi.py ✓ Coulomb correction (loggamma for stability)
|
│ ├── csv_export.py ← CSV export
|
||||||
│ │ ├── finite_size.py ✓ L0 expansion + charge distribution U term
|
│ └── visualization.py ← Plotting built from exported CSV
|
||||||
│ │ ├── screening.py ✓ Atomic electron screening (ratio method)
|
|
||||||
│ │ ├── exchange.py ✓ Hayen-2018 empirical fit coefficients
|
|
||||||
│ │ ├── radiative.py ✓ Outer radiative corrections, soft-photon resummation
|
|
||||||
│ │ └── detector_response.py ✓ Analytical detector response models
|
|
||||||
├── beta_spectrum/output/ ← output, plotting, visualization, CSV export
|
|
||||||
│ ├── __init__.py # BetaSpectrumAnalyzer, CSVEmitter, Visualizer
|
|
||||||
│ ├── core.py # BetaSpectrumAnalyzer base class
|
|
||||||
│ ├── csv_export.py # CSV export functionality
|
|
||||||
│ └── visualization.py # Plotting and visualization functionality
|
|
||||||
├── exp_data/ ← experimental data handling
|
|
||||||
│ ├── __init__.py # ExpSpectrum, EnergyCalibrator, GaussianFitter, etc.
|
|
||||||
│ ├── spectrum.py # ExpSpectrum dataclass (energies, counts, errors, metadata)
|
|
||||||
│ ├── calibration.py # EnergyCalibrator (linear/quadratic channel→energy)
|
|
||||||
│ ├── fitters.py # GaussianFitter + PeakFitter for cal sources
|
|
||||||
│ └── corrections.py # DeadTimeCorrection, PileUpCorrection, BackgroundSubtractor
|
|
||||||
├── fitter/ ← analysis framework
|
|
||||||
│ ├── __init__.py # SpectrumModel, SpectrumFitter, CWExtractor, etc.
|
|
||||||
│ ├── model.py # SpectrumModel (BetaSpectrum + DetectorResponse wrapper)
|
|
||||||
│ ├── fit_engine.py # SpectrumFitter + CurveFitter + FitConfig + FitResult
|
|
||||||
│ ├── extractor.py # CWExtractor, GVAExtractor, CWExtractionResult, GVAExtractionResult
|
|
||||||
│ └── result.py # AnalysisResult + FitSummary
|
|
||||||
├── tests/
|
|
||||||
│ ├── exp_data/ # exp_data module tests
|
|
||||||
│ ├── fitter/ # fitter module tests
|
|
||||||
│ ├── physics/ # Physics component tests
|
|
||||||
│ ├── quality/ # Code quality tests (logging, API, nuclear data)
|
|
||||||
│ ├── integration/ # Full pipeline integration tests
|
|
||||||
│ └── common/ # Shared hypothesis + property tests
|
|
||||||
├── data/
|
├── data/
|
||||||
│ ├── exchange_coeff.csv # Tabulated coefficients for X(Z,W), Z=2..120
|
│ └── custom_input_example.json ← Sample JSON input file
|
||||||
│ └── custom_input_example.json # Sample JSON input file
|
├── notebooks/ ← Calculator notebook + nbmake conftest
|
||||||
├── output/ # Test run outputs (gitignored except .gitkeep)
|
├── output/ ← Test run outputs (gitignored except .gitkeep)
|
||||||
├── AGENTS.md # Agent instructions: stack, git workflow, TDD, quality gates
|
├── tests/
|
||||||
├── CONVENTIONS.md # Physics conventions: natural units, energy representation, component design
|
│ ├── physics/ ← Physics component tests
|
||||||
├── TODO.md # Development roadmap
|
│ ├── quality/ ← Code quality tests (logging, API, nuclear data)
|
||||||
|
│ ├── integration/ ← CLI and pipeline integration tests
|
||||||
|
│ └── common/ ← Shared hypothesis + property tests
|
||||||
|
├── scripts/ ← docs_check.py, notebook runners
|
||||||
|
├── AGENTS.md ← Agent instructions: stack, git workflow, TDD, quality gates
|
||||||
|
├── CONVENTIONS.md ← Physics conventions: natural units, energy representation, component design
|
||||||
|
├── TODO.md ← Development roadmap
|
||||||
```
|
```
|
||||||
|
|
||||||
______________________________________________________________________
|
______________________________________________________________________
|
||||||
@@ -133,9 +115,9 @@ In `beta_spectrum.output`. Provides debugging and visualization tools:
|
|||||||
- `export_to_csv(filename)` — export via pandas DataFrame with YAML-style metadata header:
|
- `export_to_csv(filename)` — export via pandas DataFrame with YAML-style metadata header:
|
||||||
- the basic spectrum is always the core `phase_space × fermi` (the header carries a `# core:` line); finite size, charge distribution, screening, exchange and radiative are higher-order corrections
|
- the basic spectrum is always the core `phase_space × fermi` (the header carries a `# core:` line); finite size, charge distribution, screening, exchange and radiative are higher-order corrections
|
||||||
- `branch_{i}_spectrum` columns contain the *physical* branch spectra weighted by intensity and normalized so that the sum of branch columns equals `total_spectrum`
|
- `branch_{i}_spectrum` columns contain the *physical* branch spectra weighted by intensity and normalized so that the sum of branch columns equals `total_spectrum`
|
||||||
- with `normalize_to_one=True` each branch column integrates to 1 (intensity ignored, no total column)
|
- with `separate_branches=True` (CLI `--separate-branches`) each branch column integrates to 1 (intensity ignored, no total_spectrum column)
|
||||||
- `parse_spectrum_csv(path)` — read an exported CSV and its metadata header (module function in `beta_spectrum.output.visualization`)
|
- `parse_spectrum_csv(path)` — read an exported CSV and its metadata header (module function in `beta_spectrum.output.visualization`)
|
||||||
- `get_data()` — return all numerical data for custom analysis
|
- `get_data()` — return all numerical data for downstream analysis
|
||||||
|
|
||||||
______________________________________________________________________
|
______________________________________________________________________
|
||||||
|
|
||||||
@@ -163,27 +145,21 @@ analyzer.plot_analysis("debug.png", show_components=True) # Debug mode (4-panel
|
|||||||
analyzer.export_to_csv("spectrum.csv") # Export data to CSV
|
analyzer.export_to_csv("spectrum.csv") # Export data to CSV
|
||||||
```
|
```
|
||||||
|
|
||||||
### Fitting and C(W) extraction
|
### From the command line
|
||||||
|
|
||||||
```python
|
```bash
|
||||||
from beta_spectrum import SpectrumConfig, BetaSpectrum
|
bs_pnpi --nuclide Tc99 -vv --output spectrum.csv # ENSDF lookup + debug export
|
||||||
from fitter import SpectrumModel, SpectrumFitter, CWExtractor
|
bs_pnpi --input data/custom_input_example.json # JSON-driven configuration
|
||||||
from exp_data.spectrum import ExpSpectrum
|
|
||||||
|
|
||||||
# Build theoretical model
|
|
||||||
spectrum = BetaSpectrum.from_config(config)
|
|
||||||
model = SpectrumModel(spectrum, detector_response=detector)
|
|
||||||
|
|
||||||
# Fit to experimental data
|
|
||||||
exp = ExpSpectrum(energies=energies_keV, counts=counts, errors=errors)
|
|
||||||
fitter = SpectrumFitter(model, exp.energies, exp.counts, exp.errors)
|
|
||||||
result = fitter.fit(x0=[1.0, 0.0])
|
|
||||||
|
|
||||||
# Extract C(W) shape factor
|
|
||||||
extractor = CWExtractor(config)
|
|
||||||
cw_result = extractor.extract(measured, errors, energies_keV, model_values, endpoint_keV=Q)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
CLI flags (`--e-step`, `--neutrino-mass`, `--intensity-cutoff`, `--csv-separator`, `--separate-branches`) override JSON input values only when explicitly given; every long option has a single-letter alias.
|
||||||
|
|
||||||
|
______________________________________________________________________
|
||||||
|
|
||||||
|
## Nuclear Data (paceENSDF)
|
||||||
|
|
||||||
|
`beta_spectrum.nuclear_data` retrieves decay data automatically: `get_decay_info_from_paceENSDF()`, `decay_info_to_config()`, and `create_config_from_source("paceENSDF", nuclide="Tc99")`. Z, A, endpoint energies, half-life, spin/parity, branch intensities, log*ft and forbiddenness are populated from ENSDF; per-branch endpoints are the measured transition endpoint energies (max endpoint across branches defines the spectrum endpoint). paceENSDF is an optional extra: `pip install ".[ensdf]"`.
|
||||||
|
|
||||||
______________________________________________________________________
|
______________________________________________________________________
|
||||||
|
|
||||||
## Implementation Status
|
## Implementation Status
|
||||||
@@ -191,9 +167,6 @@ ______________________________________________________________________
|
|||||||
For a complete list of implemented and planned features, see [TODO.md](TODO.md). Key highlights:
|
For a complete list of implemented and planned features, see [TODO.md](TODO.md). Key highlights:
|
||||||
|
|
||||||
- **Physics corrections**: phase space, Fermi function, finite size, screening, exchange, radiative (with delta_cut resummation)
|
- **Physics corrections**: phase space, Fermi function, finite size, screening, exchange, radiative (with delta_cut resummation)
|
||||||
- **Detector response**: Gaussian, Gaussian+tail, Tikhonov models with tabulated support
|
|
||||||
- **Multi-branch decay**: intensity-weighted sum of branch spectra, per-branch calculators with branch-specific endpoint, transition type (from ENSDF), and `--intensity-cutoff` CLI filter
|
- **Multi-branch decay**: intensity-weighted sum of branch spectra, per-branch calculators with branch-specific endpoint, transition type (from ENSDF), and `--intensity-cutoff` CLI filter
|
||||||
- **Analysis tools**: χ² fitter (SpectrumFitter + CurveFitter), C(W) extraction pipeline, Kurie plot analysis, g_V/g_A extraction
|
- **CLI**: `bs_pnpi` with full paceENSDF integration, structured logging (`-v`/`-vv`/`-q`), `--dry-run`, `--version`, `--log-file`
|
||||||
- **Experimental data**: ExpSpectrum container, energy calibration, peak fitting, dead-time/pile-up correction
|
- **CSV metadata headers** with nuclide, decay and calculation parameters plus software version and git commit hash
|
||||||
- **CLI**: `bs_pnpi --nuclide Tc99` — full paceENSDF integration (auto-lookup of Z, A, Q-value, transition type, half-life, branches, forbiddenness from ENSDF)
|
|
||||||
- **paceENSDF integration**: `get_decay_info_from_paceENSDF()`, `create_config_from_source("paceENSDF", nuclide="Tc99")`, `_parse_nuclide_symbol()` for Z=1..98, FORBIDDENNESS_MAP, `--nuclide` CLI flag
|
|
||||||
|
|||||||
@@ -1,302 +1,119 @@
|
|||||||
# Strategic Development Roadmap
|
# Strategic Development Roadmap
|
||||||
|
|
||||||
## Scientific Goal
|
## Scientific Scope
|
||||||
|
|
||||||
Extract the shape factor C(W) from the experimentally measured beta spectrum of ⁹⁹Tc, and parametrize C(W) via the vector coupling constant g_V from the theory of beta decay.
|
High-precision calculation of beta-decay spectrum shapes. Given a nuclide, decay data and a set of correction toggles, the calculator produces C(W)-level spectral shapes accurate to a few parts in 10⁻⁴ down to 1 keV for low-to-medium Z nuclei (Hayen et al. approach).
|
||||||
|
|
||||||
This requires repeating the analysis pipeline of [Paulsen et al., Phys. Rev. C **110**, 055503 (2024)](https://doi.org/10.1103/PhysRevC/110/055503), which used Metallic Magnetic Calorimeters (MMCs) to measure the ⁹⁹Tc beta spectrum with sub-keV threshold and ~100 eV resolution.
|
For forbidden nonunique decays the shape factor C(W) enters the calculated spectrum as an **input**. Extracting C(W) from measured spectra and parametrizing it through coupling constants (SSM/SSNME, g_V/g_A fits, detector response modeling) is analysis work that lives in separate dedicated projects and is **out of scope** for this repository since v0.5.0.
|
||||||
|
|
||||||
See `docs/refs/2024_Paulsen_T99_beta_spectrum/summary.md` for full analysis details.
|
|
||||||
|
|
||||||
______________________________________________________________________
|
______________________________________________________________________
|
||||||
|
|
||||||
## Package Architecture (v0.4.0 — In Progress)
|
## Package Architecture (v0.5.0)
|
||||||
|
|
||||||
### Current State (v0.3.x)
|
Single distributable, `beta_spectrum`:
|
||||||
|
|
||||||
Everything lives in `beta_spectrum/` — a monolithic package where theory, fitting, detector response, and analysis are intermingled. This is becoming unwieldy.
|
|
||||||
|
|
||||||
### Target Architecture (v0.4.0)
|
|
||||||
|
|
||||||
```
|
```
|
||||||
beta_spectrum/ ← theory-only core
|
beta_spectrum/ ← theory-only core package
|
||||||
├── __init__.py
|
│ ├── __init__.py ← public API
|
||||||
├── base.py ← SpectrumComponent base
|
│ ├── base.py ← SpectrumComponent base
|
||||||
├── constants.py ← physical constants
|
│ ├── constants.py ← physical constants
|
||||||
├── utils.py ← T_to_W, W_to_T, momentum
|
│ ├── utils.py ← T_to_W, W_to_T, momentum
|
||||||
├── spectrum.py ← BetaSpectrum, SpectrumConfig, BranchConfig
|
│ ├── spectrum.py ← BetaSpectrum, SpectrumConfig, BranchConfig
|
||||||
├── components/ ← individual correction factors
|
│ ├── nuclear_data.py ← paceENSDF integration + JSON input
|
||||||
│ ├── phase_space.py
|
│ ├── cli.py ← bs_pnpi command-line interface
|
||||||
│ ├── fermi.py
|
│ ├── logging_utils.py ← structured logging helpers
|
||||||
│ ├── finite_size.py
|
│ ├── components/ ← individual correction factors
|
||||||
│ ├── screening.py
|
│ │ ├── phase_space.py
|
||||||
│ ├── exchange.py
|
│ │ ├── fermi.py
|
||||||
│ ├── radiative.py
|
│ │ ├── finite_size.py
|
||||||
│ └── detector_response.py ← stays here (part of theory)
|
│ │ ├── screening.py
|
||||||
└── nuclear_data.py ← paceENSDF integration
|
│ │ ├── exchange.py
|
||||||
|
│ │ └── radiative.py
|
||||||
exp_data/ ← experimental data handling
|
│ ├── data/
|
||||||
├── __init__.py
|
│ │ └── exchange_coeff.csv
|
||||||
├── raw_reader.py ← raw data loading (ROOT, CSV, ASCII)
|
│ └── output/ ← visualization, CSV export subpackage
|
||||||
├── calibration.py ← energy calibration, line fitting
|
│ ├── core.py ← BetaSpectrumAnalyzer
|
||||||
├── fitters.py ← simple Gaussian fitters for cal sources
|
│ ├── csv_export.py ← CSVEmitter
|
||||||
├── corrections.py ← dead-time, pile-up, background
|
│ └── visualization.py ← Visualizer, parse_spectrum_csv
|
||||||
└── spectrum.py ← experimental spectrum container
|
|
||||||
|
|
||||||
fitter/ ← main analysis framework
|
|
||||||
├── __init__.py
|
|
||||||
├── model.py ← theoretical model + detector convolution
|
|
||||||
├── fit_engine.py ← multi-parameter fitting orchestration
|
|
||||||
├── extractor.py ← C(W), g_V, g_A extraction
|
|
||||||
├── result.py ← FitResult, analysis results
|
|
||||||
# └── report.py ← PDF report generation (deferred)
|
|
||||||
|
|
||||||
output/ ← output, visualization, CSV export
|
|
||||||
├── __init__.py ← BetaSpectrumAnalyzer, CSVEmitter, Visualizer
|
|
||||||
├── core.py ← BetaSpectrumAnalyzer base class
|
|
||||||
├── csv_export.py ← CSV export functionality
|
|
||||||
└── visualization.py ← Plotting and visualization functionality
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Rationale
|
### Rationale
|
||||||
|
|
||||||
- **`beta_spectrum`** — pure theory. Given a nuclide and corrections, produces a theoretical spectrum. No fitting, no data, no detector convolution.
|
- **Pure theory.** Given a nuclide and corrections, the package produces a theoretical spectrum. No fitting, no experimental data, no detector convolution.
|
||||||
- **`exp_data`** — experimental data handling. Loading raw detector outputs, calibrating energy scale, fitting calibration peaks, applying instrumental corrections.
|
- **`beta_spectrum.output`** is a calculation-output subpackage (plotting and CSV export of computed spectra), not data analysis.
|
||||||
- **`fitter`** — the analysis workhorse. Takes theory model + detector response + experimental data → performs convolution + fitting → extracts physics parameters.
|
|
||||||
|
|
||||||
### Migration Plan
|
### Scope Reduction (v0.5.0)
|
||||||
|
|
||||||
#### Phase 1: Create `exp_data` module ✅ COMPLETE
|
- `exp_data/` and `fitter/` packages (experimental data handling, calibration, χ² fit engine, C(W)/g_V/g_A extraction), their tests, the `bs_exp` CLI and the ROOT extra: removed from this repository (restore from history up to `67de4e3` if an analysis project needs them).
|
||||||
|
- `detector_response.py`: removed earlier (up to `5f99566`); the deletion decision is now final.
|
||||||
- [x] `exp_data/spectrum.py` — `ExpSpectrum` dataclass: energies, counts, errors, metadata
|
- `data/raw/` local copies of detector runs and Am-241 calibration files: removed (canonical copies on experiment storage).
|
||||||
- [x] `exp_data/calibration.py` — `EnergyCalibrator` + `CalibrationResult`: linear/quadratic channel→energy
|
- `docs/gA-extraction/`, `docs/nuclear-structure/`, `docs/refs/`, `docs/corrections/07-detector-response.md`: removed; literature and nuclear-structure research is to be redone in dedicated projects.
|
||||||
- [x] `exp_data/fitters.py` — `GaussianFitter` + `PeakFitter`: peak detection and fitting
|
|
||||||
- [x] `exp_data/corrections.py` — `DeadTimeCorrection`, `PileUpCorrection`, `BackgroundSubtractor`
|
|
||||||
- [x] 43 tests, ruff + mypy clean
|
|
||||||
|
|
||||||
#### Phase 2: Create `fitter` module ✅ COMPLETE
|
|
||||||
|
|
||||||
- [x] `fitter/model.py` — `SpectrumModel`: wraps `BetaSpectrum` + `DetectorResponse`, callable model with convolution
|
|
||||||
- [x] `fitter/fit_engine.py` — `SpectrumFitter` + `FitConfig` + `FitResult`: χ² fitting, covariance, profile likelihood, confidence intervals
|
|
||||||
- [x] `fitter/extractor.py` — `CWExtractor` + `GVAExtractor`: C(W) and g_V/g_A extraction
|
|
||||||
- [x] `fitter/result.py` — `AnalysisResult` + `FitSummary`: physics-specific result container
|
|
||||||
- [x] 34 tests, ruff + mypy clean
|
|
||||||
- [x] Detector convolution handles W < 1.0 edge cases
|
|
||||||
|
|
||||||
#### Phase 3: Clean up `beta_spectrum` ✅ COMPLETE
|
|
||||||
|
|
||||||
- [x] Remove `cw_extractor.py` (moved to `fitter/extractor.py`)
|
|
||||||
- [x] Remove `fitter.py` (moved to `fitter/fit_engine.py`)
|
|
||||||
- [x] Clean up `__init__.py` — only exports theory classes
|
|
||||||
- [x] Add `CurveFitter` general-purpose fitter to `fitter/fit_engine.py` for backward compat
|
|
||||||
- [x] Add `CWExtractor.fit_parametrization()` and `fit_gV_gA()` compatibility methods
|
|
||||||
- [x] Remove old `test_cw_extractor.py` (replaced by `tests/fitter/test_extractor.py` — file was removed)
|
|
||||||
- [x] 322 tests pass, ruff + mypy clean on all 26 source files
|
|
||||||
|
|
||||||
#### Phase 4: Update package structure ✅ COMPLETE
|
|
||||||
|
|
||||||
- [x] `pyproject.toml` auto-discovers all 4 packages via `setuptools.packages.find`
|
|
||||||
- [x] `beta_spectrum.cli` keeps CLI entry point (no re-exports needed)
|
|
||||||
- [x] All imports updated across 26 source files
|
|
||||||
- [x] 322 tests pass
|
|
||||||
- [x] README updated with new architecture diagram and Quick Start examples
|
|
||||||
- [x] Version bumped to 0.4.0
|
|
||||||
- [ ] Decide: restore or delete the `detector_response` module — it was removed from
|
|
||||||
`beta_spectrum/components/` together with its tests, but `fitter/model.py` keeps a
|
|
||||||
duck-typed optional hook and `DEFAULT_JSON_SCHEMA` keeps the detector keys;
|
|
||||||
restore from git history (commits up to `5f99566`) if convolution is needed again
|
|
||||||
|
|
||||||
______________________________________________________________________
|
______________________________________________________________________
|
||||||
|
|
||||||
## Track A: ⁹⁹Tc Spectrum-Shape Analysis
|
## Roadmap: Calculator Capability
|
||||||
|
|
||||||
### A1. Research & Data Collection
|
### C1. Nuclear Data & Input Flexibility
|
||||||
|
|
||||||
- [ ] Parse additional PDF sources on ⁹⁹Tc beta decay and spectrum-shape method
|
- [x] paceENSDF integration: `get_decay_info_from_paceENSDF()`, `decay_info_to_config()`, `create_config_from_source()`, FORBIDDENNESS_MAP, `--nuclide` CLI flag
|
||||||
- Kostensalo & Suhonen, Phys. Rev. C **96**, 024317 (2017) — theoretical spectrum shape predictions
|
- [x] Measured per-branch transition endpoints from ENSDF (max endpoint across branches defines the spectrum endpoint)
|
||||||
- Suhonen, Frontier. Phys. **5**, 55 (2017) — review of g_A quenching
|
- [x] JSON custom input (`load_json_input()`, `json_to_config()`), CLI flags override file values only when explicitly given
|
||||||
- Haaranen et al., Phys. Rev. C **93**, 034308 (2016) — effective coupling constants
|
- [ ] Input sanity checks:
|
||||||
- Additional experimental ⁹⁹Tc spectra (historical data for comparison)
|
- `|Z_parent - Z_daughter|` must equal 1 (beta decay)
|
||||||
- [ ] Research methods to extract g_V from fitted C(W) shape
|
- For `beta_minus`: Z_daughter = Z_parent + 1; for `beta_plus`/`ec`: Z_daughter = Z_parent - 1
|
||||||
- Study the relationship between C(W) parametrization and g_V, g_A effective values
|
- Warn if decay_type contradicts Z values
|
||||||
- Understand how CVC (Conserved Vector Current) constrains g_V
|
- Validate `endpoint_MeV > 0` and `endpoint_MeV > level_energy_keV/1000`
|
||||||
- Review the spectrum-shape method formalism (Behrens-Bühring + shell model)
|
- Cross-check transition_type against ENSDF forbiddenness
|
||||||
- [ ] Document the full analysis pipeline: experimental data → theoretical model → C(W) extraction → g_V extraction
|
|
||||||
|
|
||||||
### A2. Detector Response Function
|
### C2. Multiple Branch Decays
|
||||||
|
|
||||||
- [x] ~~Implement analytical detector response function for 4π semiconductor detector~~
|
- [x] `BranchConfig` dataclass, multi-branch `BetaSpectrum` with per-branch calculators
|
||||||
- ~~Gaussian core with low-energy tail (charge collection effects)~~
|
- [x] Intensity normalization to 100% across branches; per-branch spectra masked beyond their endpoint
|
||||||
- ~~Parameters: energy resolution σ(E), tail fraction, tail shape~~
|
- [x] `--intensity-cutoff` CLI filter
|
||||||
- ~~Energy-dependent resolution σ(E) = a + b·√E~~
|
- [x] Core invariant: phase space × Fermi function included unconditionally (`phase_space` and `fermi` have no toggles)
|
||||||
- ~~Implemented: Gaussian, Gaussian+tail, Tikhonov models with energy-dependent resolution and Fano factor~~
|
- [x] Physical branch spectra and universal factor in CSV export; `--separate-branches` normalization mode
|
||||||
- [x] ~~Implement convolution routines~~
|
|
||||||
- ~~Convolve theoretical spectrum with detector response for comparison to data~~
|
|
||||||
- ~~Implemented: convolve() with tabulated response support~~
|
|
||||||
- [ ] Monte Carlo simulation (GEANT4) — out of scope for this project but noted for future
|
|
||||||
- Full detector geometry simulation
|
|
||||||
- Energy loss in source, dead layers, absorber
|
|
||||||
- Response matrix for unfolding (as done in Paulsen et al. with EGSNRC)
|
|
||||||
|
|
||||||
### A3. Data Processing & Analysis
|
### C3. Recoil and Weak Magnetism
|
||||||
|
|
||||||
- [ ] Data processing pipeline for 1-hour run spectra
|
Theory documented in `docs/corrections/06-recoil.md`; implementation pending.
|
||||||
- Background subtraction
|
|
||||||
- Energy calibration (using known X-ray/gamma lines)
|
|
||||||
- Dead-time correction
|
|
||||||
- Pulse pile-up correction
|
|
||||||
- [ ] Endpoint fitting and energy calibration
|
|
||||||
- Kurie plot analysis for endpoint determination
|
|
||||||
- Polynomial calibration using known spectral lines
|
|
||||||
- Linearity correction across energy range
|
|
||||||
- [ ] Data quality assessment procedures
|
|
||||||
- Residual analysis (data vs model)
|
|
||||||
- χ²/ndf evaluation
|
|
||||||
- Consistency checks across multiple runs
|
|
||||||
- Statistical combination of long-exposure spectra
|
|
||||||
|
|
||||||
### A4. Fitter for C(W) Extraction
|
- [ ] Implement recoil correction (R_N, Q terms, weak magnetism) as a `SpectrumComponent`
|
||||||
|
|
||||||
- [x] ~~Implement fitter routine to extract experimental C(W) from data~~
|
### C4. Beta+ / Electron Capture Support
|
||||||
- ~~χ² minimization: theoretical spectrum (with all corrections) vs measured spectrum~~
|
|
||||||
- ~~Free parameters: C(W) shape parameters, endpoint energy, normalization, background~~
|
|
||||||
- ~~Covariance matrix and uncertainty propagation~~
|
|
||||||
- ~~Implemented: `CurveFitter` with `least_squares` optimization, `FitResult` with covariance, chi2, residuals~~
|
|
||||||
- [x] ~~Parametrize C(W) in terms of g_V (and g_A)~~
|
|
||||||
- ~~Fit C(W) data to theoretical parametrization~~
|
|
||||||
- ~~Extract g_V^eff and g_A^eff from the fit~~
|
|
||||||
- ~~Compare with Paulsen et al. results: g_A^eff = 0.574(36), g_V^eff = 0.376(5)~~
|
|
||||||
- ~~Implemented: `CWExtractor` with Kurie plot analysis, parametrized fitting, g_V/g_A extraction~~
|
|
||||||
- [ ] Systematic uncertainty analysis
|
|
||||||
- Vary correction implementations within uncertainties
|
|
||||||
- Test sensitivity to detector response model
|
|
||||||
- Background model dependence
|
|
||||||
|
|
||||||
______________________________________________________________________
|
- [ ] Fermi function sign change for positrons
|
||||||
|
- [ ] Radiative corrections for β⁺ (Sirlin corrections differ for e⁻ vs e⁺)
|
||||||
|
- [ ] Screening correction for positrons (different atomic interaction)
|
||||||
|
- [ ] Exchange correction (negligible for β⁺ but check)
|
||||||
|
|
||||||
## Track B: Package Usability Improvements
|
### C5. Neutrino Spectrum Support
|
||||||
|
|
||||||
### B1. Nuclear Data Integration
|
- [ ] Convenience analyser methods for plotting neutrino spectra (convert electron spectrum, plot both scaled)
|
||||||
|
- [ ] Neutrino radiative correction h(W̃, W0) per Sirlin 2011; conversion f_ν(W_ν) = f_e(W̃) · [1 + (α/2π)(h − g)]
|
||||||
|
|
||||||
- [x] ~~paceENSDF integration for automated nuclear data retrieval~~
|
### C6. Neutrino Mass Effects
|
||||||
- ~~`get_decay_info_from_paceENSDF(nuclide, decay_type)` — retrieves Q-value, half-life, spin/parity, branches, forbiddenness from ENSDF~~
|
|
||||||
- ~~`decay_info_to_config()` — converts DecayInfo to SpectrumConfig with branch support~~
|
|
||||||
- ~~`create_config_from_source("paceENSDF", nuclide="Tc99")` — one-liner config creation~~
|
|
||||||
- ~~`_parse_nuclide_symbol()` — parses "Tc99" → (Tc, 43, 99) with full Z lookup for Z=1..98~~
|
|
||||||
- ~~`_resolve_decay_index()` — finds correct decay index (ground state by default)~~
|
|
||||||
- ~~`_get_decay_mode_symbol()` — maps beta_minus→BM, beta_plus/ec→ECBP~~
|
|
||||||
- ~~FORBIDDENNESS_MAP — translates paceENSDF codes (0A, 1F, 2F, …) to transition_type~~
|
|
||||||
- ~~CLI `--nuclide Tc99` uses paceENSDF automatically~~
|
|
||||||
- ~~Branch intensities, log_ft, level energies, transition types all populated from ENSDF~~
|
|
||||||
- ~~Tested with Tc99 (2 branches, F2) and Co60 (multi-branch)~~
|
|
||||||
|
|
||||||
### B2. Input Flexibility
|
Implemented: `neutrino_mass_MeV` config field, modified phase space factor √(1 − m_ν²/(W₀−W)²), `--neutrino-mass` CLI option.
|
||||||
|
|
||||||
- [x] ~~Generalize parameter inputs via custom input file (YAML/JSON)~~
|
- [ ] Document which corrections need m_ν as an additional input parameter
|
||||||
- ~~Declare isotope, transition type, detector parameters~~
|
- [ ] Study impact on radiative corrections (mass singularities)
|
||||||
- ~~Toggle corrections on/off per-component~~
|
|
||||||
- ~~Reproducible calculation configurations~~
|
|
||||||
- ~~Implemented: `load_json_input()`, `json_to_config()`, `DEFAULT_JSON_SCHEMA` with full detector param support, CLI `--input` flag~~
|
|
||||||
- [x] ~~Retrieve parameters directly from ENSDF database~~
|
|
||||||
- ~~Auto-populate W0, Z, A, transition type from isotope name~~
|
|
||||||
- ~~Reduce manual configuration errors~~
|
|
||||||
- ~~Implemented: `get_decay_info_from_paceENSDF()` returns DecayInfo with Z_parent, Z_daughter, A_number, endpoint_MeV, transition_type, forbiddenness_code, half_life, branches; CLI `--nuclide` uses this automatically~~
|
|
||||||
|
|
||||||
### B3. Multiple Branch Decays
|
### C7. Automated Report Generation
|
||||||
|
|
||||||
- [x] ~~Add support for multiple branch decays~~
|
- [ ] Procedurally generated PDF report for a calculation:
|
||||||
- ~~Calculate total spectral shape weighted by branch intensities~~
|
- Parameter table (nuclide, decay data, enabled corrections, version, commit)
|
||||||
- ~~Handle decays with significant branching to excited states~~
|
|
||||||
- ~~Sum contributions from all branches with proper weighting~~
|
|
||||||
- ~~Implemented: `BranchConfig` dataclass, multi-branch `BetaSpectrum` with per-branch calculators~~
|
|
||||||
- ~~Branch data from paceENSDF: intensity, endpoint (Q-value - excitation energy), transition type~~
|
|
||||||
- ~~Intensity normalization to 100% across all branches~~
|
|
||||||
- ~~CSV export: total spectrum, per-branch spectra, per-branch components (PhaseSpace, Radiative), universal components (Fermi, Screening, etc.)~~
|
|
||||||
- ~~Plot: total spectrum + individual branches with legend (transition type, endpoint energy)~~
|
|
||||||
- ~~Debug plot: vertical layout with per-branch panels, universal components panel~~
|
|
||||||
- ~~Energy grid extends to max branch endpoint, per-branch spectra masked beyond their endpoint~~
|
|
||||||
- ~~Single-branch = trivial case of multi-branch (no separate mode)~~
|
|
||||||
- ~~Per-branch transition types extracted from ENSDF (not defaulting to 'A')~~
|
|
||||||
- ~~`--intensity-cutoff` CLI option to filter out negligible branches~~
|
|
||||||
- ~~Branch spectra plotted as raw (un-normalized) contributions so total = sum of branches~~
|
|
||||||
|
|
||||||
### B4. Beta+ Decay Support
|
|
||||||
|
|
||||||
- [ ] Modify corrections to support β⁺ decays
|
|
||||||
- Fermi function sign change for positrons
|
|
||||||
- Radiative corrections for β⁺ (Sirlin corrections differ for e⁻ vs e⁺)
|
|
||||||
- Screening correction for positrons (different atomic interaction)
|
|
||||||
- Exchange correction (negligible for β⁺ but check)
|
|
||||||
|
|
||||||
### B5. Neutrino Spectrum Support
|
|
||||||
|
|
||||||
- [ ] Add convenience analyser methods for plotting neutrino spectra
|
|
||||||
- Convert electron spectrum to neutrino spectrum
|
|
||||||
- Plot both on same figure with proper scaling
|
|
||||||
- [ ] Adopt corrections that differ for neutrino spectrum
|
|
||||||
- Implement neutrino radiative correction h(W̃, W0) per Sirlin 2011
|
|
||||||
- Conversion formula: f_ν(W_ν) = f_e(W̃) · [1 + (α/2π)(h − g)]
|
|
||||||
- Neutrino-specific corrections (no Fermi function, different radiative terms)
|
|
||||||
|
|
||||||
### B6. Neutrino Mass Effects
|
|
||||||
|
|
||||||
- [ ] Implement option to produce spectra with non-zero neutrino mass
|
|
||||||
- Modify phase space: (W0 − W)² → (W0 − W)² · √(1 − m_ν²/(W0−W)²)
|
|
||||||
- Add kink feature near endpoint for spectral distortion
|
|
||||||
- Useful for neutrino mass search applications
|
|
||||||
- [ ] Research and document implications of significant neutrino mass
|
|
||||||
- Which corrections need m_ν as input parameter?
|
|
||||||
- Impact on radiative corrections (mass singularities)
|
|
||||||
- Impact on endpoint fitting procedures
|
|
||||||
|
|
||||||
### B7. Automated Report Generation
|
|
||||||
|
|
||||||
- [ ] Implement extensive procedurally generated PDF report
|
|
||||||
- Comprehensive details of the calculation
|
|
||||||
- Component plots (phase space, Fermi, each correction factor)
|
- Component plots (phase space, Fermi, each correction factor)
|
||||||
- Total spectrum with all corrections applied
|
- Total spectrum with all corrections applied
|
||||||
- Residual analysis, fit quality metrics
|
|
||||||
- Parameter table with uncertainties
|
|
||||||
- References to theoretical sources
|
- References to theoretical sources
|
||||||
- [ ] Extend report generation to fitters
|
|
||||||
- Fit results with covariance matrix
|
|
||||||
- Parameter correlations
|
|
||||||
- Confidence intervals
|
|
||||||
- Goodness-of-fit statistics
|
|
||||||
|
|
||||||
### B8. CLI & Output Improvements
|
### C8. CLI & Output Improvements
|
||||||
|
|
||||||
- [x] Implement structured logging system
|
- [ ] Auto-deduce `transition_type` from nuclear data (remove as explicit parameter where possible)
|
||||||
- Log to stdout with configurable verbosity levels (-v=INFO, -vv=DEBUG)
|
- [ ] Auto-deduce `decay_type` from Z_parent vs Z_daughter difference
|
||||||
- Visualize workflow steps: data loading, parameter resolution, calculation progress
|
- [x] Structured logging (`-v`, `-vv`, `-q`, `--log-file`)
|
||||||
- Log all used parameters: Z_parent, Z_daughter, A_number, endpoint, transition type, enabled corrections
|
- [x] CSV metadata headers (ISO timestamp, nuclide, decay, calculation params, version, commit hash)
|
||||||
- Optional log file output with timestamped filename
|
- [x] `--dry-run`, `--version`, single-letter aliases for every long option
|
||||||
- Integrate Python `logging` module with custom formatter
|
- [x] Plots built from the exported CSV; commit hash + UTC timestamp stamps; normal/debug display modes
|
||||||
- [x] Add metadata header to CSV output files
|
|
||||||
- Date and time of calculation (ISO 8601 format)
|
|
||||||
- Nuclide information: parent/daughter symbol, Z, A
|
|
||||||
- Decay/transition info: endpoint energy, transition type, forbiddenness
|
|
||||||
- Calculation parameters: energy step, enabled corrections, detector settings
|
|
||||||
- Software version and git commit hash (7 chars)
|
|
||||||
- Format: YAML-style comments at top of CSV file
|
|
||||||
- [ ] Optimize CLI argument design and add sanity checks
|
|
||||||
- Auto-deduce `transition_type` from `decay_type` and nuclear data — remove as explicit CLI parameter
|
|
||||||
- Auto-deduce `decay_type` from Z_parent vs Z_daughter difference — remove as explicit CLI parameter
|
|
||||||
- Add sanity checks for custom input:
|
|
||||||
- `|Z_parent - Z_daughter|` must equal 1 (beta decay)
|
|
||||||
- For `beta_minus`: Z_daughter must be Z_parent + 1
|
|
||||||
- For `beta_plus`/`ec`: Z_daughter must be Z_parent - 1
|
|
||||||
- Warn if decay_type contradicts Z values (e.g., beta+ with Z_daughter > Z_parent)
|
|
||||||
- Validate endpoint_MeV > 0 and endpoint_MeV > level_energy_keV/1000
|
|
||||||
- Cross-check transition_type against ENSDF forbiddenness (if paceENSDF source)
|
|
||||||
- [x] Add `--dry-run` option to validate input and display resolved parameters without calculation
|
|
||||||
- [x] Add `--version` flag
|
|
||||||
- [x] Add `-q/--quiet` flag to suppress terminal output
|
|
||||||
- [x] Add `./output/` directory for debug verification artifacts (gitignored except `.gitkeep`)
|
|
||||||
- [x] Add debug verification step to development workflow: `bs_pnpi -vv` with parameter consistency checks across all components, documented in DEVELOPMENT.md §4.5
|
|
||||||
- [x] Add plot output with ID tracking (commit hash + UTC timestamp) and two display modes:
|
|
||||||
- Normal mode (default): single spectrum plot with nuclear data header
|
|
||||||
- Debug mode (-vv): 4-panel view with all correction factors and deviations
|
|
||||||
|
|
||||||
______________________________________________________________________
|
______________________________________________________________________
|
||||||
|
|
||||||
@@ -304,58 +121,46 @@ ______________________________________________________________________
|
|||||||
|
|
||||||
### Additional Corrections
|
### Additional Corrections
|
||||||
|
|
||||||
Not implementing at this time. Additional corrections (weak magnetism, recoil kinematics, etc.) require a complex implementation with project-wide consequences. This is a far-future plan, irrelevant to the current ⁹⁹Tc analysis goal where the shape factor C(W) absorbs all unmodelled energy-dependent effects.
|
Additional corrections beyond the implemented set require complex implementation with project-wide consequences; deferred as far-future work.
|
||||||
|
|
||||||
### Nuclear Shape Factors C(Z,W)
|
### Theoretical Shape Factors C(Z,W)
|
||||||
|
|
||||||
Not implementing theoretical shape factor calculations. The entire point of this project is to **extract** C(W) from experimental data, not to compute it from nuclear theory (shell model, QRPA, etc.). Theoretical shape factor calculations require:
|
Never computed here. Shell-model / QRPA transition densities and effective interactions belong to nuclear-structure codes; this repository treats C(W) as an input parameter of the spectrum model. The dedicated research into nuclear shape factors will be redone in separate projects.
|
||||||
|
|
||||||
- Nuclear structure models (NUSHELLX, shell model codes)
|
|
||||||
- One-body transition densities (OBTDs)
|
|
||||||
- Coulomb displacement energy calculations
|
|
||||||
- Multiple effective interactions
|
|
||||||
|
|
||||||
These are handled by the nuclear physics community. Our role is to provide the analysis framework that extracts C(W) from data, then compare the extracted shape to theoretical predictions.
|
|
||||||
|
|
||||||
______________________________________________________________________
|
______________________________________________________________________
|
||||||
|
|
||||||
## Current Status
|
## Current Status
|
||||||
|
|
||||||
**Version:** 0.3.1 → **In progress: 0.4.0 — Package restructuring**
|
**Version:** v0.5.0 — theory-only scope reduction. Calculation logic unchanged from v0.4.1.
|
||||||
|
|
||||||
### Completed (v0.3.x)
|
### Completed (v0.3.x — theory core)
|
||||||
|
|
||||||
- All theoretical corrections: phase space, Fermi function, finite size, screening, exchange, radiative (with delta_cut resummation)
|
- All implemented theoretical corrections: phase space, Fermi function, finite size, screening, exchange, radiative (with delta_cut resummation)
|
||||||
- Detector response module with analytical models (Gaussian, Gaussian+tail, Tikhonov), convolution API
|
|
||||||
- χ² curve fitting framework (CurveFitter) with confidence intervals, profile likelihood, correlation analysis
|
|
||||||
- C(W) shape factor extraction pipeline (CWExtractor) with Kurie plot analysis, parametrized fitting, g_V/g_A extraction
|
|
||||||
- Multi-branch decay support (unified single/multi-branch)
|
- Multi-branch decay support (unified single/multi-branch)
|
||||||
- CLI interface (`bs_pnpi`) with paceENSDF integration (`--nuclide`), structured logging (-v/-vv/-q), --dry-run, --version, --log-file
|
- CLI (`bs_pnpi`) with paceENSDF integration, structured logging, `--dry-run`, `--version`, `--log-file`
|
||||||
- CSV metadata headers with branch info
|
- CSV metadata headers with branch info
|
||||||
- JSON input with full detector param support
|
- paceENSDF `DecayInfo` retrieval (Z, A, endpoint, transition type, half-life, spin/parity, branches)
|
||||||
- paceENSDF integration: `get_decay_info_from_paceENSDF()` returns DecayInfo with Z, A, endpoint, transition type, half-life, spin/parity, branches
|
|
||||||
- Comprehensive test suite (262 tests, including 6 paceENSDF integration tests)
|
|
||||||
- Notebook quality control with nbmake and auto-save plot hooks
|
- Notebook quality control with nbmake and auto-save plot hooks
|
||||||
- Plot output: unified multi-branch approach, externalized headers, compact legends, consistent titles
|
- Plot output: unified multi-branch approach, externalized headers, consistent titles
|
||||||
|
|
||||||
### Completed (v0.4.0)
|
### Completed (v0.4.0 — package split, later obsolete)
|
||||||
|
|
||||||
- **Track B: Package restructuring** — separating into `beta_spectrum` (theory), `exp_data` (experimental data), `fitter` (analysis framework)
|
- Interim split into `beta_spectrum` / `exp_data` / `fitter` packages
|
||||||
- [x] Phase 1: Create `exp_data` module (raw_data.py, calibration.py, fitters.py, corrections.py, spectrum.py) — 43 tests
|
- Superseded by the v0.5.0 scope reduction: the analysis packages were removed entirely
|
||||||
- [x] Phase 2: Create `fitter` module (model.py, fit_engine.py, extractor.py, result.py) — 34 tests
|
|
||||||
- [x] Phase 3: Clean up `beta_spectrum` (remove cw_extractor.py, fitter.py, **init**.py cleanup) — 322 tests
|
### Completed (v0.4.1 — normalization & CLI)
|
||||||
- [x] Phase 4: Package auto-discovered by setuptools, all imports updated, black formatting applied
|
|
||||||
- **v0.4.0 is COMPLETE**
|
- Core spectrum made unconditional (`phase_space × fermi`): core toggles removed, physical branch spectra in CSV export, plots built from the exported CSV
|
||||||
- 322 tests pass (262 original + 60 new)
|
- Normalization audit fixes: lone surviving branch defines the spectrum, CLI options propagate through the JSON flow, positive-integral guard for both export modes, paceENSDF per-branch measured endpoints (Co60 fixed), single delegated paceENSDF config builder
|
||||||
- ruff + mypy clean on all 26 source files
|
- CLI surface refresh: `--separate-branches` rename, single-letter aliases, dry-run cleanup, stale detector_* JSON schema keys removed
|
||||||
- black formatting applied to all new code
|
|
||||||
- `pyproject.toml` auto-discovers all 4 packages: `beta_spectrum`, `beta_spectrum.output`, `exp_data`, `fitter`
|
### Completed (v0.5.0 — scope reduction)
|
||||||
- README updated with new architecture diagram and Quick Start examples
|
|
||||||
- Version bumped to 0.4.0
|
- Removal of `exp_data/`, `fitter/`, raw detector data, analysis notebooks/scripts
|
||||||
|
- Documentation rewrite to theory-only scope
|
||||||
|
- 280 tests pass (theory-only suite), ruff + mypy clean
|
||||||
|
|
||||||
### Next Immediate Step
|
### Next Immediate Step
|
||||||
|
|
||||||
- **Track A: ⁹⁹Tc spectrum-shape analysis** — start real data processing pipeline
|
- C1 input sanity checks (decay/Z consistency, endpoint validation)
|
||||||
- Data processing pipeline for experimental runs
|
- C3 recoil & weak magnetism correction implementation
|
||||||
- Endpoint fitting and energy calibration
|
|
||||||
- Systematic uncertainty analysis
|
|
||||||
|
|||||||
@@ -52,4 +52,4 @@ __all__ = [
|
|||||||
"DEFAULT_JSON_SCHEMA",
|
"DEFAULT_JSON_SCHEMA",
|
||||||
]
|
]
|
||||||
|
|
||||||
__version__ = "0.4.0"
|
__version__ = "0.5.0"
|
||||||
|
|||||||
+69
-73
@@ -65,12 +65,14 @@ def _build_parser() -> argparse.ArgumentParser:
|
|||||||
# Source selection flags (backward compatible, mutually exclusive)
|
# Source selection flags (backward compatible, mutually exclusive)
|
||||||
source_group = parser.add_mutually_exclusive_group(required=False)
|
source_group = parser.add_mutually_exclusive_group(required=False)
|
||||||
source_group.add_argument(
|
source_group.add_argument(
|
||||||
|
"-N",
|
||||||
"--nuclide",
|
"--nuclide",
|
||||||
type=str,
|
type=str,
|
||||||
help="Nuclide symbol for paceENSDF lookup (e.g. 'Tc99', 'Co60'). "
|
help="Nuclide symbol for paceENSDF lookup (e.g. 'Tc99', 'Co60'). "
|
||||||
"Retrieves decay parameters from the ENSDF database via paceENSDF.",
|
"Retrieves decay parameters from the ENSDF database via paceENSDF.",
|
||||||
)
|
)
|
||||||
source_group.add_argument(
|
source_group.add_argument(
|
||||||
|
"-i",
|
||||||
"--input",
|
"--input",
|
||||||
type=str,
|
type=str,
|
||||||
metavar="FILE",
|
metavar="FILE",
|
||||||
@@ -79,20 +81,27 @@ def _build_parser() -> argparse.ArgumentParser:
|
|||||||
|
|
||||||
# Common parameters
|
# Common parameters
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
"-s",
|
||||||
"--e-step",
|
"--e-step",
|
||||||
type=float,
|
type=float,
|
||||||
default=0.001,
|
default=None,
|
||||||
help="Energy step size in MeV (default: 0.001). All energies are in MeV throughout the toolkit.",
|
help="Energy step size in MeV (default: 0.001 for nuclide input; "
|
||||||
|
"for JSON input the file's e_step_MeV is used when the flag is "
|
||||||
|
"omitted). All energies are in MeV throughout the toolkit.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
"-m",
|
||||||
"--neutrino-mass",
|
"--neutrino-mass",
|
||||||
type=float,
|
type=float,
|
||||||
default=0.0,
|
default=None,
|
||||||
help="Neutrino mass in MeV (default: 0.0). Set to non-zero value to include neutrino mass effects in phase space calculation.",
|
help="Neutrino mass in MeV (default: 0.0, or the JSON file's "
|
||||||
|
"neutrino_mass_MeV when omitted). Set to non-zero value to include "
|
||||||
|
"neutrino mass effects in phase space calculation.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Output
|
# Output
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
"-o",
|
||||||
"--output",
|
"--output",
|
||||||
type=str,
|
type=str,
|
||||||
metavar="FILE",
|
metavar="FILE",
|
||||||
@@ -101,6 +110,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|||||||
|
|
||||||
# Plotting
|
# Plotting
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
"-p",
|
||||||
"--plot",
|
"--plot",
|
||||||
type=str,
|
type=str,
|
||||||
metavar="FILE",
|
metavar="FILE",
|
||||||
@@ -124,6 +134,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|||||||
help="Suppress all terminal output (only errors shown).",
|
help="Suppress all terminal output (only errors shown).",
|
||||||
)
|
)
|
||||||
log_group.add_argument(
|
log_group.add_argument(
|
||||||
|
"-L",
|
||||||
"--log-file",
|
"--log-file",
|
||||||
type=str,
|
type=str,
|
||||||
metavar="PATH",
|
metavar="PATH",
|
||||||
@@ -131,20 +142,23 @@ def _build_parser() -> argparse.ArgumentParser:
|
|||||||
"Log file is always opened in append mode at DEBUG level.",
|
"Log file is always opened in append mode at DEBUG level.",
|
||||||
)
|
)
|
||||||
log_group.add_argument(
|
log_group.add_argument(
|
||||||
|
"-d",
|
||||||
"--dry-run",
|
"--dry-run",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Validate input and display resolved configuration without computing spectrum.",
|
help="Validate input and display resolved configuration without computing spectrum.",
|
||||||
)
|
)
|
||||||
log_group.add_argument(
|
log_group.add_argument(
|
||||||
|
"-c",
|
||||||
"--intensity-cutoff",
|
"--intensity-cutoff",
|
||||||
type=float,
|
type=float,
|
||||||
default=0.0,
|
default=None,
|
||||||
metavar="FRAC",
|
metavar="FRAC",
|
||||||
help="Minimum branch intensity fraction to include in the spectrum "
|
help="Minimum branch intensity fraction to include in the spectrum "
|
||||||
"(default: 0.0 = include all branches). "
|
"(default: 0.0 = include all branches). "
|
||||||
"Branches below this threshold are excluded from the calculation and plots.",
|
"Branches below this threshold are excluded from the calculation and plots.",
|
||||||
)
|
)
|
||||||
log_group.add_argument(
|
log_group.add_argument(
|
||||||
|
"-V",
|
||||||
"--csv-verbosity",
|
"--csv-verbosity",
|
||||||
type=str,
|
type=str,
|
||||||
default="full",
|
default="full",
|
||||||
@@ -152,15 +166,20 @@ def _build_parser() -> argparse.ArgumentParser:
|
|||||||
help="CSV output verbosity: 'full' (all components) or 'concise' (total spectrum and normalized branch components only) (default: full)",
|
help="CSV output verbosity: 'full' (all components) or 'concise' (total spectrum and normalized branch components only) (default: full)",
|
||||||
)
|
)
|
||||||
log_group.add_argument(
|
log_group.add_argument(
|
||||||
|
"-S",
|
||||||
"--csv-separator",
|
"--csv-separator",
|
||||||
type=str,
|
type=str,
|
||||||
default=",",
|
default=None,
|
||||||
help="CSV column separator (default: ','). Use 'tab' for tab separation.",
|
help="CSV column separator (default: ','). Use 'tab' for tab separation.",
|
||||||
)
|
)
|
||||||
log_group.add_argument(
|
log_group.add_argument(
|
||||||
"--normalize-to-one",
|
"-b",
|
||||||
|
"--separate-branches",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Normalize each branch to 1, ignoring intensity. In this case, total spectrum will not be produced (only energy and branch-wise spectra).",
|
default=None,
|
||||||
|
help="Emit separate branch spectra, each normalized to integrate to "
|
||||||
|
"1 (intensities ignored; no total_spectrum column). Default: total "
|
||||||
|
"spectrum with intensity-normalized branch columns.",
|
||||||
)
|
)
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
@@ -173,15 +192,10 @@ def _run(args: argparse.Namespace) -> None:
|
|||||||
from beta_spectrum.logging_utils import get_logger
|
from beta_spectrum.logging_utils import get_logger
|
||||||
from beta_spectrum.nuclear_data import (
|
from beta_spectrum.nuclear_data import (
|
||||||
create_config_from_source,
|
create_config_from_source,
|
||||||
get_decay_info_from_paceENSDF,
|
|
||||||
json_to_config,
|
json_to_config,
|
||||||
load_json_input,
|
load_json_input,
|
||||||
)
|
)
|
||||||
from beta_spectrum.spectrum import (
|
from beta_spectrum.spectrum import BetaSpectrum
|
||||||
BetaSpectrum,
|
|
||||||
BranchConfig,
|
|
||||||
SpectrumConfig,
|
|
||||||
)
|
|
||||||
from beta_spectrum.output import BetaSpectrumAnalyzer
|
from beta_spectrum.output import BetaSpectrumAnalyzer
|
||||||
|
|
||||||
# Determine logging level
|
# Determine logging level
|
||||||
@@ -230,66 +244,45 @@ def _run(args: argparse.Namespace) -> None:
|
|||||||
else:
|
else:
|
||||||
source_type = "paceENSDF"
|
source_type = "paceENSDF"
|
||||||
|
|
||||||
# Build config
|
# Resolve optional CLI flags: None means "not provided". For paceENSDF
|
||||||
|
# they fall back to literal defaults; for JSON they let the file's own
|
||||||
|
# values through unless explicitly overridden.
|
||||||
|
e_step_MeV = 0.001 if args.e_step is None else float(args.e_step)
|
||||||
|
neutrino_mass_MeV = 0.0 if args.neutrino_mass is None else float(args.neutrino_mass)
|
||||||
|
intensity_cutoff = (
|
||||||
|
0.0 if args.intensity_cutoff is None else float(args.intensity_cutoff)
|
||||||
|
)
|
||||||
|
csv_separator = "," if args.csv_separator is None else args.csv_separator
|
||||||
|
separate_branches = bool(args.separate_branches)
|
||||||
|
|
||||||
|
# Build config — a single delegated path for every data source.
|
||||||
config: SpectrumConfig
|
config: SpectrumConfig
|
||||||
if source_type == "paceENSDF":
|
if source_type == "paceENSDF":
|
||||||
logger.info("Source: paceENSDF nuclide=%s", source_value)
|
logger.info("Source: paceENSDF nuclide=%s", source_value)
|
||||||
decay_info = get_decay_info_from_paceENSDF(source_value, "beta_minus")
|
config = create_config_from_source(
|
||||||
|
"paceENSDF",
|
||||||
# Normalize branch intensities to sum to 1.0
|
nuclide=source_value,
|
||||||
if decay_info.branches:
|
e_step_MeV=e_step_MeV,
|
||||||
total_intensity = sum(b.intensity for b in decay_info.branches)
|
intensity_cutoff=intensity_cutoff,
|
||||||
if total_intensity > 0:
|
neutrino_mass_MeV=neutrino_mass_MeV,
|
||||||
branches = [
|
csv_separator=csv_separator,
|
||||||
BranchConfig(
|
separate_branches=separate_branches,
|
||||||
endpoint_MeV=(
|
)
|
||||||
(decay_info.endpoint_MeV - b.level_energy_keV / 1000.0)
|
|
||||||
if b.level_energy_keV > 0
|
|
||||||
else decay_info.endpoint_MeV
|
|
||||||
),
|
|
||||||
transition_type=b.transition_type,
|
|
||||||
intensity=b.intensity / total_intensity,
|
|
||||||
)
|
|
||||||
for b in decay_info.branches
|
|
||||||
if b.intensity / total_intensity >= args.intensity_cutoff
|
|
||||||
]
|
|
||||||
config = SpectrumConfig(
|
|
||||||
Z_parent=decay_info.Z_parent,
|
|
||||||
Z_daughter=decay_info.Z_daughter,
|
|
||||||
A_number=decay_info.A_number,
|
|
||||||
endpoint_MeV=decay_info.endpoint_MeV,
|
|
||||||
transition_type=decay_info.transition_type,
|
|
||||||
e_step_MeV=args.e_step,
|
|
||||||
neutrino_mass_MeV=args.neutrino_mass,
|
|
||||||
branches=branches,
|
|
||||||
intensity_cutoff=args.intensity_cutoff,
|
|
||||||
csv_separator=args.csv_separator,
|
|
||||||
normalize_to_one=args.normalize_to_one,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
config = create_config_from_source(
|
|
||||||
"paceENSDF",
|
|
||||||
nuclide=source_value,
|
|
||||||
e_step_MeV=args.e_step,
|
|
||||||
intensity_cutoff=args.intensity_cutoff,
|
|
||||||
neutrino_mass_MeV=args.neutrino_mass,
|
|
||||||
csv_separator=args.csv_separator,
|
|
||||||
normalize_to_one=args.normalize_to_one,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
config = create_config_from_source(
|
|
||||||
"paceENSDF",
|
|
||||||
nuclide=source_value,
|
|
||||||
e_step_MeV=args.e_step,
|
|
||||||
intensity_cutoff=args.intensity_cutoff,
|
|
||||||
neutrino_mass_MeV=args.neutrino_mass,
|
|
||||||
csv_separator=args.csv_separator,
|
|
||||||
normalize_to_one=args.normalize_to_one,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
logger.info("Source: JSON file=%s", source_value)
|
logger.info("Source: JSON file=%s", source_value)
|
||||||
data = load_json_input(source_value)
|
data = load_json_input(source_value)
|
||||||
config = json_to_config(data)
|
config = json_to_config(data)
|
||||||
|
# CLI flags override the JSON file values only when explicitly
|
||||||
|
# provided; unset flags keep the file's own values.
|
||||||
|
if args.e_step is not None:
|
||||||
|
config.e_step_MeV = e_step_MeV
|
||||||
|
if args.neutrino_mass is not None:
|
||||||
|
config.neutrino_mass_MeV = neutrino_mass_MeV
|
||||||
|
if args.intensity_cutoff is not None:
|
||||||
|
config.intensity_cutoff = intensity_cutoff
|
||||||
|
if args.csv_separator is not None:
|
||||||
|
config.csv_separator = csv_separator
|
||||||
|
config.separate_branches = separate_branches
|
||||||
|
|
||||||
# Dry run: display resolved config and exit
|
# Dry run: display resolved config and exit
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
@@ -373,7 +366,6 @@ def _print_dry_run_output(config, source_type: str) -> None:
|
|||||||
f"Nuclide: {_decay_notation(config.Z_parent, config.Z_daughter, config.A_number)}"
|
f"Nuclide: {_decay_notation(config.Z_parent, config.Z_daughter, config.A_number)}"
|
||||||
)
|
)
|
||||||
print(f"Endpoint: {config.endpoint_MeV * 1000:.1f} keV")
|
print(f"Endpoint: {config.endpoint_MeV * 1000:.1f} keV")
|
||||||
print(f"Transition: {config.transition_type}")
|
|
||||||
print(f"e_step: {config.e_step_MeV:.4f} MeV")
|
print(f"e_step: {config.e_step_MeV:.4f} MeV")
|
||||||
print(f"neutrino_mass_MeV: {config.neutrino_mass_MeV:.6f}")
|
print(f"neutrino_mass_MeV: {config.neutrino_mass_MeV:.6f}")
|
||||||
print(f"Corrections: {', '.join(enabled) if enabled else 'none'}")
|
print(f"Corrections: {', '.join(enabled) if enabled else 'none'}")
|
||||||
@@ -393,8 +385,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("========================================")
|
||||||
|
|
||||||
|
|
||||||
@@ -516,10 +506,16 @@ def _decay_notation(Z_parent: int, Z_daughter: int, A: int) -> str:
|
|||||||
return f"{_nuclide_symbol(Z_parent, A)} -> {_nuclide_symbol(Z_daughter, A)}"
|
return f"{_nuclide_symbol(Z_parent, A)} -> {_nuclide_symbol(Z_daughter, A)}"
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main(argv: list[str] | None = None) -> None:
|
||||||
"""Main entry point for the CLI."""
|
"""Main entry point for the CLI.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
argv : list[str], optional
|
||||||
|
Explicit argument vector (for tests/embedding); None reads sys.argv.
|
||||||
|
"""
|
||||||
parser = _build_parser()
|
parser = _build_parser()
|
||||||
args = parser.parse_args()
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_run(args)
|
_run(args)
|
||||||
|
|||||||
@@ -96,6 +96,10 @@ class BranchInfo:
|
|||||||
log(ft) value for this branch.
|
log(ft) value for this branch.
|
||||||
transition_type : str
|
transition_type : str
|
||||||
Forbiddenness classification for this branch (A, F1, F1U, F2, ...).
|
Forbiddenness classification for this branch (A, F1, F1U, F2, ...).
|
||||||
|
endpoint_MeV : float
|
||||||
|
Measured beta endpoint energy of this transition in MeV (ENSDF
|
||||||
|
column 7). 0.0 means "not provided": consumers fall back to
|
||||||
|
base-endpoint minus level-energy subtraction.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
level_index: int
|
level_index: int
|
||||||
@@ -103,6 +107,7 @@ class BranchInfo:
|
|||||||
intensity: float
|
intensity: float
|
||||||
log_ft: Optional[float]
|
log_ft: Optional[float]
|
||||||
transition_type: str = "A"
|
transition_type: str = "A"
|
||||||
|
endpoint_MeV: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
def _parse_nuclide_symbol(symbol: str) -> Tuple[str, int, int]:
|
def _parse_nuclide_symbol(symbol: str) -> Tuple[str, int, int]:
|
||||||
@@ -384,24 +389,19 @@ def get_decay_info_from_paceENSDF(
|
|||||||
|
|
||||||
# Extract Q-value from ground state branch (state[7] = endpoint energy in keV)
|
# Extract Q-value from ground state branch (state[7] = endpoint energy in keV)
|
||||||
# and half-life from halflife_data
|
# and half-life from halflife_data
|
||||||
endpoint_keV = 0.0
|
|
||||||
half_life_str = "unknown"
|
half_life_str = "unknown"
|
||||||
|
|
||||||
|
# The spectrum endpoint is the maximum measured transition endpoint
|
||||||
|
# across all branches (ENSDF column 7, keV). Selecting the ground
|
||||||
|
# state branch or the first branch is wrong whenever those branches
|
||||||
|
# are absent or non-maximal (e.g. Co60, whose first listed branch
|
||||||
|
# endpoint is below higher-level transition endpoints).
|
||||||
|
endpoint_keV = 0.0
|
||||||
if beta_data:
|
if beta_data:
|
||||||
for k, v in beta_data.items():
|
for _k, v in beta_data.items():
|
||||||
for state in v:
|
for state in v:
|
||||||
if state[0] == 0: # ground state branch
|
if len(state) > 7 and state[7] is not None:
|
||||||
endpoint_keV = float(state[7])
|
endpoint_keV = max(endpoint_keV, float(state[7]))
|
||||||
break
|
|
||||||
if endpoint_keV > 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
# No ground state branch — use first available branch
|
|
||||||
if endpoint_keV == 0.0:
|
|
||||||
for k, v in beta_data.items():
|
|
||||||
if v and len(v) > 0:
|
|
||||||
endpoint_keV = float(v[0][7])
|
|
||||||
break
|
|
||||||
|
|
||||||
if halflife_data:
|
if halflife_data:
|
||||||
for k, v in halflife_data.items():
|
for k, v in halflife_data.items():
|
||||||
@@ -447,6 +447,11 @@ def get_decay_info_from_paceENSDF(
|
|||||||
intensity=float(state[9]) / 100.0,
|
intensity=float(state[9]) / 100.0,
|
||||||
log_ft=float(state[11]) if state[11] is not None else None,
|
log_ft=float(state[11]) if state[11] is not None else None,
|
||||||
transition_type=branch_transition_type,
|
transition_type=branch_transition_type,
|
||||||
|
endpoint_MeV=(
|
||||||
|
float(state[7]) / 1000.0
|
||||||
|
if len(state) > 7 and state[7] is not None
|
||||||
|
else 0.0
|
||||||
|
),
|
||||||
)
|
)
|
||||||
branches.append(branch)
|
branches.append(branch)
|
||||||
|
|
||||||
@@ -472,7 +477,7 @@ def decay_info_to_config(
|
|||||||
intensity_cutoff: float = 0.0,
|
intensity_cutoff: float = 0.0,
|
||||||
neutrino_mass_MeV: float = 0.0,
|
neutrino_mass_MeV: float = 0.0,
|
||||||
csv_separator: str = ",",
|
csv_separator: str = ",",
|
||||||
normalize_to_one: bool = False,
|
separate_branches: bool = False,
|
||||||
) -> SpectrumConfig:
|
) -> SpectrumConfig:
|
||||||
"""
|
"""
|
||||||
Convert DecayInfo to a SpectrumConfig.
|
Convert DecayInfo to a SpectrumConfig.
|
||||||
@@ -489,20 +494,28 @@ def decay_info_to_config(
|
|||||||
Neutrino mass in MeV.
|
Neutrino mass in MeV.
|
||||||
csv_separator : str
|
csv_separator : str
|
||||||
CSV column separator.
|
CSV column separator.
|
||||||
normalize_to_one : bool
|
separate_branches : bool
|
||||||
If True, normalize each branch to 1, ignoring intensity.
|
If True, emit separate branch spectra, each normalized to
|
||||||
|
integrate to 1 (intensity ignored; no total spectrum column).
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
SpectrumConfig
|
SpectrumConfig
|
||||||
Configuration ready for BetaSpectrum.from_config().
|
Configuration ready for BetaSpectrum.from_config().
|
||||||
"""
|
"""
|
||||||
# Convert BranchInfo -> BranchConfig for SpectrumConfig
|
# Convert BranchInfo -> BranchConfig for SpectrumConfig. Per-branch
|
||||||
|
# endpoints are the measured transition endpoint energies when
|
||||||
|
# available; the base-endpoint minus level-energy subtraction is only
|
||||||
|
# a legacy fallback for hand-built DecayInfo objects without them.
|
||||||
branches: Optional[List[BranchConfig]] = None
|
branches: Optional[List[BranchConfig]] = None
|
||||||
if info.branches:
|
if info.branches:
|
||||||
branches = [
|
branches = [
|
||||||
BranchConfig(
|
BranchConfig(
|
||||||
endpoint_MeV=info.endpoint_MeV - b.level_energy_keV / 1000.0,
|
endpoint_MeV=(
|
||||||
|
b.endpoint_MeV
|
||||||
|
if b.endpoint_MeV > 0.0
|
||||||
|
else info.endpoint_MeV - b.level_energy_keV / 1000.0
|
||||||
|
),
|
||||||
transition_type=b.transition_type,
|
transition_type=b.transition_type,
|
||||||
intensity=b.intensity,
|
intensity=b.intensity,
|
||||||
)
|
)
|
||||||
@@ -523,7 +536,7 @@ def decay_info_to_config(
|
|||||||
branches=branches,
|
branches=branches,
|
||||||
intensity_cutoff=intensity_cutoff,
|
intensity_cutoff=intensity_cutoff,
|
||||||
csv_separator=csv_separator,
|
csv_separator=csv_separator,
|
||||||
normalize_to_one=normalize_to_one,
|
separate_branches=separate_branches,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -542,14 +555,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],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -615,8 +620,13 @@ def load_json_input(filepath: str) -> Dict[str, Any]:
|
|||||||
if not path.exists():
|
if not path.exists():
|
||||||
raise FileNotFoundError(f"Input file not found: {filepath}")
|
raise FileNotFoundError(f"Input file not found: {filepath}")
|
||||||
|
|
||||||
with open(path, "r") as f:
|
try:
|
||||||
data = json.load(f)
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
except OSError as exc:
|
||||||
|
raise FileNotFoundError(f"Input file not readable: {filepath}") from exc
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise ValueError(f"Invalid JSON in input file {filepath}: {exc}") from exc
|
||||||
|
|
||||||
# Fill in defaults for missing optional fields
|
# Fill in defaults for missing optional fields
|
||||||
for key, default in DEFAULT_JSON_SCHEMA.items():
|
for key, default in DEFAULT_JSON_SCHEMA.items():
|
||||||
@@ -699,7 +709,7 @@ def create_config_from_source(
|
|||||||
intensity_cutoff = kwargs.get("intensity_cutoff", 0.0)
|
intensity_cutoff = kwargs.get("intensity_cutoff", 0.0)
|
||||||
neutrino_mass = kwargs.get("neutrino_mass_MeV", 0.0)
|
neutrino_mass = kwargs.get("neutrino_mass_MeV", 0.0)
|
||||||
csv_separator = kwargs.get("csv_separator", ",")
|
csv_separator = kwargs.get("csv_separator", ",")
|
||||||
normalize_to_one = kwargs.get("normalize_to_one", False)
|
separate_branches = kwargs.get("separate_branches", False)
|
||||||
info = get_decay_info_from_paceENSDF(nuclide, decay_type, decay_index)
|
info = get_decay_info_from_paceENSDF(nuclide, decay_type, decay_index)
|
||||||
return decay_info_to_config(
|
return decay_info_to_config(
|
||||||
info,
|
info,
|
||||||
@@ -707,7 +717,7 @@ def create_config_from_source(
|
|||||||
intensity_cutoff=intensity_cutoff,
|
intensity_cutoff=intensity_cutoff,
|
||||||
neutrino_mass_MeV=neutrino_mass,
|
neutrino_mass_MeV=neutrino_mass,
|
||||||
csv_separator=csv_separator,
|
csv_separator=csv_separator,
|
||||||
normalize_to_one=normalize_to_one,
|
separate_branches=separate_branches,
|
||||||
)
|
)
|
||||||
elif source == "json":
|
elif source == "json":
|
||||||
if not json_path:
|
if not json_path:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
CSV export module — CSV export functionality for BetaSpectrumAnalyzer.
|
CSV export module — CSV export functionality for BetaSpectrumAnalyzer.
|
||||||
|
|
||||||
Column semantics (v0.4.0 onward):
|
Column semantics (v0.4.1 onward):
|
||||||
- The basic spectrum is ALWAYS core = phase_space x fermi; the Fermi function
|
- The basic spectrum is ALWAYS core = phase_space x fermi; the Fermi function
|
||||||
is a core spectrum component, never an optional correction. Higher-order
|
is a core spectrum component, never an optional correction. Higher-order
|
||||||
corrections (finite size, charge distribution, screening, exchange,
|
corrections (finite size, charge distribution, screening, exchange,
|
||||||
@@ -12,14 +12,14 @@ Column semantics (v0.4.0 onward):
|
|||||||
(the total column itself is normalized to integrate to 1). In the legacy
|
(the total column itself is normalized to integrate to 1). In the legacy
|
||||||
export branch columns contained only the per-branch phase-space x
|
export branch columns contained only the per-branch phase-space x
|
||||||
radiative product WITHOUT the Fermi function — physically wrong.
|
radiative product WITHOUT the Fermi function — physically wrong.
|
||||||
- ``normalize_to_one`` mode: each branch column is normalized to
|
- ``separate_branches`` mode: each branch column is normalized to
|
||||||
integrate to 1 individually, intensity is ignored, no total column.
|
integrate to 1 individually, intensity is ignored, no total column.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import TYPE_CHECKING, Dict, Optional
|
from typing import TYPE_CHECKING, Dict
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -62,7 +62,7 @@ class CSVEmitter:
|
|||||||
"""
|
"""
|
||||||
components = analyzer.components
|
components = analyzer.components
|
||||||
config = analyzer.config
|
config = analyzer.config
|
||||||
to_one = config.normalize_to_one
|
separate = config.separate_branches
|
||||||
n_branches = analyzer._n_branches
|
n_branches = analyzer._n_branches
|
||||||
|
|
||||||
# Physical branch spectra: core(phase_space x fermi) x corrections.
|
# Physical branch spectra: core(phase_space x fermi) x corrections.
|
||||||
@@ -127,9 +127,9 @@ class CSVEmitter:
|
|||||||
header_lines.append(f"# e_step: {config.e_step_MeV:.4f} MeV")
|
header_lines.append(f"# e_step: {config.e_step_MeV:.4f} MeV")
|
||||||
header_lines.append(f"# n_points: {len(analyzer.energies_MeV)}")
|
header_lines.append(f"# n_points: {len(analyzer.energies_MeV)}")
|
||||||
|
|
||||||
if to_one:
|
if separate:
|
||||||
header_lines.append(
|
header_lines.append(
|
||||||
"# branch normalization: to_one "
|
"# branch normalization: separate "
|
||||||
"(each branch integrates to 1, intensity ignored)"
|
"(each branch integrates to 1, intensity ignored)"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -179,16 +179,17 @@ class CSVEmitter:
|
|||||||
"energy_MeV": analyzer.energies_MeV,
|
"energy_MeV": analyzer.energies_MeV,
|
||||||
}
|
}
|
||||||
|
|
||||||
integral: Optional[float] = None
|
# Positive-integral guard applies to BOTH normalization modes: a
|
||||||
if not to_one:
|
# degenerate spectrum must never be written (not even as zeros).
|
||||||
total_raw = analyzer.spectrum(analyzer.W)
|
total_raw = analyzer.spectrum(analyzer.W)
|
||||||
integral = float(np.trapezoid(total_raw, analyzer.energies_MeV))
|
integral = float(np.trapezoid(total_raw, analyzer.energies_MeV))
|
||||||
if integral <= 0.0:
|
if integral <= 0.0:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Total spectrum integral is non-positive: every branch "
|
"Total spectrum integral is non-positive: every branch "
|
||||||
"is degenerate (check neutrino_mass vs branch endpoints "
|
"is degenerate (check neutrino_mass vs branch endpoints "
|
||||||
"and intensity_cutoff)"
|
"and intensity_cutoff)"
|
||||||
)
|
)
|
||||||
|
if not separate:
|
||||||
data["total_spectrum"] = total_raw / integral
|
data["total_spectrum"] = total_raw / integral
|
||||||
|
|
||||||
for i in range(n_branches):
|
for i in range(n_branches):
|
||||||
@@ -198,27 +199,32 @@ class CSVEmitter:
|
|||||||
else:
|
else:
|
||||||
col_name = f"branch_{key_i}_spectrum"
|
col_name = f"branch_{key_i}_spectrum"
|
||||||
|
|
||||||
if to_one:
|
if separate:
|
||||||
area = float(np.trapezoid(physical[i], analyzer.energies_MeV))
|
area = float(np.trapezoid(physical[i], analyzer.energies_MeV))
|
||||||
if area > 0.0:
|
if area > 0.0:
|
||||||
data[col_name] = physical[i] / area
|
data[col_name] = physical[i] / area
|
||||||
else:
|
else:
|
||||||
data[col_name] = np.zeros_like(physical[i])
|
data[col_name] = np.zeros_like(physical[i])
|
||||||
else:
|
else:
|
||||||
assert integral is not None
|
|
||||||
intensity_i = analyzer._unified_branches[i].intensity
|
intensity_i = analyzer._unified_branches[i].intensity
|
||||||
data[col_name] = physical[i] * intensity_i / integral
|
data[col_name] = physical[i] * intensity_i / integral
|
||||||
|
|
||||||
if csv_verbosity == "full":
|
if csv_verbosity == "full":
|
||||||
# Per-branch component columns (factorized ingredients).
|
# Per-branch component columns (factorized ingredients).
|
||||||
for i in range(n_branches):
|
# Only multi-branch mode carries per-branch factors: in single
|
||||||
for comp_name in ["PhaseSpace", "Radiative"]:
|
# mode the bare PhaseSpace/Radiative columns below already
|
||||||
key = analyzer._get_component_key(i, comp_name)
|
# describe the single spectrum, and prefixed duplicates would
|
||||||
if key in components:
|
# silently disagree with them once renamed.
|
||||||
data[f"branch_{i + 1}_{comp_name}"] = components[key]
|
if analyzer._is_multi:
|
||||||
|
for i in range(n_branches):
|
||||||
|
for comp_name in ["PhaseSpace", "Radiative"]:
|
||||||
|
key = analyzer._get_component_key(i, comp_name)
|
||||||
|
if key in components:
|
||||||
|
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(
|
||||||
|
|||||||
@@ -57,9 +57,6 @@ class SpectrumConfig:
|
|||||||
"""
|
"""
|
||||||
Configuration for beta spectrum calculation.
|
Configuration for beta spectrum calculation.
|
||||||
|
|
||||||
Supports declarative detector response specification:
|
|
||||||
to enable automatic detector smearing via convolve_with_detector().
|
|
||||||
|
|
||||||
Supports multi-branch decay via the `branches` field. When branches are
|
Supports multi-branch decay via the `branches` field. When branches are
|
||||||
provided, the total spectrum is the intensity-weighted sum of individual
|
provided, the total spectrum is the intensity-weighted sum of individual
|
||||||
branch spectra. Each branch has its own endpoint energy and transition type.
|
branch spectra. Each branch has its own endpoint energy and transition type.
|
||||||
@@ -90,9 +87,10 @@ class SpectrumConfig:
|
|||||||
|
|
||||||
# CSV output options
|
# CSV output options
|
||||||
csv_separator: str = "," # Default separator for CSV output
|
csv_separator: str = "," # Default separator for CSV output
|
||||||
normalize_to_one: bool = (
|
separate_branches: bool = (
|
||||||
False # If True, normalize each branch to 1, ignoring intensity
|
False # If True: emit separate branch spectra, each normalized to
|
||||||
)
|
)
|
||||||
|
# integrate to 1 (intensity ignored); no total spectrum column
|
||||||
|
|
||||||
|
|
||||||
def _effective_single_fields(config: SpectrumConfig) -> Tuple[float, str]:
|
def _effective_single_fields(config: SpectrumConfig) -> Tuple[float, str]:
|
||||||
|
|||||||
@@ -5,14 +5,5 @@
|
|||||||
"A_number": 99,
|
"A_number": 99,
|
||||||
"endpoint_MeV": 0.294,
|
"endpoint_MeV": 0.294,
|
||||||
"transition_type": "A",
|
"transition_type": "A",
|
||||||
"e_step_MeV": 0.001,
|
"e_step_MeV": 0.001
|
||||||
"use_detector_response": false,
|
|
||||||
"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]
|
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user