Repository: LIBRA-project/libra-toolbox
Pull request #73: Activation foil classes
Author: login: RemDelaporteMathurin
Created: 2025-05-08T15:39:42Z | Merged: 2025-05-09T01:45:22Z

This PR moves away from `get_decay_lines` in favour of a class architecture for check sources.

This will be useful in #71


TODO:
- fix tests
- document


Diff:
diff --git a/libra_toolbox/neutron_detection/activation_foils/__init__.py b/libra_toolbox/neutron_detection/activation_foils/__init__.py
index 049aafc..34b4b03 100644
--- a/libra_toolbox/neutron_detection/activation_foils/__init__.py
+++ b/libra_toolbox/neutron_detection/activation_foils/__init__.py
@@ -1 +1,19 @@
+from datetime import datetime
+from . import calibration
+
+from dataclasses import dataclass
+
+
+@dataclass
+class CheckSource:
+    nuclide: calibration.Nuclide
+    activity_date: datetime.date
+    activity: float
+
+
+class ActivationFoil:
+    nuclide: calibration.Nuclide
+    mass: float
+
+
 from . import explicit, settings, calculations
diff --git a/libra_toolbox/neutron_detection/activation_foils/calibration.py b/libra_toolbox/neutron_detection/activation_foils/calibration.py
index 904cbac..9f32ed1 100644
--- a/libra_toolbox/neutron_detection/activation_foils/calibration.py
+++ b/libra_toolbox/neutron_detection/activation_foils/calibration.py
@@ -1,39 +1,57 @@
-import datetime
+from dataclasses import dataclass
+from typing import List
 
-def get_decay_lines(nuclides:list[str])->dict:
-    """ Creates dictionary of check source data
-    given a list of check source nuclides. """
-    # energy is the gamma energy in units of eV
-    # intensity is the percentage of decays that result in this energy gamma
-    all_decay_lines = {'Ba133':{'energy':[80.9979, 276.3989, 302.8508, 356.0129, 383.8485],
-                         'intensity':[0.329, 0.0716, 0.1834, 0.6205, 0.0894],
-                         'half_life':[10.551*365.25*24*3600],
-                         'activity_date':datetime.date(2014, 3, 19),
-                         'activity':1 * 3.7e4},
-                'Co60':{'energy':[1173.228, 1332.492],
-                        'intensity':[0.9985, 0.999826],
-                        'half_life':[1925.28*24*3600],
-                        'actvity_date':datetime.date(2014, 3, 19),
-                        'activity':0.872 * 3.7e4},
-                'Na22':{'energy':[511, 1274.537],
-                        'intensity':[1.80, 0.9994],
-                        'half_life':[2.6018*365.25*24*3600],
-                        'actvity_date':datetime.date(2014, 3, 19),
-                        'activity': 5 * 3.7e4},
-                'Cs137':{'energy':[661.657],
-                         'intensity':[0.851],
-                         'half_life':[30.08*365.25*24*3600],
-                         'actvity_date':datetime.date(2014, 3, 19),
-                         'activity':4.66 * 3.7e4},
-                'Mn54':{'energy':[834.848],
-                        'intensity':[0.99976],
-                        'half_life':[312.20*24*3600],
-                        'actvity_date':datetime.date(2016, 5, 2),
-                        'activity':6.27 * 3.7e4}}
-    decay_lines = {}
-    for nuclide in nuclides:
-        if nuclide in all_decay_lines.keys():
-            decay_lines[nuclide] = all_decay_lines[nuclide]
-        else:
-            raise ValueError(f'{nuclide} not yet added to get_decay_lines()')
-    return decay_lines
\ No newline at end of file
+
+@dataclass
+class Nuclide:
+    """
+    Class to hold the information of a nuclide.
+
+    Attributes
+    ----------
+    name :
+        The name of the nuclide.
+    energy :
+        The energy of the gamma rays emitted by the nuclide (in keV).
+    intensity :
+        The intensity of the gamma rays emitted by the nuclide.
+    half_life :
+        The half-life of the nuclide in seconds.
+    """
+
+    name: str
+    energy: List[float]
+    intensity: List[float]
+    half_life: float
+
+
+ba133 = Nuclide(
+    name="Ba133",
+    energy=[80.9979, 276.3989, 302.8508, 356.0129, 383.8485],
+    intensity=[0.329, 0.0716, 0.1834, 0.6205, 0.0894],
+    half_life=10.551 * 365.25 * 24 * 3600,
+)
+co60 = Nuclide(
+    name="Co60",
+    energy=[1173.228, 1332.492],
+    intensity=[0.9985, 0.999826],
+    half_life=1925.28 * 24 * 3600,
+)
+na22 = Nuclide(
+    name="Na22",
+    energy=[511, 1274.537],
+    intensity=[1.80, 0.9994],
+    half_life=2.6018 * 365.25 * 24 * 3600,
+)
+cs137 = Nuclide(
+    name="Cs137",
+    energy=[661.657],
+    intensity=[0.851],
+    half_life=30.08 * 365.25 * 24 * 3600,
+)
+mn54 = Nuclide(
+    name="Mn54",
+    energy=[834.848],
+    intensity=[0.99976],
+    half_life=312.20 * 24 * 3600,
+)
diff --git a/test/neutron_detection/test_calibration.py b/test/neutron_detection/test_calibration.py
deleted file mode 100644
index c30625c..0000000
--- a/test/neutron_detection/test_calibration.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from libra_toolbox.neutron_detection.activation_foils import calibration
-
-
-def test_get_decay_lines():
-    decay_lines = calibration.get_decay_lines(['Na22', 'Cs137'])
-    assert isinstance(decay_lines, dict)
-    assert 'energy' in decay_lines['Na22'].keys()
-
-


Reviews:
user: login: cdunn314
body: Looks pretty good, but the issue is that we can and sometimes do use several different check sources of the same nuclide. For example, we use both a Na22 source with an activity of 9.98 uCi on Sep 29, 2023, and another one that is 1 uCi on Jan 2006. I think creating several subclasses of CheckSources for Na22, Mn54, Cs137, Co60, and Ba133 with their half-lives, photon energies, and intensities built in, but for which the user specifies the activity, name and activity date.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T17:49:38Z
user: login: cdunn314
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T18:23:17Z
user: login: cdunn314
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T18:25:09Z
user: login: cdunn314
body: Just a couple little edits in the doc string
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T18:25:34Z
user: login: RemDelaporteMathurin
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T18:29:25Z

Review comments:
diff_hunk: @@ -1,39 +1,57 @@
-import datetime
+from dataclasses import dataclass
+from typing import List
 
-def get_decay_lines(nuclides:list[str])->dict:
-    """ Creates dictionary of check source data
-    given a list of check source nuclides. """
-    # energy is the gamma energy in units of eV
-    # intensity is the percentage of decays that result in this energy gamma
-    all_decay_lines = {'Ba133':{'energy':[80.9979, 276.3989, 302.8508, 356.0129, 383.8485],
-                         'intensity':[0.329, 0.0716, 0.1834, 0.6205, 0.0894],
-                         'half_life':[10.551*365.25*24*3600],
-                         'activity_date':datetime.date(2014, 3, 19),
-                         'activity':1 * 3.7e4},
-                'Co60':{'energy':[1173.228, 1332.492],
-                        'intensity':[0.9985, 0.999826],
-                        'half_life':[1925.28*24*3600],
-                        'actvity_date':datetime.date(2014, 3, 19),
-                        'activity':0.872 * 3.7e4},
-                'Na22':{'energy':[511, 1274.537],
-                        'intensity':[1.80, 0.9994],
-                        'half_life':[2.6018*365.25*24*3600],
-                        'actvity_date':datetime.date(2014, 3, 19),
-                        'activity': 5 * 3.7e4},
-                'Cs137':{'energy':[661.657],
-                         'intensity':[0.851],
-                         'half_life':[30.08*365.25*24*3600],
-                         'actvity_date':datetime.date(2014, 3, 19),
-                         'activity':4.66 * 3.7e4},
-                'Mn54':{'energy':[834.848],
-                        'intensity':[0.99976],
-                        'half_life':[312.20*24*3600],
-                        'actvity_date':datetime.date(2016, 5, 2),
-                        'activity':6.27 * 3.7e4}}
-    decay_lines = {}
-    for nuclide in nuclides:
-        if nuclide in all_decay_lines.keys():
-            decay_lines[nuclide] = all_decay_lines[nuclide]
-        else:
-            raise ValueError(f'{nuclide} not yet added to get_decay_lines()')
-    return decay_lines
\ No newline at end of file
+
+@dataclass
+class Nuclide:
+    """
+    Class to hold the information of a nuclide.
+
+    Attributes
+    ----------
+    name :
+        The name of the nuclide.
+    energy :
+        The energy of the gamma rays emitted by the nuclide.
path: libra_toolbox/neutron_detection/activation_foils/calibration.py
user: login: cdunn314
body: `        The energy of the gamma rays emitted by the nuclide in keV`
created_at: 2025-05-08T18:23:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,39 +1,57 @@
-import datetime
+from dataclasses import dataclass
+from typing import List
 
-def get_decay_lines(nuclides:list[str])->dict:
-    """ Creates dictionary of check source data
-    given a list of check source nuclides. """
-    # energy is the gamma energy in units of eV
-    # intensity is the percentage of decays that result in this energy gamma
-    all_decay_lines = {'Ba133':{'energy':[80.9979, 276.3989, 302.8508, 356.0129, 383.8485],
-                         'intensity':[0.329, 0.0716, 0.1834, 0.6205, 0.0894],
-                         'half_life':[10.551*365.25*24*3600],
-                         'activity_date':datetime.date(2014, 3, 19),
-                         'activity':1 * 3.7e4},
-                'Co60':{'energy':[1173.228, 1332.492],
-                        'intensity':[0.9985, 0.999826],
-                        'half_life':[1925.28*24*3600],
-                        'actvity_date':datetime.date(2014, 3, 19),
-                        'activity':0.872 * 3.7e4},
-                'Na22':{'energy':[511, 1274.537],
-                        'intensity':[1.80, 0.9994],
-                        'half_life':[2.6018*365.25*24*3600],
-                        'actvity_date':datetime.date(2014, 3, 19),
-                        'activity': 5 * 3.7e4},
-                'Cs137':{'energy':[661.657],
-                         'intensity':[0.851],
-                         'half_life':[30.08*365.25*24*3600],
-                         'actvity_date':datetime.date(2014, 3, 19),
-                         'activity':4.66 * 3.7e4},
-                'Mn54':{'energy':[834.848],
-                        'intensity':[0.99976],
-                        'half_life':[312.20*24*3600],
-                        'actvity_date':datetime.date(2016, 5, 2),
-                        'activity':6.27 * 3.7e4}}
-    decay_lines = {}
-    for nuclide in nuclides:
-        if nuclide in all_decay_lines.keys():
-            decay_lines[nuclide] = all_decay_lines[nuclide]
-        else:
-            raise ValueError(f'{nuclide} not yet added to get_decay_lines()')
-    return decay_lines
\ No newline at end of file
+
+@dataclass
+class Nuclide:
+    """
+    Class to hold the information of a nuclide.
+
+    Attributes
+    ----------
+    name :
+        The name of the nuclide.
+    energy :
+        The energy of the gamma rays emitted by the nuclide.
+    intensity :
+        The intensity of the gamma rays emitted by the nuclide. (must sum to 1)
path: libra_toolbox/neutron_detection/activation_foils/calibration.py
user: login: cdunn314
body: Intensity doesn't need to sum to 1. For example, in Na22, 1.8 photons are emitted per decay due to positron annihilation creating two photons per beta+ particle being emitted.
created_at: 2025-05-08T18:25:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,39 +1,57 @@
-import datetime
+from dataclasses import dataclass
+from typing import List
 
-def get_decay_lines(nuclides:list[str])->dict:
-    """ Creates dictionary of check source data
-    given a list of check source nuclides. """
-    # energy is the gamma energy in units of eV
-    # intensity is the percentage of decays that result in this energy gamma
-    all_decay_lines = {'Ba133':{'energy':[80.9979, 276.3989, 302.8508, 356.0129, 383.8485],
-                         'intensity':[0.329, 0.0716, 0.1834, 0.6205, 0.0894],
-                         'half_life':[10.551*365.25*24*3600],
-                         'activity_date':datetime.date(2014, 3, 19),
-                         'activity':1 * 3.7e4},
-                'Co60':{'energy':[1173.228, 1332.492],
-                        'intensity':[0.9985, 0.999826],
-                        'half_life':[1925.28*24*3600],
-                        'actvity_date':datetime.date(2014, 3, 19),
-                        'activity':0.872 * 3.7e4},
-                'Na22':{'energy':[511, 1274.537],
-                        'intensity':[1.80, 0.9994],
-                        'half_life':[2.6018*365.25*24*3600],
-                        'actvity_date':datetime.date(2014, 3, 19),
-                        'activity': 5 * 3.7e4},
-                'Cs137':{'energy':[661.657],
-                         'intensity':[0.851],
-                         'half_life':[30.08*365.25*24*3600],
-                         'actvity_date':datetime.date(2014, 3, 19),
-                         'activity':4.66 * 3.7e4},
-                'Mn54':{'energy':[834.848],
-                        'intensity':[0.99976],
-                        'half_life':[312.20*24*3600],
-                        'actvity_date':datetime.date(2016, 5, 2),
-                        'activity':6.27 * 3.7e4}}
-    decay_lines = {}
-    for nuclide in nuclides:
-        if nuclide in all_decay_lines.keys():
-            decay_lines[nuclide] = all_decay_lines[nuclide]
-        else:
-            raise ValueError(f'{nuclide} not yet added to get_decay_lines()')
-    return decay_lines
\ No newline at end of file
+
+@dataclass
+class Nuclide:
+    """
+    Class to hold the information of a nuclide.
+
+    Attributes
+    ----------
+    name :
+        The name of the nuclide.
+    energy :
+        The energy of the gamma rays emitted by the nuclide.
+    intensity :
+        The intensity of the gamma rays emitted by the nuclide. (must sum to 1)
path: libra_toolbox/neutron_detection/activation_foils/calibration.py
user: login: RemDelaporteMathurin
body: Ok good point thank you. It needs to be relative to something though no? i guess it's photon per decay?
created_at: 2025-05-08T18:29:25Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-05-08T16:25:11Z
body: ## [Codecov](https://app.codecov.io/gh/LIBRA-project/libra-toolbox/pull/73?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=LIBRA-project) Report
:white_check_mark: All modified and coverable lines are covered by tests.
:white_check_mark: Project coverage is 83.96%. Comparing base ([`bf88ee2`](https://app.codecov.io/gh/LIBRA-project/libra-toolbox/commit/bf88ee2fe387b48143ce1e4f5fb3bf6aae776f37?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=LIBRA-project)) to head ([`1681225`](https://app.codecov.io/gh/LIBRA-project/libra-toolbox/commit/16812259d2ed2fc8ff9861de74134266ff4ac817?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=LIBRA-project)).
:warning: Report is 128 commits behind head on main.

<details><summary>Additional details and impacted files</summary>



```diff
@@            Coverage Diff             @@
##             main      #73      +/-   ##
==========================================
+ Coverage   83.66%   83.96%   +0.30%     
==========================================
  Files          18       18              
  Lines        1114     1129      +15     
==========================================
+ Hits          932      948      +16     
+ Misses        182      181       -1     
```
</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/LIBRA-project/libra-toolbox/pull/73?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=LIBRA-project).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=LIBRA-project).
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: RemDelaporteMathurin
created_at: 2025-05-08T17:55:31Z
body: > Looks pretty good, but the issue is that we can and sometimes do use several different check sources of the same nuclide. For example, we use both a Na22 source with an activity of 9.98 uCi on Sep 29, 2023, and another one that is 1 uCi on Jan 2006. I think creating several subclasses of CheckSources for Na22, Mn54, Cs137, Co60, and Ba133 with their half-lives, photon energies, and intensities built in, but for which the user specifies the activity, name and activity date.

That makes sense to me. So instead of a subclass of CheckSource, we can create a class `Nuclide` that contains physical quantities like half-lives, energies, intensities
author_association: MEMBER