Skip to content

Commit f7bcd10

Browse files
committed
Added Experiment class
1 parent 347a17d commit f7bcd10

File tree

2 files changed

+120
-74
lines changed

2 files changed

+120
-74
lines changed

libra_toolbox/neutron_detection/activation_foils/explicit.py

Lines changed: 40 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,24 @@
11
from settings import *
22
import foils
3-
from .calculations import n93_number, delay_time
3+
from calculations import n93_number, delay_time
44
import numpy as np
55
import pandas as pd
66
import os
7+
import warnings
78

89

9-
# def get_foil_data(foil: dict,
10-
# filepath=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'nuclide_data.xlsx'),
11-
# xslib='EAF2010'):
12-
13-
# # Read in nuclide data
14-
# df = pd.read_excel(filepath, skiprows=1)
15-
16-
# # Only get info for one nuclide
17-
# mask = df['Nuclide'] == foil['nuclide']
18-
19-
# # Calculate number of nuclide atoms in foil
20-
# num_element = (foil['mass']
21-
# / (df['Element_Atomic_Mass'][mask].item() * ureg.g / ureg.mol)
22-
# * (6.022e23 * ureg.particle / ureg.mol)
23-
# )
24-
25-
# foil['number'] = num_element * df['Natural_Abundance'][mask].item()
26-
27-
# # Get density and mass attenuation coefficient
28-
# foil['density'] = df['Density'][mask].item() * ureg.g / ureg.cm**3
29-
# foil['mass_attenuation_coefficient'] = df['Mass_Attenuation_Coefficient'][mask].item() * (ureg.cm**2/(ureg.g))
30-
31-
# # Get cross section for available reactions
32-
# for reaction in foil['reactions'].keys():
33-
# heading = reaction + '_' + xslib + '_xs'
34-
# if heading in df.keys():
35-
# foil['reactions'][reaction]['cross_section'] = df[heading][mask].item() * ureg.barn
10+
def get_chain(irradiations, decay_constant):
3611

37-
# return foil
38-
39-
40-
41-
def get_chain(irradiations, decay_constant=Nb92m_decay_constant):
42-
"""
43-
Returns the value of
12+
""" Returns the value of
4413
(1 - exp(-\lambda * \Delta t_1)) * (1 - exp(-\lambda * \Delta t_2)) * ... * (1 - exp(-\lambda * \Delta t_n))
4514
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
4615
4716
Args:
4817
irradiations (list): list of dictionaries with keys "t_on" and "t_off" for irradiations
4918
5019
Returns:
51-
float or pint.Quantity: the value of the chain
52-
"""
20+
float or pint.Quantity: the value of the chain """
21+
5322
result = 1
5423
periods = [{"start": irradiations[0]["t_on"], "end": irradiations[0]["t_off"]}]
5524
for irr in irradiations[1:]:
@@ -61,6 +30,7 @@ def get_chain(irradiations, decay_constant=Nb92m_decay_constant):
6130
result = 1 - result * np.exp(-decay_constant * delta_t)
6231
return result
6332

33+
6434
def get_efficiency(energies=None, coeff=None,
6535
coeff_energy_bounds=None,
6636
coeff_type='total', geometric_eff=1.0):
@@ -72,23 +42,26 @@ def get_efficiency(energies=None, coeff=None,
7242
if coeff is None:
7343
# These are the efficiencies reported for activation foil analysis
7444
# of the BABY 100 mL runs
75-
geometric_efficiency = 0.5
45+
geometric_eff = 0.5
7646
intrinsic_efficiency = 0.344917296922981
7747
total_efficiency = geometric_eff * intrinsic_efficiency
48+
warnings.warn('Using NaI efficiency from BABY 100 mL runs, which may not reflect detector efficiency accurately.')
7849
else:
7950
# Check that efficiency is being interpolated between the bounds of the fit curve
80-
if energies.min() < coeff_energy_bounds.min() or energies.max() > coeff_energy_bounds.max():
81-
raise Warning('Efficiency is being extrapolated according to efficiency fit curve bounds.')
82-
if coeff_type.lower() is 'total':
83-
total_efficiency = np.polyval(coeff, energies)
84-
elif coeff_type.lower() is 'intrinsic':
85-
intrinsic_efficiency = np.polyval(coeff, energies)
51+
ergs = energies.to(ureg.keV).magnitude
52+
erg_bounds = coeff_energy_bounds.to(ureg.keV).magnitude
53+
if np.min(ergs) < np.min(erg_bounds) or np.max(ergs) > np.max(erg_bounds):
54+
warnings.warn('Efficiency is being extrapolated according to efficiency fit curve bounds.')
55+
if coeff_type.lower()=='total':
56+
total_efficiency = np.polyval(coeff, ergs)
57+
elif coeff_type.lower()=='intrinsic':
58+
intrinsic_efficiency = np.polyval(coeff, ergs)
8659
total_efficiency = geometric_eff * intrinsic_efficiency
8760
total_efficiency *= ureg.count / ureg.particle
8861
return total_efficiency
8962

9063

91-
def get_neutron_flux(experiment: dict, irradiations: list, foil: foils.Foil):
64+
def get_neutron_flux(experiment: foils.Experiment, irradiations: list, foil: foils.Foil):
9265
"""calculates the neutron flux during the irradiation
9366
Based on Equation 1 from:
9467
Lee, Dongwon, et al. "Determination of the Deuterium-Tritium (D-T) Generator
@@ -108,15 +81,17 @@ def get_neutron_flux(experiment: dict, irradiations: list, foil: foils.Foil):
10881
# experiment["time_generator_off"], experiment["start_time_counting"]
10982
# )
11083
time_between_generator_off_and_start_of_counting = (
111-
experiment["start_time_counting"] - experiment["time_generator_off"]
84+
experiment.start_time_counting - experiment.time_generator_off
11285
).seconds * ureg.second
11386

114-
if 'total_eff_coeff' in experiment.keys():
115-
total_efficiency = get_efficiency(experiment['total_eff_coeff'], foil.photon_energies,
116-
coeff_energy_bounds=experiment['efficiency_bounds'],
87+
if experiment.total_eff_coeff is not None:
88+
total_efficiency = get_efficiency(energies=foil.photon_energies,
89+
coeff=experiment.total_eff_coeff,
90+
coeff_energy_bounds=experiment.efficiency_bounds,
11791
coeff_type='total')
118-
elif 'intrinsic_eff_coeff' in experiment.keys():
119-
total_efficiency = get_efficiency(experiment['intrinsic_eff_coeff'], foil.photon_energies,
92+
elif experiment.intrinsic_eff_coeff is not None:
93+
total_efficiency = get_efficiency(energies=foil.photon_energies,
94+
coeff=experiment.intrinsic_eff_coeff,
12095
coeff_energy_bounds=experiment['efficiency_bounds'],
12196
coeff_type='intrinsic')
12297
else:
@@ -128,10 +103,11 @@ def get_neutron_flux(experiment: dict, irradiations: list, foil: foils.Foil):
128103
f_spec = total_efficiency * foil.branching_ratio
129104

130105
print('total efficiency', total_efficiency)
131-
number_of_decays_measured = experiment["photon_counts"] / f_spec
106+
number_of_decays_measured = experiment.photon_counts / f_spec
107+
132108
print('number of decays measured', number_of_decays_measured)
133-
print('number', foil.atoms)
134-
print('cross section', foil.cross_section)
109+
# print('number', foil.atoms)
110+
# print('cross section', foil.cross_section)
135111

136112

137113
flux = (
@@ -143,14 +119,11 @@ def get_neutron_flux(experiment: dict, irradiations: list, foil: foils.Foil):
143119
f_time = (get_chain(irradiations, foil.decay_constant)
144120
* np.exp( -foil.decay_constant
145121
* time_between_generator_off_and_start_of_counting)
146-
* (1 - np.exp( -foil.decay_constant * experiment['real_count_time']))
147-
* (experiment['live_count_time'] / experiment['real_count_time'])
122+
* (1 - np.exp( -foil.decay_constant * experiment.real_count_time))
123+
* (experiment.live_count_time / experiment.real_count_time)
148124
/ foil.decay_constant
149125
)
150126

151-
print('att coeff', foil['mass_attenuation_coefficient'])
152-
print('density', foil['density'])
153-
print('thickness', foil['thickness'])
154127

155128
# Correction factor of gamma-ray self-attenuation in the foil
156129
f_self = ( (1 -
@@ -162,23 +135,23 @@ def get_neutron_flux(experiment: dict, irradiations: list, foil: foils.Foil):
162135
* foil.thickness)
163136
).to('dimensionless')
164137

165-
print('flux: ', flux.to_reduced_units())
166-
print('f_time', f_time)
167-
print('f_self', f_self)
168-
169138
flux /= (f_time * f_self)
170-
171139

140+
# print('flux: ', flux.to_reduced_units())
141+
# print('f_time', f_time)
142+
# print('f_self', f_self)
172143

173144
# convert n/cm2/s to n/s
174-
area_of_sphere = 4 * np.pi * experiment["distance_from_center_of_target_plane"] ** 2
145+
area_of_sphere = 4 * np.pi * experiment.distance_from_center_of_target_plane ** 2
175146

176147
flux *= area_of_sphere
177148

149+
flux = flux.to(1/ureg.s)
150+
178151
return flux
179152

180153

181-
def get_neutron_flux_error(experiment: dict):
154+
def get_neutron_flux_error(experiment: foils.Experiment, foil: foils.Foil):
182155
"""
183156
Returns the uncertainty of the neutron flux as a pint.Quantity
184157
@@ -189,7 +162,7 @@ def get_neutron_flux_error(experiment: dict):
189162
Returns:
190163
pint.Quantity: uncertainty of the neutron flux
191164
"""
192-
error_counts = experiment["photon_counts_uncertainty"] / experiment["photon_counts"]
165+
error_counts = experiment.photon_counts_uncertainty / experiment.photon_counts
193166
error_mass = 0.0001 * ureg.g / experiment["foil_mass"]
194167
error_geometric_eff = 0.025 / geometric_efficiency
195168
error_intrinsic_eff = 0.025 / nal_gamma_efficiency

libra_toolbox/neutron_detection/activation_foils/foils.py

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,55 @@
11
import numpy as np
2+
import datetime
3+
from zoneinfo import ZoneInfo
24
from settings import ureg
35

6+
7+
def convert_to_datetime(time:str, tzinfo=ZoneInfo('America/New_York')):
8+
if isinstance(time, str):
9+
datetime_obj = datetime.datetime.strptime(time, "%m/%d/%Y %H:%M:%S")
10+
elif isinstance(time, datetime.datetime):
11+
datetime_obj = time
12+
else:
13+
raise ValueError('Time is neither a string nor a datetime.datetime object.')
14+
# Check if timezone info is included
15+
if datetime_obj.tzinfo is None:
16+
# If not, use New York time as default
17+
datetime_obj = datetime_obj.replace(tzinfo=tzinfo)
18+
return datetime_obj
19+
20+
21+
class Experiment:
22+
def __init__(self, time_generator_off,
23+
start_time_counting,
24+
distance_from_center_of_target_plane: ureg.Quantity,
25+
real_count_time: ureg.Quantity,
26+
name=None, generator=None, run=None,
27+
live_count_time=None,
28+
tzinfo=ZoneInfo('America/New_York')):
29+
# Ensure times are all datetime objects with timezones
30+
self.time_generator_off = convert_to_datetime(time_generator_off)
31+
self.start_time_counting = convert_to_datetime(start_time_counting)
32+
self.real_count_time = real_count_time
33+
self.distance_from_center_of_target_plane = distance_from_center_of_target_plane
34+
self.name = name
35+
self.generator = generator
36+
self.run = run
37+
38+
if live_count_time:
39+
self.live_count_time = live_count_time
40+
else:
41+
self.live_count_time = real_count_time
42+
self.total_eff_coeff = None
43+
self.intrinsic_eff_coeff = None
44+
self.geometric_efficiency = 1
45+
self.efficiency_bounds = np.array([])
46+
self.photon_counts = None
47+
self.photon_counts_uncertainty = None
48+
49+
50+
51+
52+
453
class Foil:
554
def __init__(self, mass: float, thickness: float, name=''):
655
"""
@@ -56,7 +105,7 @@ def __init__(self, mass: float, thickness: float, name=''):
56105
self.branching_ratio = np.array([0.9915])
57106
# branching ratio used in BABY 100 mL analysis
58107
# self.branching_ratio = [0.999]
59-
self.photon_energies = np.array([934.44])
108+
self.photon_energies = np.array([934.44]) * ureg.keV
60109

61110
# Cross section from EAF-2010 at 14 MeV
62111
self.cross_section = np.array([0.4729]) * ureg.barn
@@ -81,14 +130,17 @@ def __init__(self, mass: float, thickness: float, name=''):
81130

82131
self.reaction = ["Zr90(n,2n)Zr89",
83132
"Zr90(n,2n)Zr89"]
84-
self.branching_ratio = np.array([0.9904,
85-
0.45])
86-
self.photon_energies = np.array([909.15,
87-
511]) * ureg.keV
133+
self.branching_ratio = np.array([0.45,
134+
0.9904])
135+
self.photon_energies = np.array([511,
136+
909.15]) * ureg.keV
88137

89138
# Cross sections from EAF-2010 at 14 MeV
90139
self.cross_section = np.array([0.5068,
91140
0.5068]) * ureg.barn
141+
# Cross sections from ENDF/B-VIII.0 at 14.1 MeV
142+
# self.cross_section = np.array([0.5382,
143+
# 0.5382]) * ureg.barn
92144

93145
self.density = 6.505 * ureg.g / ureg.cm**3
94146
self.atomic_mass = 91.222 * ureg.g / ureg.mol
@@ -98,6 +150,27 @@ def __init__(self, mass: float, thickness: float, name=''):
98150
self.get_atoms()
99151

100152
#From NIST Xray Mass Attenuation Coefficients Table 3
101-
self.mass_attenuation_coefficient = np.array([0.08590,
102-
0.06156]) * ureg.cm**2 / ureg.g # at 1 MeV
153+
self.mass_attenuation_coefficient = np.array([0.06156,
154+
0.08590]) * ureg.cm**2 / ureg.g # at 1 MeV
155+
156+
157+
def convert_dict_to_foils(data:dict):
158+
if data["element"] == "Nb":
159+
foil = Niobium(mass=data["foil_mass"],
160+
thickness=0.01 * ureg.inch,
161+
name=data["foil_name"])
162+
elif data["element"] == "Zr":
163+
foil = Zirconium(mass=data["foil_mass"],
164+
thickness=0.005 * ureg.inch,
165+
name=data["foil_name"])
166+
167+
# Clean up data dictionary to not include foil class arguments
168+
experiment_dict = {}
169+
for key in data.keys():
170+
if key not in ["element", "foil_mass", "foil_name"]:
171+
experiment_dict[key] = data[key]
172+
experiment = Experiment(**experiment_dict)
173+
174+
return experiment, foil
175+
103176

0 commit comments

Comments
 (0)