refactor: unify plotting — single-branch treated as multi-branch with one branch; remove old single-branch methods
This commit is contained in:
+154
-458
@@ -2,6 +2,8 @@
|
|||||||
Analyzer module — BetaSpectrumAnalyzer class.
|
Analyzer module — BetaSpectrumAnalyzer class.
|
||||||
|
|
||||||
Introspection, analysis, and debugging tools for BetaSpectrum.
|
Introspection, analysis, and debugging tools for BetaSpectrum.
|
||||||
|
All nuclides (single- and multi-branch) are treated uniformly as one or more
|
||||||
|
branches — single-branch is just a special case with one branch.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -9,6 +11,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -55,10 +58,33 @@ class BetaSpectrumAnalyzer:
|
|||||||
self._logger = logger
|
self._logger = logger
|
||||||
self._is_multi = spectrum._is_multi
|
self._is_multi = spectrum._is_multi
|
||||||
|
|
||||||
|
# Unified branch view: single-branch treated as one-branch multi-branch
|
||||||
|
if not self._is_multi and self.spectrum.branch_spectra == []:
|
||||||
|
self._unified_branches: List[_UnifiedBranch] = [
|
||||||
|
_UnifiedBranch(
|
||||||
|
endpoint_MeV=config.endpoint_MeV,
|
||||||
|
transition_type=config.transition_type,
|
||||||
|
intensity=1.0,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
self._unified_branches = [
|
||||||
|
_UnifiedBranch(
|
||||||
|
endpoint_MeV=b.endpoint_MeV,
|
||||||
|
transition_type=b.transition_type,
|
||||||
|
intensity=b.intensity,
|
||||||
|
)
|
||||||
|
for b in self.spectrum.branches
|
||||||
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def components(self) -> Dict[str, np.ndarray]:
|
def components(self) -> Dict[str, np.ndarray]:
|
||||||
if self._components_cache is None:
|
if self._components_cache is None:
|
||||||
self._components_cache = self.spectrum.calculate_components(self.W)
|
raw = self.spectrum.calculate_components(self.W)
|
||||||
|
# Normalize keys: single-branch has bare names,
|
||||||
|
# multi-branch has branch_0., branch_1., etc.
|
||||||
|
# We keep the raw keys for export_to_csv compatibility.
|
||||||
|
self._components_cache = raw
|
||||||
assert self._components_cache is not None
|
assert self._components_cache is not None
|
||||||
return self._components_cache
|
return self._components_cache
|
||||||
|
|
||||||
@@ -73,9 +99,7 @@ class BetaSpectrumAnalyzer:
|
|||||||
return self._branch_spectra_cache
|
return self._branch_spectra_cache
|
||||||
|
|
||||||
def total_spectrum(self, normalize: bool = True) -> np.ndarray:
|
def total_spectrum(self, normalize: bool = True) -> np.ndarray:
|
||||||
"""
|
"""Calculate the total spectrum."""
|
||||||
Calculate the total_spectrum
|
|
||||||
"""
|
|
||||||
total = self.spectrum(self.W)
|
total = self.spectrum(self.W)
|
||||||
|
|
||||||
if normalize:
|
if normalize:
|
||||||
@@ -127,6 +151,42 @@ class BetaSpectrumAnalyzer:
|
|||||||
|
|
||||||
return convolved
|
return convolved
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Unified branch view
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _n_branches(self) -> int:
|
||||||
|
"""Number of branches (unified view)."""
|
||||||
|
return len(self._unified_branches)
|
||||||
|
|
||||||
|
def _get_branch_spectrum(self, i: int) -> np.ndarray:
|
||||||
|
"""Get the spectrum array for branch i."""
|
||||||
|
if i < len(self.spectrum.branch_spectra):
|
||||||
|
return self.spectrum.branch_spectra[i]
|
||||||
|
# Single-branch: the spectrum itself is the only branch
|
||||||
|
return self.spectrum(self.W)
|
||||||
|
|
||||||
|
def _get_component_key(self, i: int, name: str) -> str:
|
||||||
|
"""Get the canonical key for a per-branch component."""
|
||||||
|
if self._is_multi:
|
||||||
|
return f"branch_{i}.{name}"
|
||||||
|
return name
|
||||||
|
|
||||||
|
def _has_component(self, i: int, name: str) -> bool:
|
||||||
|
"""Check if branch i has a given component."""
|
||||||
|
key = self._get_component_key(i, name)
|
||||||
|
return key in self.components
|
||||||
|
|
||||||
|
def _get_component(self, i: int, name: str) -> np.ndarray:
|
||||||
|
"""Get a per-branch component array."""
|
||||||
|
key = self._get_component_key(i, name)
|
||||||
|
return self.components[key]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Plotting helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _build_figure_header(self, fig: "Figure") -> None:
|
def _build_figure_header(self, fig: "Figure") -> None:
|
||||||
"""Add a compact header with nuclide info, e_step, commit and timestamp outside axes."""
|
"""Add a compact header with nuclide info, e_step, commit and timestamp outside axes."""
|
||||||
parent = self._element_symbol(self.config.Z_parent)
|
parent = self._element_symbol(self.config.Z_parent)
|
||||||
@@ -153,24 +213,26 @@ class BetaSpectrumAnalyzer:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _build_figure_title(self, fig: "Figure") -> str:
|
def _build_figure_title(self, fig: "Figure") -> str:
|
||||||
"""Build a unified figure title and return the figure for suptitle."""
|
"""Build a unified figure title."""
|
||||||
parent = self._element_symbol(self.config.Z_parent)
|
parent = self._element_symbol(self.config.Z_parent)
|
||||||
daughter = self._element_symbol(self.config.Z_daughter)
|
daughter = self._element_symbol(self.config.Z_daughter)
|
||||||
n = self.config.A_number
|
n = self.config.A_number
|
||||||
title = f"Beta-decay: {n}{parent} -> {n}{daughter}"
|
title = f"Beta-decay: {n}{parent} -> {n}{daughter}"
|
||||||
if self._is_multi and self.spectrum.branches:
|
if self._n_branches > 1:
|
||||||
title += f" ({len(self.spectrum.branches)} branches)"
|
title += f" ({self._n_branches} branches)"
|
||||||
return title
|
return title
|
||||||
|
|
||||||
def _format_branch_label(self, i: int) -> str:
|
def _format_branch_label(self, i: int) -> str:
|
||||||
"""Abbreviated branch label for legend."""
|
"""Abbreviated branch label for legend."""
|
||||||
if i >= len(self.spectrum.branches):
|
branch = self._unified_branches[i]
|
||||||
return f"Br. {i+1}"
|
|
||||||
branch = self.spectrum.branches[i]
|
|
||||||
e0 = branch.endpoint_MeV * 1000
|
e0 = branch.endpoint_MeV * 1000
|
||||||
intensity_pct = branch.intensity * 100
|
intensity_pct = branch.intensity * 100
|
||||||
return f"Br. {i+1}: {e0:.1f} keV ({intensity_pct:.1f}%)"
|
return f"Br. {i+1}: {e0:.1f} keV ({intensity_pct:.1f}%)"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Plotting — unified multi-branch approach
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def plot_analysis(
|
def plot_analysis(
|
||||||
self,
|
self,
|
||||||
save_path: Optional[str] = None,
|
save_path: Optional[str] = None,
|
||||||
@@ -179,294 +241,24 @@ class BetaSpectrumAnalyzer:
|
|||||||
"""
|
"""
|
||||||
Create visualization of the spectrum and all correction factors.
|
Create visualization of the spectrum and all correction factors.
|
||||||
|
|
||||||
In single-branch mode: standard 4-panel debug view or single spectrum plot.
|
|
||||||
In multi-branch mode: spectrum plot with individual branches shown,
|
|
||||||
and debug view with vertical layout for branch comparison.
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
save_path : str, optional
|
save_path : str, optional
|
||||||
Path to save the figure.
|
Path to save the figure.
|
||||||
show_components : bool
|
show_components : bool
|
||||||
If True, show full debug view.
|
If True, show full debug view.
|
||||||
If False, show only the spectrum plot with nuclear data header.
|
If False, show only the spectrum plot.
|
||||||
If True, use logarithmic y-scale. Set to False for
|
|
||||||
linear scale, which is better when branch intensities are similar.
|
|
||||||
"""
|
"""
|
||||||
total = self.total_spectrum(normalize=True)
|
total = self.total_spectrum(normalize=True)
|
||||||
components = self.components
|
components = self.components
|
||||||
self._timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
self._timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||||
|
|
||||||
if self._is_multi:
|
if show_components:
|
||||||
if show_components:
|
self._plot_debug_view(total, components, save_path)
|
||||||
self._plot_multi_branch_debug(
|
|
||||||
total, components, save_path
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self._plot_multi_branch_spectra(
|
|
||||||
total, components, save_path
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
if show_components:
|
self._plot_spectra_view(total, components, save_path)
|
||||||
self._plot_debug_view(total, components, save_path)
|
|
||||||
else:
|
|
||||||
self._plot_spectrum_only(total, save_path)
|
|
||||||
|
|
||||||
def _plot_spectrum_only(
|
def _plot_spectra_view(
|
||||||
self,
|
|
||||||
total: np.ndarray,
|
|
||||||
save_path: Optional[str],
|
|
||||||
) -> None:
|
|
||||||
"""Plot only the total spectrum."""
|
|
||||||
fig, ax = plt.subplots(figsize=(10, 6))
|
|
||||||
|
|
||||||
ax.plot(self.energies_MeV, total, "b-", lw=2, label="Normalized spectrum")
|
|
||||||
ax.set_xlabel(r"Electron kinetic energy $E$ [MeV]", fontsize=12)
|
|
||||||
ax.set_ylabel(
|
|
||||||
f"Normalized Counts / {self.config.e_step_MeV * 1000:.1f} keV", fontsize=12
|
|
||||||
)
|
|
||||||
fig.suptitle(
|
|
||||||
self._build_figure_title(fig), fontsize=14, fontweight="bold"
|
|
||||||
)
|
|
||||||
ax.set_yscale("log")
|
|
||||||
ax.grid(True, alpha=0.3)
|
|
||||||
ax.legend(loc="best", fontsize=10)
|
|
||||||
|
|
||||||
self._build_figure_header(fig)
|
|
||||||
|
|
||||||
plt.tight_layout()
|
|
||||||
|
|
||||||
if save_path:
|
|
||||||
fig.savefig(save_path, dpi=150, bbox_inches="tight")
|
|
||||||
print(f"Figure saved to {save_path}")
|
|
||||||
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
def _plot_debug_view(
|
|
||||||
self,
|
|
||||||
total: np.ndarray,
|
|
||||||
components: Dict[str, np.ndarray],
|
|
||||||
save_path: Optional[str],
|
|
||||||
) -> None:
|
|
||||||
"""Full debug view with 4-panel spectrum analysis."""
|
|
||||||
fig = plt.figure(figsize=(14, 10))
|
|
||||||
|
|
||||||
fig.suptitle(
|
|
||||||
self._build_figure_title(fig), fontsize=14, fontweight="bold"
|
|
||||||
)
|
|
||||||
|
|
||||||
e_step_keV = self.config.e_step_MeV * 1000
|
|
||||||
|
|
||||||
# 1. Main spectrum plot (top-left)
|
|
||||||
ax1 = plt.subplot(2, 2, 1)
|
|
||||||
ax1.plot(self.energies_MeV, total, "b-", lw=2, label="Total spectrum")
|
|
||||||
ax1.set_xlabel(r"Electron kinetic energy $E$ [MeV]", fontsize=10)
|
|
||||||
ax1.set_ylabel(
|
|
||||||
f"Normalized Counts / {e_step_keV:.1f} keV", fontsize=10
|
|
||||||
)
|
|
||||||
ax1.set_yscale("log")
|
|
||||||
ax1.grid(True, alpha=0.3)
|
|
||||||
ax1.legend(fontsize=8)
|
|
||||||
|
|
||||||
# 2. Correction plots (top-right)
|
|
||||||
ax2 = plt.subplot(2, 2, 2)
|
|
||||||
|
|
||||||
component_order = [
|
|
||||||
"PhaseSpace",
|
|
||||||
"Fermi",
|
|
||||||
"Screening",
|
|
||||||
"Exchange",
|
|
||||||
"FiniteSizeL0",
|
|
||||||
"ChargeDistributionU",
|
|
||||||
"Radiative",
|
|
||||||
]
|
|
||||||
colors = ["gray", "red", "green", "blue", "orange", "purple", "brown"]
|
|
||||||
|
|
||||||
for name, color in zip(component_order, colors):
|
|
||||||
if name in components:
|
|
||||||
values = components[name]
|
|
||||||
if name in ["Fermi", "PhaseSpace"]:
|
|
||||||
values = values / np.max(values)
|
|
||||||
label = name
|
|
||||||
else:
|
|
||||||
label = name
|
|
||||||
ax2.plot(
|
|
||||||
self.energies_MeV, values, color=color, lw=1.5,
|
|
||||||
label=label, alpha=0.8,
|
|
||||||
)
|
|
||||||
|
|
||||||
ax2.set_xlabel("Electron kinetic energy E [MeV]", fontsize=10)
|
|
||||||
ax2.set_ylabel("Correction factor", fontsize=10)
|
|
||||||
ax2.set_title("Spectrum components", fontsize=11)
|
|
||||||
ax2.grid(True, alpha=0.3)
|
|
||||||
ax2.legend(loc="best", fontsize=8)
|
|
||||||
|
|
||||||
# 3. Cumulative effect (bottom-left)
|
|
||||||
ax3 = plt.subplot(2, 2, 3)
|
|
||||||
|
|
||||||
cumulative = np.ones_like(self.W)
|
|
||||||
|
|
||||||
ax3.plot(
|
|
||||||
self.energies_MeV, np.ones_like(cumulative), "k--", lw=1.5,
|
|
||||||
label="Baseline", alpha=0.7,
|
|
||||||
)
|
|
||||||
|
|
||||||
for name, color in zip(component_order, colors):
|
|
||||||
if name in components:
|
|
||||||
cumulative *= components[name]
|
|
||||||
norm_cumulative = cumulative / np.max(cumulative)
|
|
||||||
ax3.plot(
|
|
||||||
self.energies_MeV, norm_cumulative, color=color, lw=1.5,
|
|
||||||
label=f"+ {name}", alpha=0.7,
|
|
||||||
)
|
|
||||||
|
|
||||||
ax3.set_xlabel("Electron kinetic energy E [MeV]", fontsize=10)
|
|
||||||
ax3.set_ylabel("Normalized spectrum", fontsize=10)
|
|
||||||
ax3.set_title("Cumulative effect", fontsize=11)
|
|
||||||
ax3.grid(True, alpha=0.3)
|
|
||||||
ax3.legend(loc="best", fontsize=8)
|
|
||||||
|
|
||||||
# 4. Deviation from unity (bottom-right)
|
|
||||||
ax4 = plt.subplot(2, 2, 4)
|
|
||||||
|
|
||||||
for name, color in zip(component_order, colors):
|
|
||||||
if name in components and name not in ["Fermi", "PhaseSpace"]:
|
|
||||||
deviation = components[name] - 1.0
|
|
||||||
ax4.plot(
|
|
||||||
self.energies_MeV, deviation, color=color, lw=1.5,
|
|
||||||
label=name, alpha=0.7,
|
|
||||||
)
|
|
||||||
|
|
||||||
ax4.set_xlabel("Electron kinetic energy E [MeV]", fontsize=10)
|
|
||||||
ax4.set_ylabel("Deviation from unity", fontsize=10)
|
|
||||||
ax4.set_title("Correction deviations (C - 1)", fontsize=11)
|
|
||||||
ax4.grid(True, alpha=0.3)
|
|
||||||
ax4.legend(loc="best", fontsize=8)
|
|
||||||
ax4.axhline(y=0, color="k", linestyle="-", lw=0.5)
|
|
||||||
|
|
||||||
self._build_figure_header(fig)
|
|
||||||
|
|
||||||
plt.tight_layout()
|
|
||||||
|
|
||||||
if save_path:
|
|
||||||
fig.savefig(save_path, dpi=150, bbox_inches="tight")
|
|
||||||
print(f"Figure saved to {save_path}")
|
|
||||||
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
def _element_symbol(self, Z: int) -> str:
|
|
||||||
symbols = [
|
|
||||||
"",
|
|
||||||
"H",
|
|
||||||
"He",
|
|
||||||
"Li",
|
|
||||||
"Be",
|
|
||||||
"B",
|
|
||||||
"C",
|
|
||||||
"N",
|
|
||||||
"O",
|
|
||||||
"F",
|
|
||||||
"Ne",
|
|
||||||
"Na",
|
|
||||||
"Mg",
|
|
||||||
"Al",
|
|
||||||
"Si",
|
|
||||||
"P",
|
|
||||||
"S",
|
|
||||||
"Cl",
|
|
||||||
"Ar",
|
|
||||||
"K",
|
|
||||||
"Ca",
|
|
||||||
"Sc",
|
|
||||||
"Ti",
|
|
||||||
"V",
|
|
||||||
"Cr",
|
|
||||||
"Mn",
|
|
||||||
"Fe",
|
|
||||||
"Co",
|
|
||||||
"Ni",
|
|
||||||
"Cu",
|
|
||||||
"Zn",
|
|
||||||
"Ga",
|
|
||||||
"Ge",
|
|
||||||
"As",
|
|
||||||
"Se",
|
|
||||||
"Br",
|
|
||||||
"Kr",
|
|
||||||
"Rb",
|
|
||||||
"Sr",
|
|
||||||
"Y",
|
|
||||||
"Zr",
|
|
||||||
"Nb",
|
|
||||||
"Mo",
|
|
||||||
"Tc",
|
|
||||||
"Ru",
|
|
||||||
"Rh",
|
|
||||||
"Pd",
|
|
||||||
"Ag",
|
|
||||||
"Cd",
|
|
||||||
"In",
|
|
||||||
"Sn",
|
|
||||||
"Sb",
|
|
||||||
"Te",
|
|
||||||
"I",
|
|
||||||
"Xe",
|
|
||||||
"Cs",
|
|
||||||
"Ba",
|
|
||||||
"La",
|
|
||||||
"Ce",
|
|
||||||
"Pr",
|
|
||||||
"Nd",
|
|
||||||
"Pm",
|
|
||||||
"Sm",
|
|
||||||
"Eu",
|
|
||||||
"Gd",
|
|
||||||
"Tb",
|
|
||||||
"Dy",
|
|
||||||
"Ho",
|
|
||||||
"Er",
|
|
||||||
"Tm",
|
|
||||||
"Yb",
|
|
||||||
"Lu",
|
|
||||||
"Hf",
|
|
||||||
"Ta",
|
|
||||||
"W",
|
|
||||||
"Re",
|
|
||||||
"Os",
|
|
||||||
"Ir",
|
|
||||||
"Pt",
|
|
||||||
"Au",
|
|
||||||
"Hg",
|
|
||||||
"Tl",
|
|
||||||
"Pb",
|
|
||||||
"Bi",
|
|
||||||
"Po",
|
|
||||||
"At",
|
|
||||||
"Rn",
|
|
||||||
"Fr",
|
|
||||||
"Ra",
|
|
||||||
"Ac",
|
|
||||||
"Th",
|
|
||||||
"Pa",
|
|
||||||
"U",
|
|
||||||
"Np",
|
|
||||||
"Pu",
|
|
||||||
"Am",
|
|
||||||
"Cm",
|
|
||||||
"Bk",
|
|
||||||
"Cf",
|
|
||||||
"Es",
|
|
||||||
"Fm",
|
|
||||||
]
|
|
||||||
return symbols[Z] if Z < len(symbols) else f"Z{Z}"
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Multi-branch plotting
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _plot_multi_branch_spectra(
|
|
||||||
self,
|
self,
|
||||||
total: np.ndarray,
|
total: np.ndarray,
|
||||||
components: Dict[str, np.ndarray],
|
components: Dict[str, np.ndarray],
|
||||||
@@ -481,18 +273,21 @@ class BetaSpectrumAnalyzer:
|
|||||||
"#bcbd22", "#17becf",
|
"#bcbd22", "#17becf",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
e_step_keV = self.config.e_step_MeV * 1000
|
||||||
|
|
||||||
# Compute branch contributions (universal * intensity * branch_spectrum)
|
# Compute branch contributions (universal * intensity * branch_spectrum)
|
||||||
branch_contribs = []
|
branch_contribs = []
|
||||||
for i, branch_spec in enumerate(self.spectrum.branch_spectra):
|
for i in range(self._n_branches):
|
||||||
universal_product = np.ones_like(self.W)
|
universal_product = np.ones_like(self.W)
|
||||||
for name in [
|
for name in [
|
||||||
"Fermi", "Screening", "FiniteSizeL0",
|
"Fermi", "Screening", "FiniteSizeL0",
|
||||||
"ChargeDistributionU", "Exchange",
|
"ChargeDistributionU", "Exchange",
|
||||||
]:
|
]:
|
||||||
if name in components:
|
key = self._get_component_key(i, name)
|
||||||
universal_product *= components[name]
|
if key in components:
|
||||||
bs = self.spectrum.get_branch_spectra(self.W)
|
universal_product *= components[key]
|
||||||
contrib = universal_product * self.spectrum.branches[i].intensity * bs[i]
|
bs = self._get_branch_spectrum(i)
|
||||||
|
contrib = universal_product * self._unified_branches[i].intensity * bs
|
||||||
branch_contribs.append(contrib)
|
branch_contribs.append(contrib)
|
||||||
|
|
||||||
total_raw = sum(branch_contribs)
|
total_raw = sum(branch_contribs)
|
||||||
@@ -511,7 +306,7 @@ class BetaSpectrumAnalyzer:
|
|||||||
|
|
||||||
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(
|
ax.set_ylabel(
|
||||||
f"Normalized Counts / {self.config.e_step_MeV * 1000:.1f} keV", fontsize=12
|
f"Normalized Counts / {e_step_keV:.1f} keV", fontsize=12
|
||||||
)
|
)
|
||||||
fig.suptitle(
|
fig.suptitle(
|
||||||
self._build_figure_title(fig), fontsize=14, fontweight="bold"
|
self._build_figure_title(fig), fontsize=14, fontweight="bold"
|
||||||
@@ -520,7 +315,7 @@ class BetaSpectrumAnalyzer:
|
|||||||
ax.grid(True, alpha=0.3)
|
ax.grid(True, alpha=0.3)
|
||||||
ax.legend(
|
ax.legend(
|
||||||
loc="best", fontsize=9,
|
loc="best", fontsize=9,
|
||||||
ncol=min(2, len(self.spectrum.branches) + 1),
|
ncol=min(2, self._n_branches + 1),
|
||||||
)
|
)
|
||||||
|
|
||||||
self._build_figure_header(fig)
|
self._build_figure_header(fig)
|
||||||
@@ -531,14 +326,14 @@ class BetaSpectrumAnalyzer:
|
|||||||
fig.savefig(save_path, dpi=150, bbox_inches="tight")
|
fig.savefig(save_path, dpi=150, bbox_inches="tight")
|
||||||
print(f"Figure saved to {save_path}")
|
print(f"Figure saved to {save_path}")
|
||||||
|
|
||||||
def _plot_multi_branch_debug(
|
def _plot_debug_view(
|
||||||
self,
|
self,
|
||||||
total: np.ndarray,
|
total: np.ndarray,
|
||||||
components: Dict[str, np.ndarray],
|
components: Dict[str, np.ndarray],
|
||||||
save_path: Optional[str],
|
save_path: Optional[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Debug view for multi-branch: vertical layout with 4 panels."""
|
"""Debug view: vertical layout with 4 panels."""
|
||||||
n_branches = len(self.spectrum.branches)
|
n_branches = self._n_branches
|
||||||
e_step_keV = self.config.e_step_MeV * 1000
|
e_step_keV = self.config.e_step_MeV * 1000
|
||||||
|
|
||||||
fig, axes = plt.subplots(4, 1, figsize=(12, 16), sharex=True)
|
fig, axes = plt.subplots(4, 1, figsize=(12, 16), sharex=True)
|
||||||
@@ -554,16 +349,17 @@ class BetaSpectrumAnalyzer:
|
|||||||
|
|
||||||
# Panel 1: Total spectrum + per-branch spectra
|
# Panel 1: Total spectrum + per-branch spectra
|
||||||
branch_contribs = []
|
branch_contribs = []
|
||||||
for i, branch_spec in enumerate(self.spectrum.branch_spectra):
|
for i in range(n_branches):
|
||||||
universal_product = np.ones_like(self.W)
|
universal_product = np.ones_like(self.W)
|
||||||
for name in [
|
for name in [
|
||||||
"Fermi", "Screening", "FiniteSizeL0",
|
"Fermi", "Screening", "FiniteSizeL0",
|
||||||
"ChargeDistributionU", "Exchange",
|
"ChargeDistributionU", "Exchange",
|
||||||
]:
|
]:
|
||||||
if name in components:
|
key = self._get_component_key(i, name)
|
||||||
universal_product *= components[name]
|
if key in components:
|
||||||
bs = self.spectrum.get_branch_spectra(self.W)
|
universal_product *= components[key]
|
||||||
contrib = universal_product * self.spectrum.branches[i].intensity * bs[i]
|
bs = self._get_branch_spectrum(i)
|
||||||
|
contrib = universal_product * self._unified_branches[i].intensity * bs
|
||||||
branch_contribs.append(contrib)
|
branch_contribs.append(contrib)
|
||||||
|
|
||||||
total_raw = sum(branch_contribs)
|
total_raw = sum(branch_contribs)
|
||||||
@@ -596,10 +392,12 @@ class BetaSpectrumAnalyzer:
|
|||||||
self.energies_MeV, fermi_scaled, "r-", lw=1.5,
|
self.energies_MeV, fermi_scaled, "r-", lw=1.5,
|
||||||
label="Fermi", alpha=0.8,
|
label="Fermi", alpha=0.8,
|
||||||
)
|
)
|
||||||
for i, branch in enumerate(self.spectrum.branches):
|
for i in range(n_branches):
|
||||||
color = branch_colors[i % len(branch_colors)]
|
color = branch_colors[i % len(branch_colors)]
|
||||||
if f"branch_{i}.PhaseSpace" in components:
|
branch = self._unified_branches[i]
|
||||||
ps_vals = components[f"branch_{i}.PhaseSpace"]
|
key = self._get_component_key(i, "PhaseSpace")
|
||||||
|
if key in components:
|
||||||
|
ps_vals = components[key]
|
||||||
W0_branch = T_to_W(branch.endpoint_MeV)
|
W0_branch = T_to_W(branch.endpoint_MeV)
|
||||||
mask = self.W <= W0_branch
|
mask = self.W <= W0_branch
|
||||||
ps_masked = np.where(mask, ps_vals, 0.0)
|
ps_masked = np.where(mask, ps_vals, 0.0)
|
||||||
@@ -639,10 +437,11 @@ class BetaSpectrumAnalyzer:
|
|||||||
|
|
||||||
# Panel 4: W0-dependent corrections (Radiative for each branch)
|
# Panel 4: W0-dependent corrections (Radiative for each branch)
|
||||||
ax = axes[3]
|
ax = axes[3]
|
||||||
for i, branch in enumerate(self.spectrum.branches):
|
for i in range(n_branches):
|
||||||
color = branch_colors[i % len(branch_colors)]
|
color = branch_colors[i % len(branch_colors)]
|
||||||
if f"branch_{i}.Radiative" in components:
|
key = self._get_component_key(i, "Radiative")
|
||||||
rad_vals = components[f"branch_{i}.Radiative"]
|
if key in components:
|
||||||
|
rad_vals = components[key]
|
||||||
ax.plot(
|
ax.plot(
|
||||||
self.energies_MeV, rad_vals, color=color, lw=1.5,
|
self.energies_MeV, rad_vals, color=color, lw=1.5,
|
||||||
alpha=0.7, label=f"Br. {i+1}",
|
alpha=0.7, label=f"Br. {i+1}",
|
||||||
@@ -661,21 +460,31 @@ class BetaSpectrumAnalyzer:
|
|||||||
fig.savefig(save_path, dpi=150, bbox_inches="tight")
|
fig.savefig(save_path, dpi=150, bbox_inches="tight")
|
||||||
print(f"Figure saved to {save_path}")
|
print(f"Figure saved to {save_path}")
|
||||||
|
|
||||||
def export_to_csv(self, filename: str, source_type: str = "unknown") -> None:
|
def _element_symbol(self, Z: int) -> str:
|
||||||
"""
|
"""Return chemical symbol for atomic number Z."""
|
||||||
Export spectrum data to CSV with metadata header.
|
symbols = [
|
||||||
|
"", "H", "He", "Li", "Be", "B", "C", "N", "O", "F",
|
||||||
|
"Ne", "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar", "K",
|
||||||
|
"Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu",
|
||||||
|
"Zn", "Ga", "Ge", "As", "Se", "Br", "Kr", "Rb", "Sr", "Y",
|
||||||
|
"Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd", "In",
|
||||||
|
"Sn", "Sb", "Te", "I", "Xe", "Cs", "Ba", "La", "Ce", "Pr",
|
||||||
|
"Nd", "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm",
|
||||||
|
"Yb", "Lu", "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au",
|
||||||
|
"Hg", "Tl", "Pb", "Bi", "Po", "At", "Rn", "Fr", "Ra", "Ac",
|
||||||
|
"Th", "Pa", "U", "Np", "Pu", "Am", "Cm", "Bk", "Cf", "Es",
|
||||||
|
]
|
||||||
|
return symbols[Z] if Z < len(symbols) else f"Z{Z}"
|
||||||
|
|
||||||
Parameters
|
# ---------------------------------------------------------------------------
|
||||||
----------
|
# Export
|
||||||
filename : str
|
# ---------------------------------------------------------------------------
|
||||||
Output CSV file path.
|
|
||||||
source_type : str
|
def export_to_csv(self, filename: str, source_type: str = "unknown") -> None:
|
||||||
Data source type (paceENSDF, json, cli) for the header.
|
"""Export spectrum data to CSV with metadata header."""
|
||||||
"""
|
|
||||||
total = self.total_spectrum(normalize=True)
|
total = self.total_spectrum(normalize=True)
|
||||||
components = self.components
|
components = self.components
|
||||||
|
|
||||||
# Build enabled corrections list
|
|
||||||
enabled = []
|
enabled = []
|
||||||
if self.config.use_phase_space:
|
if self.config.use_phase_space:
|
||||||
enabled.append("phase_space")
|
enabled.append("phase_space")
|
||||||
@@ -692,115 +501,8 @@ class BetaSpectrumAnalyzer:
|
|||||||
if self.config.use_exchange:
|
if self.config.use_exchange:
|
||||||
enabled.append("exchange")
|
enabled.append("exchange")
|
||||||
|
|
||||||
# Build metadata header
|
parent_symbol = self._element_symbol(self.config.Z_parent)
|
||||||
def _element_symbol(Z: int) -> str:
|
daughter_symbol = self._element_symbol(self.config.Z_daughter)
|
||||||
symbols = [
|
|
||||||
"",
|
|
||||||
"H",
|
|
||||||
"He",
|
|
||||||
"Li",
|
|
||||||
"Be",
|
|
||||||
"B",
|
|
||||||
"C",
|
|
||||||
"N",
|
|
||||||
"O",
|
|
||||||
"F",
|
|
||||||
"Ne",
|
|
||||||
"Na",
|
|
||||||
"Mg",
|
|
||||||
"Al",
|
|
||||||
"Si",
|
|
||||||
"P",
|
|
||||||
"S",
|
|
||||||
"Cl",
|
|
||||||
"Ar",
|
|
||||||
"K",
|
|
||||||
"Ca",
|
|
||||||
"Sc",
|
|
||||||
"Ti",
|
|
||||||
"V",
|
|
||||||
"Cr",
|
|
||||||
"Mn",
|
|
||||||
"Fe",
|
|
||||||
"Co",
|
|
||||||
"Ni",
|
|
||||||
"Cu",
|
|
||||||
"Zn",
|
|
||||||
"Ga",
|
|
||||||
"Ge",
|
|
||||||
"As",
|
|
||||||
"Se",
|
|
||||||
"Br",
|
|
||||||
"Kr",
|
|
||||||
"Rb",
|
|
||||||
"Sr",
|
|
||||||
"Y",
|
|
||||||
"Zr",
|
|
||||||
"Nb",
|
|
||||||
"Mo",
|
|
||||||
"Tc",
|
|
||||||
"Ru",
|
|
||||||
"Rh",
|
|
||||||
"Pd",
|
|
||||||
"Ag",
|
|
||||||
"Cd",
|
|
||||||
"In",
|
|
||||||
"Sn",
|
|
||||||
"Sb",
|
|
||||||
"Te",
|
|
||||||
"I",
|
|
||||||
"Xe",
|
|
||||||
"Cs",
|
|
||||||
"Ba",
|
|
||||||
"La",
|
|
||||||
"Ce",
|
|
||||||
"Pr",
|
|
||||||
"Nd",
|
|
||||||
"Pm",
|
|
||||||
"Sm",
|
|
||||||
"Eu",
|
|
||||||
"Gd",
|
|
||||||
"Tb",
|
|
||||||
"Dy",
|
|
||||||
"Ho",
|
|
||||||
"Er",
|
|
||||||
"Tm",
|
|
||||||
"Yb",
|
|
||||||
"Lu",
|
|
||||||
"Hf",
|
|
||||||
"Ta",
|
|
||||||
"W",
|
|
||||||
"Re",
|
|
||||||
"Os",
|
|
||||||
"Ir",
|
|
||||||
"Pt",
|
|
||||||
"Au",
|
|
||||||
"Hg",
|
|
||||||
"Tl",
|
|
||||||
"Pb",
|
|
||||||
"Bi",
|
|
||||||
"Po",
|
|
||||||
"At",
|
|
||||||
"Rn",
|
|
||||||
"Fr",
|
|
||||||
"Ra",
|
|
||||||
"Ac",
|
|
||||||
"Th",
|
|
||||||
"Pa",
|
|
||||||
"U",
|
|
||||||
"Np",
|
|
||||||
"Pu",
|
|
||||||
"Am",
|
|
||||||
"Cm",
|
|
||||||
"Bk",
|
|
||||||
"Cf",
|
|
||||||
"Es",
|
|
||||||
"Fm",
|
|
||||||
]
|
|
||||||
return symbols[Z] if Z < len(symbols) else f"Z{Z}"
|
|
||||||
|
|
||||||
parent_symbol = _element_symbol(self.config.Z_parent)
|
|
||||||
daughter_symbol = _element_symbol(self.config.Z_daughter)
|
|
||||||
|
|
||||||
header_lines = [
|
header_lines = [
|
||||||
f"# beta-spectrum v{__import__('beta_spectrum').__version__}",
|
f"# beta-spectrum v{__import__('beta_spectrum').__version__}",
|
||||||
@@ -813,16 +515,13 @@ class BetaSpectrumAnalyzer:
|
|||||||
f"# e_step: {self.config.e_step_MeV:.4f} MeV",
|
f"# e_step: {self.config.e_step_MeV:.4f} MeV",
|
||||||
]
|
]
|
||||||
|
|
||||||
if self._is_multi and self.spectrum.branches:
|
# Branch info — unified view
|
||||||
header_lines.append(f"# branches: {len(self.spectrum.branches)}")
|
for i, branch in enumerate(self._unified_branches):
|
||||||
for i, branch in enumerate(self.spectrum.branches):
|
header_lines.append(
|
||||||
header_lines.append(
|
f"# branch_{i+1}: E₀={branch.endpoint_MeV*1000:.1f} keV, "
|
||||||
f"# branch_{i+1}: E₀={branch.endpoint_MeV*1000:.1f} keV, "
|
f"transition={branch.transition_type}, "
|
||||||
f"transition={branch.transition_type}, "
|
f"intensity={branch.intensity:.4f}"
|
||||||
f"intensity={branch.intensity:.4f}"
|
)
|
||||||
)
|
|
||||||
else:
|
|
||||||
header_lines.append("# branches: 1 (single)")
|
|
||||||
|
|
||||||
if self.config.use_detector_response:
|
if self.config.use_detector_response:
|
||||||
header_lines.append(
|
header_lines.append(
|
||||||
@@ -835,45 +534,34 @@ class BetaSpectrumAnalyzer:
|
|||||||
|
|
||||||
header_lines.append(f"# git_commit: {get_git_short_hash()}")
|
header_lines.append(f"# git_commit: {get_git_short_hash()}")
|
||||||
|
|
||||||
# Write CSV with header
|
|
||||||
data: Dict[str, np.ndarray] = {
|
data: Dict[str, np.ndarray] = {
|
||||||
"energy_MeV": self.energies_MeV,
|
"energy_MeV": self.energies_MeV,
|
||||||
"spectrum": total,
|
"spectrum": total,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self._is_multi and self.spectrum.branches:
|
# Per-branch spectra and components
|
||||||
# Multi-branch mode: total spectrum + per-branch spectra + components
|
for i in range(self._n_branches):
|
||||||
# (branch info — intensity, endpoint, transition — is in the header, not repeated columns)
|
data[f"branch_{i+1}_spectrum"] = self._get_branch_spectrum(i)
|
||||||
for i, branch_spec in enumerate(self.spectrum.branch_spectra):
|
for comp_name in ["PhaseSpace", "Radiative"]:
|
||||||
data[f"branch_{i+1}_spectrum"] = branch_spec(self.W)
|
key = self._get_component_key(i, comp_name)
|
||||||
|
if key in components:
|
||||||
|
data[f"branch_{i+1}_{comp_name}"] = components[key]
|
||||||
|
|
||||||
# Per-branch components (PhaseSpace, Radiative, etc.)
|
# Universal components (no prefix)
|
||||||
for i, branch_spec in enumerate(self.spectrum.branch_spectra):
|
for name, values in components.items():
|
||||||
branch_comps = branch_spec.calculate_components(self.W)
|
if not name.startswith("branch_"):
|
||||||
for comp_name, values in branch_comps.items():
|
|
||||||
data[f"branch_{i+1}_{comp_name}"] = values
|
|
||||||
|
|
||||||
# Universal components (same for all branches — no prefix needed)
|
|
||||||
for name, values in components.items():
|
|
||||||
if not name.startswith("branch_"):
|
|
||||||
data[name] = values
|
|
||||||
else:
|
|
||||||
# Single-branch mode: standard export
|
|
||||||
for name, values in components.items():
|
|
||||||
data[name] = values
|
data[name] = values
|
||||||
|
|
||||||
df = pd.DataFrame(data)
|
df = pd.DataFrame(data)
|
||||||
df.to_csv(filename, index=False, float_format="%.4e")
|
df.to_csv(filename, index=False, float_format="%.4e")
|
||||||
|
|
||||||
# Prepend header
|
|
||||||
with open(filename, "r") as f:
|
with open(filename, "r") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
with open(filename, "w") as f:
|
with open(filename, "w") as f:
|
||||||
f.write("\n".join(header_lines) + "\n" + content)
|
f.write("\n".join(header_lines) + "\n" + content)
|
||||||
|
|
||||||
if self._logger:
|
if self._logger:
|
||||||
mode = "multi-branch" if self._is_multi else "single-branch"
|
self._logger.info("CSV exported to %s", filename)
|
||||||
self._logger.info("CSV exported to %s (%s mode)", filename, mode)
|
|
||||||
|
|
||||||
def get_data(self) -> Dict[str, Any]:
|
def get_data(self) -> Dict[str, Any]:
|
||||||
"""Get all numerical data for custom analysis."""
|
"""Get all numerical data for custom analysis."""
|
||||||
@@ -884,3 +572,11 @@ class BetaSpectrumAnalyzer:
|
|||||||
"components": self.components,
|
"components": self.components,
|
||||||
"config": self.config,
|
"config": self.config,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _UnifiedBranch:
|
||||||
|
"""Unified branch view for both single- and multi-branch modes."""
|
||||||
|
endpoint_MeV: float
|
||||||
|
transition_type: str
|
||||||
|
intensity: float
|
||||||
|
|||||||
Reference in New Issue
Block a user