libra-toolbox 0.6.1.post1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ try:
2
+ # Python 3.8+
3
+ from importlib import metadata
4
+ except ImportError:
5
+ try:
6
+ # For older Python versions
7
+ import importlib_metadata as metadata
8
+ except ImportError:
9
+ # If both imports fail, set version to unknown
10
+ __version__ = "unknown"
11
+
12
+ try:
13
+ # Attempt to get the version of the package
14
+ __version__ = metadata.version("libra_toolbox")
15
+ except Exception:
16
+ # If it fails, set version to unknown
17
+ __version__ = "unknown"
18
+
19
+ from . import tritium
20
+ from . import neutron_detection
21
+ from . import neutronics
@@ -0,0 +1,21 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
6
+ TYPE_CHECKING = False
7
+ if TYPE_CHECKING:
8
+ from typing import Tuple
9
+ from typing import Union
10
+
11
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
12
+ else:
13
+ VERSION_TUPLE = object
14
+
15
+ version: str
16
+ __version__: str
17
+ __version_tuple__: VERSION_TUPLE
18
+ version_tuple: VERSION_TUPLE
19
+
20
+ __version__ = version = '0.6.1.post1'
21
+ __version_tuple__ = version_tuple = (0, 6, 1)
@@ -0,0 +1 @@
1
+ from . import activation_foils, diamond
@@ -0,0 +1 @@
1
+ from . import explicit, settings, calculations
@@ -0,0 +1,188 @@
1
+ import numpy as np
2
+ import sympy as sp
3
+ from .settings import *
4
+ import datetime
5
+ import matplotlib.pyplot as plt
6
+
7
+ t = sp.Symbol("t")
8
+ neutron_flux = sp.Symbol("\Gamma_n")
9
+ number_of_Nb93 = sp.Symbol(r"N_{Nb93}")
10
+ decay_constant = sp.Symbol("\lambda", positive=True)
11
+ cross_section = sp.Symbol("\sigma")
12
+ overall_efficiency = sp.Symbol("\eta")
13
+
14
+
15
+ def n93_number(foil_mass: float):
16
+ n93_molar_mass = 92.90637 * ureg.g / ureg.mol
17
+ return (foil_mass / n93_molar_mass).to(ureg.particle)
18
+
19
+
20
+ def delay_time(start_time: str, end_time: str):
21
+ # convert string to time with datetime
22
+ start_time = datetime.datetime.strptime(start_time, "%m/%d/%Y %H:%M:%S")
23
+ end_time = datetime.datetime.strptime(end_time, "%m/%d/%Y %H:%M:%S")
24
+ return (end_time - start_time).total_seconds() * ureg.s
25
+
26
+
27
+ def N_during_irradiation(
28
+ N0, t, decay_constant, neutron_flux, cross_section, nb_Nb93, t_0, mod=sp
29
+ ):
30
+ A = nb_Nb93 * neutron_flux * cross_section / decay_constant
31
+ return A + (N0 - A) * mod.exp(-decay_constant * (t - t_0))
32
+
33
+
34
+ def N_during_rest(N0, t, decay_constant, t_0, mod=sp):
35
+ return N0 * mod.exp(-decay_constant * (t - t_0))
36
+
37
+
38
+ def get_number_of_Nb92m_after_last_irradiations(irradiations: list):
39
+ """
40
+ Returns the number of Nb92m at the end of the last irradiation
41
+ as a symbolic expression with sympy
42
+
43
+
44
+ Args:
45
+ irradiations (list): list of dictionaries with keys "t_on" and "t_off"
46
+
47
+ Returns:
48
+ sp.Expression: symbolic expression for the number of Nb92m at the end of the last irradiation
49
+ """
50
+
51
+ # there is a series of irradiation and rest, get the number of Nb92m at the end of the last irradiation
52
+ t_on = irradiations[0]["t_on"]
53
+ t_off = irradiations[0]["t_off"]
54
+ N_after_irradiation = N_during_irradiation(
55
+ 0, t_off, decay_constant, neutron_flux, cross_section, number_of_Nb93, t_on
56
+ )
57
+ for irr in irradiations[1:]:
58
+ N_at_end_of_rest = N_during_rest(
59
+ N_after_irradiation, irr["t_on"], decay_constant, t_0=t_off
60
+ )
61
+ N_after_irradiation = N_during_irradiation(
62
+ N0=N_at_end_of_rest,
63
+ t=irr["t_off"],
64
+ decay_constant=decay_constant,
65
+ neutron_flux=neutron_flux,
66
+ cross_section=cross_section,
67
+ nb_Nb93=number_of_Nb93,
68
+ t_0=irr["t_on"],
69
+ )
70
+ t_off = irr["t_off"]
71
+
72
+ return N_after_irradiation
73
+
74
+
75
+ def get_number_ofNb92m_numpy(
76
+ irradiations: list, times: np.ndarray, neutron_flux: float, nb_Nb93: float
77
+ ):
78
+ """
79
+ Returns the number of Nb92m at different times as a numpy array
80
+
81
+ Args:
82
+ irradiations (list): list of dictionaries with keys "t_on" and "t_off"
83
+ times (np.ndarray): array of times
84
+ neutron_flux (float): neutron flux
85
+ nb_Nb93 (float): number of Nb93 atoms in the foil
86
+
87
+ Returns:
88
+ np.ndarray: array of the number of Nb92m at different times
89
+ """
90
+ N = np.zeros_like(times)
91
+ N[0] = 0
92
+ decay_constant = Nb92m_decay_constant.to(1 / ureg.s).magnitude
93
+ cross_section = Nb93_n_2n_Nb92m_cross_section_at_14Mev.to(ureg.cm**2).magnitude
94
+ t_on = irradiations[0]["t_on"]
95
+ t_off = irradiations[0]["t_off"]
96
+ previous_irradiation_off = t_off
97
+ idx = np.where(
98
+ np.logical_and(
99
+ (times >= irradiations[0]["t_on"]), (times <= irradiations[0]["t_off"])
100
+ )
101
+ )
102
+ N[idx] = N_during_irradiation(
103
+ 0,
104
+ times[idx],
105
+ decay_constant,
106
+ neutron_flux,
107
+ cross_section,
108
+ nb_Nb93,
109
+ t_on,
110
+ mod=np,
111
+ )
112
+ current_N = N[idx][-1]
113
+
114
+ for irr in irradiations[1:]:
115
+
116
+ idx_rest = np.where((times > previous_irradiation_off) & (times <= irr["t_on"]))
117
+ N[idx_rest] = N_during_rest(
118
+ current_N,
119
+ times[idx_rest],
120
+ decay_constant,
121
+ t_0=previous_irradiation_off,
122
+ mod=np,
123
+ )
124
+
125
+ idx_irr = np.where(
126
+ np.logical_and((times > irr["t_on"]), (times <= irr["t_off"]))
127
+ )
128
+ N[idx_irr] = N_during_irradiation(
129
+ N[idx_rest][-1],
130
+ times[idx_irr],
131
+ decay_constant,
132
+ neutron_flux,
133
+ cross_section,
134
+ nb_Nb93,
135
+ t_0=irr["t_on"],
136
+ mod=np,
137
+ )
138
+ previous_irradiation_off = irr["t_off"]
139
+ current_N = N[idx_irr][-1]
140
+
141
+ if times[-1] not in irradiations[-1].values():
142
+ idx_rest = np.where((times > previous_irradiation_off))
143
+ N[idx_rest] = N_during_rest(
144
+ current_N,
145
+ times[idx_rest],
146
+ decay_constant,
147
+ t_0=previous_irradiation_off,
148
+ mod=np,
149
+ )
150
+ return N
151
+
152
+
153
+ if __name__ == "__main__":
154
+
155
+ irradiations = [
156
+ {"t_on": 0, "t_off": 12 * 3600},
157
+ {"t_on": 24 * 3600, "t_off": 36 * 3600},
158
+ ]
159
+
160
+ times = np.linspace(0, 48 * 3600, 1000)
161
+ neutron_flux_val = 3.51e9 * ureg["1/(cm^2 h)"] # 1/(cm^2 h)
162
+ nb_Nb93 = 1.78e21
163
+ N = get_number_ofNb92m_numpy(
164
+ irradiations, times, neutron_flux_val.to(ureg["1/(cm^2 s)"]).magnitude, nb_Nb93
165
+ )
166
+ plt.plot(times / 3600, N)
167
+
168
+ N_analytical = get_number_of_Nb92m_after_last_irradiations(irradiations)
169
+
170
+ N_analytical = (
171
+ N_analytical.subs(
172
+ cross_section,
173
+ Nb93_n_2n_Nb92m_cross_section_at_14Mev.to(ureg.cm**2).magnitude,
174
+ )
175
+ .subs(decay_constant, Nb92m_decay_constant.to(1 / ureg.s).magnitude)
176
+ .subs(neutron_flux, neutron_flux_val.to(ureg["1/(cm^2 s)"]).magnitude)
177
+ .subs(number_of_Nb93, nb_Nb93)
178
+ )
179
+
180
+ plt.scatter([irradiations[-1]["t_off"] / 3600], [N_analytical], color="red")
181
+ plt.xlabel("Time (h)")
182
+ plt.ylabel("Number of Nb92m")
183
+
184
+ for irr in irradiations:
185
+ plt.gca().axvspan(
186
+ irr["t_on"] / 3600, irr["t_off"] / 3600, alpha=0.2, color="red"
187
+ )
188
+ plt.show()
@@ -0,0 +1,39 @@
1
+ import datetime
2
+
3
+ def get_decay_lines(nuclides:list[str])->dict:
4
+ """ Creates dictionary of check source data
5
+ given a list of check source nuclides. """
6
+ # energy is the gamma energy in units of eV
7
+ # intensity is the percentage of decays that result in this energy gamma
8
+ all_decay_lines = {'Ba133':{'energy':[80.9979, 276.3989, 302.8508, 356.0129, 383.8485],
9
+ 'intensity':[0.329, 0.0716, 0.1834, 0.6205, 0.0894],
10
+ 'half_life':[10.551*365.25*24*3600],
11
+ 'activity_date':datetime.date(2014, 3, 19),
12
+ 'activity':1 * 3.7e4},
13
+ 'Co60':{'energy':[1173.228, 1332.492],
14
+ 'intensity':[0.9985, 0.999826],
15
+ 'half_life':[1925.28*24*3600],
16
+ 'actvity_date':datetime.date(2014, 3, 19),
17
+ 'activity':0.872 * 3.7e4},
18
+ 'Na22':{'energy':[511, 1274.537],
19
+ 'intensity':[1.80, 0.9994],
20
+ 'half_life':[2.6018*365.25*24*3600],
21
+ 'actvity_date':datetime.date(2014, 3, 19),
22
+ 'activity': 5 * 3.7e4},
23
+ 'Cs137':{'energy':[661.657],
24
+ 'intensity':[0.851],
25
+ 'half_life':[30.08*365.25*24*3600],
26
+ 'actvity_date':datetime.date(2014, 3, 19),
27
+ 'activity':4.66 * 3.7e4},
28
+ 'Mn54':{'energy':[834.848],
29
+ 'intensity':[0.99976],
30
+ 'half_life':[312.20*24*3600],
31
+ 'actvity_date':datetime.date(2016, 5, 2),
32
+ 'activity':6.27 * 3.7e4}}
33
+ decay_lines = {}
34
+ for nuclide in nuclides:
35
+ if nuclide in all_decay_lines.keys():
36
+ decay_lines[nuclide] = all_decay_lines[nuclide]
37
+ else:
38
+ raise ValueError(f'{nuclide} not yet added to get_decay_lines()')
39
+ return decay_lines
@@ -0,0 +1,26 @@
1
+
2
+
3
+ def get_channel(filename):
4
+ """
5
+ Extract the channel number from a given filename string.
6
+
7
+ Parameters
8
+ ----------
9
+ filename : str
10
+ The input filename string containing the channel information.
11
+ Should look something like : "Data_CH<channel_number>@V...CSV"
12
+
13
+ Returns
14
+ -------
15
+ int
16
+ The extracted channel number.
17
+
18
+ Example
19
+ -------
20
+ >>> get_channel("Data_CH4@V1725_292_Background_250322.CSV")
21
+ 4
22
+ """
23
+ return int(filename.split('@')[0][7:])
24
+
25
+
26
+
@@ -0,0 +1,110 @@
1
+ from .settings import *
2
+ from .calculations import n93_number, delay_time
3
+ import numpy as np
4
+
5
+
6
+ def get_chain(irradiations):
7
+ """
8
+ Returns the value of
9
+ (1 - exp(-\lambda * \Delta t_1)) * (1 - exp(-\lambda * \Delta t_2)) * ... * (1 - exp(-\lambda * \Delta t_n))
10
+ where \Delta t_i is the time between the end of the i-th period (rest or irradiation) and the start of the next one
11
+
12
+ Args:
13
+ irradiations (list): list of dictionaries with keys "t_on" and "t_off" for irradiations
14
+
15
+ Returns:
16
+ float or pint.Quantity: the value of the chain
17
+ """
18
+ result = 1
19
+ periods = [{"start": irradiations[0]["t_on"], "end": irradiations[0]["t_off"]}]
20
+ for irr in irradiations[1:]:
21
+ periods.append({"start": periods[-1]["end"], "end": irr["t_on"]})
22
+ periods.append({"start": irr["t_on"], "end": irr["t_off"]})
23
+
24
+ for period in periods:
25
+ delta_t = period["end"] - period["start"]
26
+ result = 1 - result * np.exp(-Nb92m_decay_constant * delta_t)
27
+ return result
28
+
29
+
30
+ def get_neutron_flux(experiment: dict, irradiations: list):
31
+ """calculates the neutron flux during the irradiation
32
+
33
+ Args:
34
+ experiment (dict): dictionary containing the experiment data
35
+ irradiations (list): list of dictionaries with keys "t_on" and "t_off" for irradiations
36
+
37
+ Returns:
38
+ pint.Quantity: neutron flux
39
+ """
40
+ overall_efficiency = (
41
+ (geometric_efficiency * nal_gamma_efficiency * branching_ratio)
42
+ * ureg.count
43
+ / ureg.particle
44
+ )
45
+ number_of_Nb92m_decays_measured = experiment["photon_counts"] / overall_efficiency
46
+
47
+ number_of_Nb92m_decays_measured *= 0.5 # times by 0.5 because of double counting
48
+
49
+ flux = (
50
+ number_of_Nb92m_decays_measured
51
+ / n93_number(experiment["foil_mass"])
52
+ / Nb93_n_2n_Nb92m_cross_section_at_14Mev
53
+ )
54
+
55
+ flux *= get_chain(irradiations) ** -1
56
+ time_between_generator_off_and_start_of_counting = delay_time(
57
+ experiment["time_generator_off"], experiment["start_time_counting"]
58
+ )
59
+ flux *= (
60
+ -1
61
+ / Nb92m_decay_constant
62
+ * (
63
+ np.exp(
64
+ -Nb92m_decay_constant
65
+ * (
66
+ time_between_generator_off_and_start_of_counting
67
+ + experiment["counting_time"]
68
+ )
69
+ )
70
+ - np.exp(
71
+ -Nb92m_decay_constant * time_between_generator_off_and_start_of_counting
72
+ )
73
+ )
74
+ ) ** -1
75
+
76
+ # convert n/cm2/s to n/s
77
+ area_of_sphere = 4 * np.pi * experiment["distance_from_center_of_target_plane"] ** 2
78
+
79
+ flux *= area_of_sphere
80
+
81
+ return flux
82
+
83
+
84
+ def get_neutron_flux_error(experiment: dict):
85
+ """
86
+ Returns the uncertainty of the neutron flux as a pint.Quantity
87
+
88
+ Args:
89
+ experiment (dict): dictionary containing the experiment data
90
+ irradiations (list): list of dictionaries with keys "t_on" and "t_off" for irradiations
91
+
92
+ Returns:
93
+ pint.Quantity: uncertainty of the neutron flux
94
+ """
95
+ error_counts = experiment["photon_counts_uncertainty"] / experiment["photon_counts"]
96
+ error_mass = 0.0001 * ureg.g / experiment["foil_mass"]
97
+ error_geometric_eff = 0.025 / geometric_efficiency
98
+ error_intrinsic_eff = 0.025 / nal_gamma_efficiency
99
+
100
+ error = np.sqrt(
101
+ error_counts**2
102
+ + error_mass**2
103
+ + error_geometric_eff**2
104
+ + error_intrinsic_eff**2
105
+ )
106
+ return error.to(ureg.dimensionless).magnitude
107
+
108
+
109
+ if __name__ == "__main__":
110
+ pass
@@ -0,0 +1,17 @@
1
+ import pint
2
+ import numpy as np
3
+
4
+ ureg = pint.UnitRegistry()
5
+
6
+ geometric_efficiency = 0.5
7
+ nal_gamma_efficiency = 0.344917296922981
8
+ branching_ratio = 0.999
9
+
10
+
11
+ # source: https://scipub.euro-fusion.org/wp-content/uploads/eurofusion/WPJET3PR17_16948_submitted.pdf
12
+ # See Figure 3
13
+ Nb93_n_2n_Nb92m_cross_section_at_14Mev = 0.46 * ureg.barn
14
+
15
+ # source https://gammaray.inl.gov/SiteAssets/catalogs/ge/pdf/nb92m.pdf
16
+ Nb92m_half_life = 10.25 * ureg.day
17
+ Nb92m_decay_constant = np.log(2) / Nb92m_half_life
@@ -0,0 +1,2 @@
1
+ from . import process_data
2
+ from . import prt
@@ -0,0 +1,128 @@
1
+ import numpy as np
2
+
3
+
4
+ class DataProcessor:
5
+ """
6
+ A class for reading and processing data from diamond detectors as text files
7
+
8
+ Attributes:
9
+ files (list of str): List of text filenames that have been read
10
+ time_values (np.array): Array of time values from all files
11
+ energy_values (np.array): Array of energy values from all files
12
+ """
13
+
14
+ def __init__(self) -> None:
15
+ self.files = []
16
+
17
+ self.time_values = np.array([])
18
+ self.energy_values = np.array([])
19
+
20
+ def add_file(
21
+ self,
22
+ filename: str,
23
+ time_column: int,
24
+ energy_column: int,
25
+ scale_time: bool = True,
26
+ **kwargs,
27
+ ):
28
+ """
29
+ Adds a file to the data processor, reading the time and energy values from the file
30
+ and appending them to the existing data (``time_values`` and ``energy_values`` attributes).
31
+
32
+ Args:
33
+ filename (str): the name of the file to read
34
+ time_column (int): the column index of the time values in the file
35
+ energy_column (int): the column index of the energy values in the file
36
+ scale_time (bool, optional): if True, the time values are scaled from ps to s. Defaults to True.
37
+ """
38
+ self.files.append(filename)
39
+
40
+ # Should we store the data for each file separately too?
41
+ data = np.genfromtxt(filename, **kwargs)
42
+
43
+ time_values = data[:, time_column]
44
+ if scale_time:
45
+ # convert times from ps to s
46
+ time_values *= 1 / 1e12
47
+ energy_values = data[:, energy_column]
48
+
49
+ # Append time and energy values to the list
50
+ self.time_values = np.concatenate((self.time_values, time_values))
51
+ self.energy_values = np.concatenate((self.energy_values, energy_values))
52
+
53
+ # sort time and energy values
54
+ inds = np.argsort(self.time_values)
55
+ self.time_values = np.array(self.time_values)[inds]
56
+ self.energy_values = np.array(self.energy_values)[inds]
57
+
58
+ print(f"Added file: {filename} containing {len(time_values)} events")
59
+
60
+ def get_count_rate(self, bin_time: float, energy_window: tuple = None):
61
+ """
62
+ Calculate the count rate in a given time bin for the
63
+ time values stored in the data processor.
64
+
65
+ Args:
66
+ bin_time (float): the time bin width in seconds
67
+ energy_window (tuple, optional): If provided, the rate
68
+ will be computed only on this energy window. Defaults to None.
69
+
70
+ Returns:
71
+ np.array: Array of count rates (counts per second)
72
+ np.array: Array of time bin edges (in seconds)
73
+ """
74
+ time_values = self.time_values.copy()
75
+ energy_values = self.energy_values.copy()
76
+
77
+ time_bins = np.arange(time_values.min(), time_values[-2], bin_time)
78
+
79
+ if energy_window is not None:
80
+ peak_mask = (energy_values > energy_window[0]) & (
81
+ energy_values < energy_window[1]
82
+ )
83
+ time_values = time_values[peak_mask]
84
+
85
+ count_rates, count_rate_bins = np.histogram(time_values, bins=time_bins)
86
+ count_rates = count_rates / bin_time
87
+
88
+ return count_rates, count_rate_bins
89
+
90
+ def get_avg_rate(self, t_min: float, t_max: float, energy_window: tuple = None):
91
+ """
92
+ Calculate the average count rate in a given time window for the
93
+ time values stored in the data processor.
94
+ Similar to ``get_count_rate`` but returns a single value for a time window.
95
+
96
+ Args:
97
+ t_min (float): start time of the time window
98
+ t_max (float): end time of the time window
99
+ energy_window (tuple, optional): If provided, the rate
100
+ will be computed only on this energy window. Defaults to None.
101
+
102
+ Returns:
103
+ float: Average count rate (counts per second)
104
+ float: Error on the average count rate (counts per second)
105
+ """
106
+ time_values = self.time_values.copy()
107
+ energy_values = self.energy_values.copy()
108
+
109
+ if energy_window is not None:
110
+ peak_mask = (energy_values > energy_window[0]) & (
111
+ energy_values < energy_window[1]
112
+ )
113
+ time_values = time_values[peak_mask]
114
+
115
+ # Create mask to only count pulses of any energy in section time window
116
+ idx = np.logical_and(time_values > t_min, time_values < t_max)
117
+
118
+ counts = len(time_values[idx])
119
+ error = np.sqrt(len(time_values[idx]))
120
+ delta_t = t_max - t_min
121
+ count_rate = counts / delta_t
122
+ count_rate_err = error / delta_t
123
+
124
+ return count_rate, count_rate_err
125
+
126
+
127
+ if __name__ == "__main__":
128
+ pass