Skip to content

Commit ee4c84e

Browse files
authored
Add functionality to calculate delta values (#20)
* Processor utility contains a delta function calculator * Finish up `integrals_calc_delta` in processor * Add integral delta calculation per default to multi processor * Lint * Add docs for delta values * Add a run through test for delta calculation
1 parent 5b4b812 commit ee4c84e

File tree

10 files changed

+240
-4
lines changed

10 files changed

+240
-4
lines changed

docs/bg/deltas.rst

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
============
2+
Delta Values
3+
============
4+
5+
For all integrals, :math:`\delta`-values can be calculated automatically
6+
as described in the :doc:`usage documentation <../pkg/usage>`.
7+
Delta values are always calculated (1) with respect to the major isotope
8+
and (2) using the NIST database of the
9+
`iniabu <https://github.com/galactic-forensics/iniabu>`_ package.
10+
If no value can be calculated, i.e.,
11+
because the isotope is unknown to ``iniabu`` or because
12+
the natural abundance of the specified isotope is unknown,
13+
``np.nan`` will be returned as the :math:`\delta`-value.
14+
15+
.. note:: All :math:`\delta`-values are reported in per mil (‰).
16+
17+
--------------------------------
18+
Calculation :math:`\delta`-value
19+
--------------------------------
20+
21+
Be :math:`i` and :math:`j` the number of counts
22+
in the nominator and denominator isotopes, respectively, and
23+
:math:`r` the NIST isotopic ratio of the same isotopes.
24+
The :math:`\delta`-value can then be calculated as:
25+
26+
.. math::
27+
28+
\delta = \left( \frac{i/j}{r} - 1 \right) \times 1000 \qquad (‰)
29+
30+
-------------
31+
Uncertainties
32+
-------------
33+
34+
Be :math:`\sigma_i` and :math:`\sigma_j` the uncertainties of values
35+
:math:`i` and :math:`j`, respectively.
36+
The uncertainty of the :math:`\delta`-value :math:`\sigma_{\delta}`
37+
can be calculated as:
38+
39+
.. math::
40+
41+
\sigma_{\delta} = \frac{1000}{r} \left[
42+
\left(\frac{\sigma_i}{j}\right)^2 +
43+
\left(\frac{i\sigma_j}{j^2}\right)^2 \right]^{1/2}
44+
\qquad (‰)

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ Contents
7171
:maxdepth: 2
7272
:caption: Scientific Background
7373

74+
bg/deltas
7475
bg/integrals
7576

7677
.. toctree::

docs/pkg/usage.rst

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,9 @@ you could set the backgrounds as following:
169169
.. warning:: The backgrounds you define must have the same name as the peaks they are defined for.
170170
Multiple definitions per background can exist.
171171

172+
.. note:: Integral and background definitions can also be performed using a ``matplotlib`` GUI.
173+
For details, see :doc:`here <guis>`.
174+
172175
To apply the integrals,
173176
simply run:
174177

@@ -181,8 +184,23 @@ to either ``True`` or ``False``, respectively.
181184
Details on integral background corrections and the math behind it
182185
can be found :doc:`here <../bg/integrals>`.
183186

184-
.. note:: Integral and background definitions can also be performed using a ``matplotlib`` GUI.
185-
For details, see :doc:`here <guis>`.
187+
Finally, if your integral names follow the format used in the ``iniabu`` package,
188+
you can calculate :math:`\delta`-values for your individual peaks automatically.
189+
These values are always calculated with respect to the major isotope.
190+
If values are not available, ``np.nan`` will be written for that specific
191+
:math:`\delta`-value.
192+
To calculate the :math:`\delta` values, run (after calculating integrals):
193+
194+
.. code-block:: python
195+
196+
crd.integrals_calc_delta()
197+
198+
This will save the :math:`\delta`-values and associated uncertainties to
199+
``crd.integrals_delta``. If packages were defined, an :math:`\delta`-values
200+
for each package will also be calculated and stored in ``crd.integrals_delta_pkg``.
201+
Details on the calculation and error propagation can be found
202+
:doc:`here <../bg/deltas>`.
203+
186204

187205
+++++++
188206
Results

docs/pkg/variables.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,15 @@ Integrals
5555
the sum of counts in the defined area and its uncertainty.
5656
:math:`m` defined peaks, it will therefore be of size :math:`m \times 2`.
5757
The second entry of each integral is the uncertainty of the counts.
58+
- ``crd.integrals_delta``: :math:`\delta`-values for all integrals and uncertainties.
59+
The format is the same as for ``crd.integrals``.
5860
- ``crd.integrals_pkg``: Integral data for packages.
5961
For a total of :math:`p` packages and :math:`m` defined integrals,
6062
this ``numpy.ndarray`` will be of size :math:`p \times m \times 2`.
6163
As for the integrals, the sum of counts for each peak and its uncertainty
6264
are given.
65+
- ``crd.integrals_delta_pkg``: :math:`\delta`-values for all packages.
66+
The format is the same as for ``crd.integrals_pkg``.
6367

6468
----------------
6569
Single Shot Data

rimseval/multi_proc.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ def apply_to_all(self, id: int, opt_mcal: bool = False, bg_corr: bool = False):
103103
else:
104104
bg_corr = False
105105
crd_main.integrals_calc(bg_corr=bg_corr)
106+
crd_main.integrals_calc_delta()
106107
self.signal_processed.emit(str(crd_main.fname.name))
107108
rimseval.interfacer.save_cal_file(crd_main)
108109

@@ -126,6 +127,7 @@ def apply_to_all(self, id: int, opt_mcal: bool = False, bg_corr: bool = False):
126127

127128
# integrals
128129
file.integrals_calc(bg_corr=bg_corr)
130+
file.integrals_calc_delta()
129131

130132
# save calibration
131133
rimseval.interfacer.save_cal_file(file)

rimseval/processor.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ def __init__(self, fname: Path) -> None:
5656

5757
# Integrals
5858
self.integrals = None
59+
self.integrals_delta = None
5960
self.integrals_pkg = None
61+
self.integrals_delta_pkg = None
6062

6163
# parameters for calibration and evaluation
6264
self._params_mcal = None # mass calibration
@@ -600,6 +602,35 @@ def integral_windows(limits_tmp: np.array) -> List:
600602
bgs_pkg,
601603
)
602604

605+
def integrals_calc_delta(self) -> None:
606+
"""Calculate delta values for integrals and save them in class.
607+
608+
This routine uses the ``iniabu`` package to calculate delta values for defined
609+
integrals. It reads the peak names and calculates delta values for isotopes
610+
that can be understood ``iniabu``, and calculates the delta values with
611+
respect to the major isotope. These values are then saved to the class as
612+
``integrals_delta`` and ``integrals_delta_pkg``, if packages were defined.
613+
Uncertainties are propagated according to Gaussian error propagation.
614+
The format of the resulting arrays are identical to the ``integrals`` and
615+
``integrals_pkg`` arrays.
616+
617+
:raises ValueError: No integrals were calculated.
618+
"""
619+
if self.integrals is None or self.def_integrals is None:
620+
raise ValueError("No integrals were defined or calculated.")
621+
622+
peak_names = self.def_integrals[0]
623+
624+
integrals_delta = processor_utils.delta_calc(peak_names, self.integrals)
625+
626+
if self.integrals_pkg is not None:
627+
integrals_delta_pkg = np.zeros_like(self.integrals_pkg, dtype=float)
628+
for it, line in enumerate(self.integrals_pkg):
629+
integrals_delta_pkg[it] = processor_utils.delta_calc(peak_names, line)
630+
self.integrals_delta_pkg = integrals_delta_pkg
631+
632+
self.integrals_delta = integrals_delta
633+
603634
def mass_calibration(self) -> None:
604635
r"""Perform a mass calibration on the data.
605636

rimseval/processor_utils.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
"""Utilities for CRD processors. Mostly methods that can be jitted."""
22

3-
from typing import Tuple, Union
3+
from typing import List, Tuple, Union
44

55
from numba import njit
66
import numpy as np
77
from scipy import optimize
88

9-
from .utilities import fitting, utils
9+
from .utilities import fitting, ini, utils
1010

1111

1212
@njit
@@ -82,6 +82,72 @@ def dead_time_correction(
8282
return data
8383

8484

85+
def delta_calc(names: List[str], integrals: np.ndarray) -> np.ndarray:
86+
"""Calculate delta values for a given set of integrals.
87+
88+
Use ``iniabu`` to calculate the delta values with respect to major isotope.
89+
If the name of a peak is not valid or the major isotope not present, return
90+
``np.nan`` for that entry. Appropriate error propagation is done as well.
91+
92+
:param names: Names of the peaks as list.
93+
:param integrals: Integrals, as defined in ``CRDFileProcessor.integrals``.
94+
95+
:return: List of delta values, same shape and format as ``integrals``.
96+
"""
97+
# transform all names to valid ``iniabu`` names or call them ``None``
98+
names_iniabu = []
99+
for name in names:
100+
try:
101+
names_iniabu.append(ini.iso[name].name)
102+
except IndexError:
103+
names_iniabu.append(None)
104+
105+
# find major isotope names
106+
major_iso_name = []
107+
for name in names_iniabu:
108+
if name is None:
109+
major_iso_name.append(None)
110+
else:
111+
ele = name.split("-")[0]
112+
try:
113+
maj = ini._get_major_iso(ele)
114+
except IndexError:
115+
maj = None
116+
major_iso_name.append(maj)
117+
118+
integrals_dict = dict(zip(names_iniabu, range(len(names_iniabu))))
119+
120+
integrals_delta = np.zeros_like(integrals, dtype=float)
121+
122+
for it, iso in enumerate(names_iniabu):
123+
maj_iso = major_iso_name[it]
124+
125+
if iso is None or maj_iso not in names_iniabu:
126+
integrals_delta[it][0] = np.nan
127+
integrals_delta[it][1] = np.nan
128+
else:
129+
msr_nom = integrals[it][0]
130+
msr_nom_unc = integrals[it][1]
131+
msr_denom = integrals[integrals_dict[maj_iso]][0]
132+
msr_denom_unc = integrals[integrals_dict[maj_iso]][1]
133+
134+
msr_ratio = msr_nom / msr_denom
135+
integrals_delta[it][0] = ini.iso_delta(iso, maj_iso, msr_ratio)
136+
137+
# error calculation
138+
std_ratio = ini.iso_ratio(iso, maj_iso)
139+
integrals_delta[it][1] = (
140+
1000
141+
/ std_ratio
142+
* np.sqrt(
143+
(msr_nom_unc / msr_denom) ** 2
144+
+ (msr_nom * msr_denom_unc / msr_denom**2) ** 2
145+
)
146+
)
147+
148+
return integrals_delta
149+
150+
85151
def gaussian_fit_get_max(xdata: np.ndarray, ydata: np.ndarray) -> float:
86152
"""Fit a Gaussian to xdata and ydata and return the xvalue of the peak.
87153

tests/func/conftest.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Fixtures for functional tests."""
2+
3+
from pathlib import Path
4+
5+
import pytest
6+
import numpy as np
7+
8+
from rimseval.processor import CRDFileProcessor
9+
10+
11+
@pytest.fixture
12+
def crd_int_delta(crd_file) -> CRDFileProcessor:
13+
"""Initialize a CRDFileProcessor file, set some integrals, and return it.
14+
15+
The integrals set are fake, no actual calculation takes place. This fixture simply
16+
serves to test functionality (i.e., delta calculation) after processing of the CRD
17+
file.
18+
19+
:return: CRDFileProcessor instance with integrals defined and set.
20+
"""
21+
_, _, _, fname = crd_file
22+
crd = CRDFileProcessor(Path(fname))
23+
24+
peak_names = ["54Fe", "56Fe", "57Fe", "58Fe", "244Pu", "238Pu", "bg"]
25+
peak_limits = np.array(
26+
[
27+
[53.8, 54.2],
28+
[55.8, 56.2],
29+
[56.8, 57.2],
30+
[57.8, 58.2],
31+
[243.8, 244.2],
32+
[237.8, 238.2],
33+
[238.5, 238.7],
34+
]
35+
)
36+
crd.def_integrals = peak_names, peak_limits
37+
crd.integrals = np.array(
38+
[
39+
[5.84500000e04, 2.41764348e02],
40+
[9.17540000e05, 9.57883083e02],
41+
[2.11900000e04, 1.45567854e02],
42+
[2.82000000e03, 5.31036722e01],
43+
[10000, 100],
44+
[144, 12],
45+
[34212, 185],
46+
]
47+
)
48+
crd.integrals_pkg = np.array([crd.integrals, crd.integrals])
49+
return crd

tests/func/test_processor.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,16 @@ def test_integrals_definition_delete_undefined_background(crd_file):
186186
assert "Int2" not in crd.def_backgrounds[0]
187187

188188

189+
def test_integrals_delta_calc(crd_int_delta):
190+
"""Assure that the delta calculation on the integral returns correct values."""
191+
crd_int_delta.integrals_calc_delta()
192+
deltas_exp = np.array([0, 0, 0, 0, np.nan, np.nan, np.nan])
193+
deltas_rec = crd_int_delta.integrals_delta.transpose()[0] # test only deltas
194+
np.testing.assert_almost_equal(deltas_exp, deltas_rec)
195+
for line in crd_int_delta.integrals_pkg:
196+
np.testing.assert_almost_equal(crd_int_delta.integrals, line)
197+
198+
189199
def test_packages(crd_file):
190200
"""Simple test to ensure packages are made in two ways correctly."""
191201
_, ions_per_shot, _, fname = crd_file

tests/func/test_processor_utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ def test_create_packages():
4343
np.testing.assert_equal(pkg_data_rec, pkg_data_exp)
4444

4545

46+
def test_delta_calc():
47+
"""Take an integrals like array and calculate delta values.
48+
49+
Details of delta calculation is not tested.
50+
"""
51+
names = ["Fe54", "Fe56", "244Pu", "bg"]
52+
integrals = np.array([[10000, 100], [100000, 240], [100, 10], [2001, 21]])
53+
deltas = pu.delta_calc(names, integrals)
54+
assert np.isnan(deltas[2:3]).all() # last two must be nans
55+
56+
4657
def test_integrals_bg_corr():
4758
"""Background correction for defined integrals."""
4859
integrals = np.array([[10, np.sqrt(10)], [40, np.sqrt(40)]])

0 commit comments

Comments
 (0)