From ee463c498a5ad9662c0ace29e946083eeb03a8ea Mon Sep 17 00:00:00 2001 From: Talia Weiss Date: Thu, 22 Jul 2021 19:56:40 -0400 Subject: [PATCH 01/57] Updated Cicada --- Cicada | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cicada b/Cicada index eac4c64a..99cfd85d 160000 --- a/Cicada +++ b/Cicada @@ -1 +1 @@ -Subproject commit eac4c64adbcd6412912599ee754a953d01b4a860 +Subproject commit 99cfd85d3cf0d330f82131536f53e7981b0ec84a From 7f58fd12943d2f64e4502eaeebfbe738517b29f4 Mon Sep 17 00:00:00 2001 From: Talia Weiss Date: Wed, 28 Jul 2021 16:00:11 -0400 Subject: [PATCH 02/57] Got test_analysis script to run --- .../TritiumSpectrum/FakeDataGenerator.py | 9 +++++- test_analysis/fake_data_stan_analysis.py | 29 ++++++++++++------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/mermithid/processors/TritiumSpectrum/FakeDataGenerator.py b/mermithid/processors/TritiumSpectrum/FakeDataGenerator.py index 397532df..56dc5301 100644 --- a/mermithid/processors/TritiumSpectrum/FakeDataGenerator.py +++ b/mermithid/processors/TritiumSpectrum/FakeDataGenerator.py @@ -67,6 +67,7 @@ def InternalConfigure(self, params): self.Kmin = reader.read_param(params, 'Kmin', self.Q-self.m-2300) #Energy corresponding to lower bound of frequency ROI (eV) self.Kmax = reader.read_param(params, 'Kmax', self.Q-self.m+1000) #Same, for upper bound (eV) self.minf = reader.read_param(params, 'minf', 25.8e+9) #Minimum frequency + self.maxf = reader.read_param(params, 'maxf', None) if self.Kmax <= self.Kmin: logger.error("Kmax <= Kmin!") return False @@ -112,9 +113,11 @@ def InternalConfigure(self, params): # get file content if needed # get efficiency dictionary if self.apply_efficiency: + print("00000") self.efficiency_dict = self.load_efficiency_curve() np.random.seed() else: + print("11111") self.efficiency_dict = None # generate data with lineshape @@ -146,6 +149,7 @@ def InternalRun(self): else: ROIbound = [self.Kmin, self.Kmax] + print(self.efficiency_dict) Kgen = self.generate_unbinned_data(self.Q, self.m, ROIbound, self.S, self.B_1kev, @@ -216,7 +220,10 @@ def generate_unbinned_data(self, Q_mean, mass, ROIbound, S, B_1kev, nsteps=10**4 if efficiency_dict is not None: maxf = max(efficiency_dict['frequencies']) else: - maxf = max(self.load_efficiency_curve()['frequencies']) + if self.maxf is None: + maxf = max(self.load_efficiency_curve()['frequencies']) + else: + maxf = self.maxf Kmax, Kmin = Energy(minf, B_field), Energy(maxf, B_field) else: Kmin, Kmax = ROIbound[0], ROIbound[1] diff --git a/test_analysis/fake_data_stan_analysis.py b/test_analysis/fake_data_stan_analysis.py index 00ebdce0..fa8e92f4 100644 --- a/test_analysis/fake_data_stan_analysis.py +++ b/test_analysis/fake_data_stan_analysis.py @@ -40,6 +40,7 @@ aposterioriPlotter = APosterioriDistribution("posterioriDistrib") divPlotter = Histo2dDivergence("2dDivergence") +Bfield = 0.9574762 def DefineGeneratorInputs(root_file='./results/tritium_analysis.root'): """ @@ -52,6 +53,7 @@ def DefineGeneratorInputs(root_file='./results/tritium_analysis.root'): "fixed_inputs": { 'Nscatters': 20, 'minf': 1353.125e+06 - 40e+06 + 24.5e+09, #In Hz + 'maxf': 1353.125e+06 + 2*40e+06 + 24.5e+09, 'err_from_B': 0. }, "priors": [ @@ -61,7 +63,7 @@ def DefineGeneratorInputs(root_file='./results/tritium_analysis.root'): {'name': 'S', 'prior_dist': 'poisson', 'prior_params': [3300.]}, {'name': 'B_1kev', 'prior_dist': 'lognormal', 'prior_params': [-3.826151398234498, 2.1673316326073935]}, {'name': 'survival_prob', 'prior_dist': 'beta', 'prior_params': [55, 17]}, #Centered approximately around 0.77. To be replaced given complex lineshape result/systematic assessment - {'name': 'Bfield', 'prior_dist': 'normal', 'prior_params': [0.9574762, 1.51e-6]}, #From complex lineshape fit to calibration data. More sig figs on mean needed? + {'name': 'Bfield', 'prior_dist': 'normal', 'prior_params': [Bfield, 1.51e-6]}, #From complex lineshape fit to calibration data. More sig figs on mean needed? ] } @@ -72,6 +74,7 @@ def DefineGeneratorInputs(root_file='./results/tritium_analysis.root'): "variables": [ {"variable": "Nscatters", "type":"int"}, {"variable": "minf", "type": "float"}, + {"variable": "maxf", "type": "float"}, {"variable": "err_from_B", "type": "float"}, {"variable": "Q", "type": "float"}, {"variable": "mass", "type": "float"}, @@ -112,12 +115,13 @@ def GenerateFakeData(inputs_dict): """ specGen_config = { "apply_efficiency": True, - "efficiency_path": "../phase2_detection_efficiency_curve/combined_energy_corrected_count_rates/combined_energy_corrected_eff_at_quad_trap_frequencies.json", + "efficiency_path": "../tests/combined_energy_corrected_eff_at_quad_trap_frequencies.json", "detailed_or_simplified_lineshape": "detailed", "return_frequency": True, "Q": inputs_dict["Q"], "mass": inputs_dict["mass"], "minf": inputs_dict["minf"], + "maxf": inputs_dict["maxf"], "scattering_sigma": inputs_dict["sigma"], "S": inputs_dict["S"], "B_1kev": inputs_dict["B_1kev"], @@ -126,6 +130,7 @@ def GenerateFakeData(inputs_dict): "Nscatters": inputs_dict["Nscatters"], "B_field": inputs_dict["Bfield"], "n_steps": 100000, + "use_lineshape": False } histo_config = { @@ -154,7 +159,7 @@ def BinAndSaveData(tritium_data, nbins, root_file="./results/tritium_analysis.ro "energy_or_frequency": 'frequency', "variables": "F", "title": "corrected_spectrum", - "efficiency_filepath": "../phase2_detection_efficiency_curve/combined_energy_corrected_count_rates/combined_energy_corrected_eff_at_quad_trap_frequencies.json", + "efficiency_filepath": "../tests/combined_energy_corrected_eff_at_quad_trap_frequencies.json", 'bins': np.linspace(tritium_data['minf'], tritium_data['maxf'], nbins), 'fss_bins': False # If fss_bins is True, bins is ignored and overridden } @@ -210,7 +215,7 @@ def SaveUnbinnedData(tritium_data, root_file="./results/tritium_analysis.root"): -def StanTritiumAnalysis(tritium_data, fit_parameters=None, root_file='./results/tritium_analysis.root', stan_files_location='../../morpho_models/', model_code='tritium_model/models/tritium_phase_II_analyzer_binned.stan', scattering_params_R='simplified_scattering_params.R'): +def StanTritiumAnalysis(tritium_data, fit_parameters=None, root_file='./results/tritium_analysis.root', stan_files_location='./', model_code='main_model/tritium_phase_II_analyzer_binned.stan', scattering_params_R='simplified_scattering_params.R'): """ Analyzes frequency or kinetic energy data using a Stan model. Saves and plots posteriors. @@ -241,14 +246,14 @@ def StanTritiumAnalysis(tritium_data, fit_parameters=None, root_file='./results/ "cache_dir": stan_files_location+"tritium_model/cache", "warmup": 4000, #Increase for real run (to 3000-5000) "iter": 8000, #Increase for real run (to 6000-9000) - "chain": 3, #Increase for real run (to 3-4) + "chain": 1, #Increase for real run (to 3-4) "control": {'adapt_delta':0.97}, "init": { "sigma": 17.7084, "survival_prob": 0.77, "Q": 18573.24, "mass": 0.2, - "Bfield": 0.9574762, + "Bfield": Bfield, "S": 3300., "B_1kev": 0.0217933282798889, "B": 0.3, @@ -257,7 +262,7 @@ def StanTritiumAnalysis(tritium_data, fit_parameters=None, root_file='./results/ "sigma_ctr": 17.7084, #sigma params from Ali's complex lineshape fits. "sigma_std": 1.14658, #Final states not yet included. "err_from_B": 0.001, #Tiny smearing from f_c->K conversion - "Bfield_ctr": 0.9574762, #From complex lineshape fit to calibration + "Bfield_ctr": Bfield, #From complex lineshape fit to calibration "Bfield_std": 1.51e-06, #data. More sig figs on mean needed? "survival_prob_alpha": 55, #Centered around ~0.77. To be replaced "survival_prob_beta": 17, #given complex lineshape result+systematics @@ -270,7 +275,9 @@ def StanTritiumAnalysis(tritium_data, fit_parameters=None, root_file='./results/ "KEscale": 16323, #This enables the option of cmdstan running # "slope": 0.000390369173, #For efficiency modeling with unbinned data # "intercept": -6.00337656, - "Nscatters": 16 #Because peaks>16 in simplified linesahpe have means->inf as FWHM->0 + "Nscatters": 16, #Because peaks>16 in simplified linesahpe have means->inf as FWHM->0 + "slope": 1, + "intercept": 20380.5153 }, "interestParams": ['Q', 'mass', 'survival_prob', 'Bfield', 'sigma', 'S', 'B_1kev', 'KEmin', 'KE_sample', 'Nfit_signal', 'Nfit_bkgd'], } @@ -410,7 +417,7 @@ def CalibrateResults(root_filenames, vars_to_calibrate, cred_interval=[0.05, 0.9 calibrator.Run() -def FakeExperimentEnsemble(n_runs, root_basename, wait_before_runs=0, parallelize=True, n_processes=4, vars_to_calibrate=['Q']): +def FakeExperimentEnsemble(n_runs, root_basename, wait_before_runs=0, parallelize=True, n_processes=1, vars_to_calibrate=['Q']): """ To-do: add parallelization option for a Slurm environment. @@ -438,8 +445,8 @@ def FakeExperimentEnsemble(n_runs, root_basename, wait_before_runs=0, paralleliz with Pool(n_processes) as p: p.map(PerformFakeExperiment, root_filenames) - coverages = CalibrateResults(root_filenames, vars_to_calibrate) - + #coverages = CalibrateResults(root_filenames, vars_to_calibrate) + coverages = None return coverages From 7bb2abf1ffeeb02bfd23d076b7c1af966a4553bd Mon Sep 17 00:00:00 2001 From: Talia Weiss Date: Mon, 14 Nov 2022 17:42:25 -0500 Subject: [PATCH 03/57] Started adding atomic-specific constants --- mermithid/misc/SensitivityFormulas.py | 11 +++++++++++ .../Sensitivity/SensitivityCurveProcessor.py | 17 +++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/mermithid/misc/SensitivityFormulas.py b/mermithid/misc/SensitivityFormulas.py index 3f8c56c6..925180bc 100644 --- a/mermithid/misc/SensitivityFormulas.py +++ b/mermithid/misc/SensitivityFormulas.py @@ -36,6 +36,17 @@ gyro_mag_ratio_proton = 42.577*MHz/T +#Atomic tritium-related constant +ideal_gas_molar_volume = 2.24e-02*m**3 #Volume of 1mol of ideal gas at 1atm. Higher pressure --> smaller volume; higher temp --> lower volume. +#Cross sections are from Berlinsky +H_He_cross_sec_high_temp = 2.00e-19*m**2 #"High temp" means >5K +H_He_cross_sec_low_temp = 3.00e-20*m**2 +#H_H_cross_sec = +#H-H2 (289 pm kinetic diameter: Wikipedia) +#18.6-keV electrons on atomic T +#18.6-keV electrons on molecular T23.00E-20 + + # units that do not show up in numericalunits # missing pre-factors fW = W*1e-15 diff --git a/mermithid/processors/Sensitivity/SensitivityCurveProcessor.py b/mermithid/processors/Sensitivity/SensitivityCurveProcessor.py index d7246769..dec1bf7e 100644 --- a/mermithid/processors/Sensitivity/SensitivityCurveProcessor.py +++ b/mermithid/processors/Sensitivity/SensitivityCurveProcessor.py @@ -122,6 +122,11 @@ def InternalRun(self): self.create_plot() + if self.sens_main_is_atomic: + num_atoms_per_particle = 1 + else: + num_atoms_per_particle = 2 + # add second and third x axis for track lengths if self.track_length_axis: self.add_track_length_axis() @@ -165,15 +170,15 @@ def InternalRun(self): rho_opt = self.rhos[self.opt_ref] logger.info('Main curve (veff = {} cm**3, rho = {} /m**3):'.format(self.sens_main.Experiment.v_eff/(cm**3), rho_opt*(m**3))) - logger.info('Sensitivity limit: {}'.format(self.sens_main.CL90(Experiment={"number_density": rho_opt})/eV)) - logger.info('T2 in Veff: {}'.format(rho_opt*self.sens_main.Experiment.v_eff)) - logger.info('Total signal: {}'.format(rho_opt*self.sens_main.Experiment.v_eff* + logger.info('Neutrino mass 90% C.L. : {}eV'.format(self.sens_main.CL90(Experiment={"number_density": rho_opt})/eV)) + logger.info('Number of tritium particles in Veff at optimal density: {}'.format(rho_opt*self.sens_main.Experiment.v_eff)) + logger.info('Total number of signal events: {}'.format(rho_opt*self.sens_main.Experiment.v_eff* self.sens_main.Experiment.LiveTime/ - self.sens_main.tau_tritium*2)) - logger.info('Signal in last eV: {}'.format(self.sens_main.last_1ev_fraction*eV**3* + self.sens_main.tau_tritium*num_atoms_per_particle)) + logger.info('Total number of signal events in last eV: {}'.format(self.sens_main.last_1ev_fraction*eV**3* rho_opt*self.sens_main.Experiment.v_eff* self.sens_main.Experiment.LiveTime/ - self.sens_main.tau_tritium*2)) + self.sens_main.tau_tritium*num_atoms_per_particle)) return True From 40077a4db86d8e842ea740392c09576629c635fd Mon Sep 17 00:00:00 2001 From: taliaweiss Date: Wed, 2 Jul 2025 10:12:56 -0400 Subject: [PATCH 04/57] Re-merged hotfix/1.2.3 so I can delete the branch --- Cicada | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cicada b/Cicada index 99cfd85d..eac4c64a 160000 --- a/Cicada +++ b/Cicada @@ -1 +1 @@ -Subproject commit 99cfd85d3cf0d330f82131536f53e7981b0ec84a +Subproject commit eac4c64adbcd6412912599ee754a953d01b4a860 From 6aa63257e48e4f5a0c28b1f64680550a84be204b Mon Sep 17 00:00:00 2001 From: taliaweiss Date: Sat, 30 Aug 2025 21:18:08 -0400 Subject: [PATCH 05/57] Update README.md with info on getting submodules Making sure people know to run "git submodule update --init --recursive" --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index cac850c4..3cf109cf 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ You will need to install via a package manager (such as apt-get): ## Installation +After cloning mermithid, run `git submodule update --init --recursive`. + These are two possible ways of installing and working with mermithid. ### Virtual environment installation From 7c6f25125d0ee8b09a300d355d32eb97bb9f04d2 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Tue, 13 Jan 2026 13:20:55 -0500 Subject: [PATCH 06/57] Update docker-compose and sensitivity configuration documentation --- docker-compose.yaml | 22 ++++++++++++++----- .../sensitivity_configurable_parameters.rst | 12 ++++++---- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 3b701007..d019511f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,9 +1,21 @@ -version: "3" +#version: "3.9" +#Check running containers docker ps (detached) -d +#Attach to container docker attach [name] services: mermithid: - build: . - command: "/bin/bash" + image: mermithid:sensitivity_branch + container_name: mermithid_sensitivity + stdin_open: true # Interactive search + tty: true # Allocate TTY volumes: - # share a subdirectory from the host to /host in the docker (can be edited) - - ~/mermithid_share:/host + - ~/repos/mermithid:/mermithid + - ~/repos/mermithid_share:/host_data + - ~/repos/termite:/termite + command: > + bash -c " + source /usr/local/p8/mermithid/v1.2.3/setup.sh && + source /usr/local/p8/mermithid/v1.2.3/bin/this_phylloxera.sh && + source /usr/local/p8/mermithid/v1.2.3/bin/this_cicada.sh && + exec bash + " diff --git a/documentation/sensitivity_configurable_parameters.rst b/documentation/sensitivity_configurable_parameters.rst index d461fb22..44b792b8 100644 --- a/documentation/sensitivity_configurable_parameters.rst +++ b/documentation/sensitivity_configurable_parameters.rst @@ -7,6 +7,7 @@ The sensitivity calculation is configured using a configuration file. The config Our main goal is the calcualtion of sensitivity in a cavity experiment. The configurations below are to be used for the CavitySensitivity class in https://github.com/project8/mermithid/blob/feature/sensitivity_curve/mermithid/misc/SensitivityCavityFormulas.py This class is used by the CavitySensitivityCurveProcessor and the SensitivityParameterScanProcessor. +More information regarding parameter choices can be found in the Pre-CDR Structure of a config file -------------------------- @@ -15,6 +16,7 @@ Configuration files have several sections: * Experiment * Efficiency +* Threshold * FrequencyExtraction * DopplerBroadening * MagneticField @@ -39,7 +41,7 @@ Below is a list of the parameters with a short description of what role they pla * ``number_density``: The gas number density together with the total volume, livetime, and the efficiency determines the statistical power of the experiment. Gas density also determines the track length and therefore the frequency resolution. The sensitivity curve processor can optimize this parameter to maximize the sensitivity. In that case this number is overwritten in the calculation. * ``sri_factor``: The statistical rate increase factor articifially increases the number of observed events (it multiplies the total efficiency). It is highly recommended to set it to 1. * ``atomic``: If true, the calculation is done for atomic tritium. If false, moecular tritium is assumed. This affects the number of decays per gas molecule/atom (2 for molecular 1 for atomic), the track length in a given gas density (via electron scattering cross section), and the width of the final ground state. - +* ``active gas fraction``: The fraction of the gas within the CRES volume that's tritium. Very high (~1) in at atomic experiment. Lower in a molecular experiment due to presence of HT, H2, DT, and 3He, and possibly other gasses, too. **Efficiency** @@ -47,6 +49,9 @@ Below is a list of the parameters with a short description of what role they pla * ``fixed_efficiency``: For example, set to roughly 2% for a 88deg minimum trapped pitch angle, assuming 100% detection efficiency of the trapped angles. * ``radial_efficiency``: Typically set to 0.67 from a calcualtion done for a 325MHz cavity with Halbach bite and radial cut on power of > 0.5 * maximum power. * ``detection_efficiency``: Fraction of events that is not detected. +* ``unusable_dist_from_wall``: Cavity radius taken up by non-uniform field (The greater value between the Larmor radius and ioffe bite distance). + +**Threshold** **FrequencyExtraction** @@ -75,10 +80,9 @@ We use the CRLB for calculating the frequency resolution. The CRLB is calculated * ``usefixedvalue``: If True ``default_systematic_smearing`` and ``default_systematic_uncertainty`` are used. * ``default_systematic_smearing``: Default systematic broadening for this category. Units must be eV. * ``default_systematic_uncertainty``: Default systematic uncertainty for this category. Units must be eV. -* ``gas_temperature``: Temperature of the source gas. This should only be different from the cavity temperature if the gas is not in thermal equilibrium with the cavity. The gas temperature is used to calculate the Doppler broadening. +* ``gas_temperature``: Temperature of the source gas. This should only be different from the cavity temperature if the gas is not in thermal equilibrium with the cavity. The gas temperature is used to calculate the Doppler broadening. In the molecular case, the molecules are in thermal equilibrium with the wall (85 K), but in the atomic case they are not, because they are prevented from contacting the wall by the Ioffe trap (4 mK - trapped gas temperature not wall temperature of 4 K) * ``gas_temperature_uncertainty``: Absolute uncertainty of the gas temperature. -* ``fraction_uncertainty_on_doppler_broadening``: Fractional uncertainty on the Doppler broadening. - +* ``fraction_uncertainty_on_doppler_broadening``: Fractional uncertainty on the Doppler broadening. **MagneticField** From bc209bb9f588429a0676cfd59be25eb3c0ec252b Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Tue, 13 Jan 2026 13:41:00 -0500 Subject: [PATCH 07/57] Update sensitivity configuratio documentation --- documentation/sensitivity_configurable_parameters.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/sensitivity_configurable_parameters.rst b/documentation/sensitivity_configurable_parameters.rst index 44b792b8..93a1e8b4 100644 --- a/documentation/sensitivity_configurable_parameters.rst +++ b/documentation/sensitivity_configurable_parameters.rst @@ -45,7 +45,7 @@ Below is a list of the parameters with a short description of what role they pla **Efficiency** -* ``usefixedvalue``: If true, fixed efficiency is used. If false, the efficiency is the product of radial, detection, and trapping efficienc. The trapping efficiency is calculated from the minimum pitch angle. +* ``usefixedvalue``: If true, fixed efficiency is used. If false, the efficiency is the product of radial, detection, and trapping efficiency. The trapping efficiency is calculated from the minimum pitch angle. Most of these are calculated in mermithid now. * ``fixed_efficiency``: For example, set to roughly 2% for a 88deg minimum trapped pitch angle, assuming 100% detection efficiency of the trapped angles. * ``radial_efficiency``: Typically set to 0.67 from a calcualtion done for a 325MHz cavity with Halbach bite and radial cut on power of > 0.5 * maximum power. * ``detection_efficiency``: Fraction of events that is not detected. From d679f34f918b993d7361fc3fefd85ded3a3cefee Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Fri, 30 Jan 2026 17:17:39 -0500 Subject: [PATCH 08/57] Added ioffe bite calculation and pumping calculations --- .../sensitivity/SensitivityCavityFormulas.py | 61 ++++++++++++++++--- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index a80be149..5ac515ae 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -24,6 +24,30 @@ except: print("Run without morpho!") +# Jins functions - Atomic Calculator +# [m] - Fiducial distance from the cavity wall between max(ioffe_bite, larmor_radius) +def ioffe_bite(nominal_field, magnetic_inhomogenity, ioffe_field, ioffe_multipolarity, cavity_radius): + return ((2 * nominal_field**2 * magnetic_inhomogenity / (2 * nominal_field**2 * magnetic_inhomogenity + ioffe_field**2))**(1/(ioffe_multipolarity-2)) * cavity_radius * -1) + cavity_radius + +# [m^3/s] - The mean speed of the cylinder connected to a perfect pump (Dushman). Uniform source, outputs mean density in cavity. +# may need to convert tritium mass atomic to eV : eV/amu = 931494100 +def pumping_speed_limit_molecular(cavity_radius, cavity_temperature, cavity_L_over_D): + #self.FrequencyExtraction.cavity_temperature + #self.Experiment.cavity_L_over_D + return (np.pi * cavity_radius**2 * c0 * np.sqrt(kB * cavity_temperature / (4 * np.pi * tritium_mass_atomic)) / (0.5 + cavity_L_over_D / 8)) +# Useful for Atomic and Helium-3 +def pumping_speed_limit_atomic(cavity_radius, cavity_temperature, cavity_L_over_D): + return (np.pi * cavity_radius**2 * c0 * np.sqrt(kB * cavity_temperature / (2 * np.pi * tritium_mass_atomic)) / (0.5 + cavity_L_over_D / 8)) + +def turbopump_speed(number_turbos, pumping_speed_gas): + return number_turbos * pumping_speed_gas + +# [m^3/s] - Assumed ambient room temperature and a pumping speed of 0.5 L/s +def cavity_termination_speed_molecular(pumping_speed, cavity_top_plate_temp): + return (pumping_speed * np.sqrt(cavity_top_plate_temp * 28 / (293 * 2 * tritium_mass_atomic))) +def cavity_termination_speed_atomic(): + return (pumping_speed * np.sqrt(cavity_top_plate_temp * 28 / (293 * tritium_mass_atomic))) + # Wouters functinos @@ -231,6 +255,13 @@ def CalcDefaults(self, overwrite=False): #Cyclotron radius is sometimes used in the effective volume calculation self.cyc_rad = cyclotron_radius(self.cavity_freq, self.T_endpoint) + #Ioffe bite used in radial efficiency and effective volume calculation + self.unusable_dist_from_wall = self.Efficiency.unusable_dist_from_wall + if calculate_ioffe_bite and all(hasattr(self.MagneticField, attr) for attr in ("nominal_field", "magnetic_inhomogenity", "ioffe_field", "ioffe_multipolarity")): + self.unusable_dist_from_wall = ioffe_bite(self.MagneticField.nominal_field, self.MagneticField.magnetic_inhomogenity, self.MagneticField.ioffe_field, self.MagneticField.ioffe_multipolarity, self.cavity_radius) + elif calculate_ioffe_bite: + logger.info("Error: Did not specify all attributes for Ioffe bite calculation in config file") + #Assigning the background constant if it's not in the config file if hasattr(self.Experiment, "bkgd_constant"): self.bkgd_constant = self.Experiment.bkgd_constant @@ -243,10 +274,12 @@ def CalcDefaults(self, overwrite=False): self.EffectiveVolume() logger.info("Trap radius: {} cm".format(round(self.cavity_radius/cm, 3), 2)) logger.info("Total trap volume: {} m^3".format(self.total_trap_volume/m**3)) - logger.info("Cyclotron radius: {}m".format(self.cyc_rad/m)) + logger.info("Ioffe bite: {} m".format(self.unusable_dist_from_wall/m)) + logger.info("Cyclotron radius: {} m".format(self.cyc_rad/m)) if self.use_cyc_rad: - logger.info("Using cyclotron radius as unusable distance from wall, for radial efficiency calculation") - + logger.info("Using cyclotron (Larmor) radius as unusable distance from wall, for radial efficiency calculation") + else: + logger.info("Using ioffe bite as unusable distance from wall, for radial efficiency calculation") #### #Initialization related to the energy resolution: #### @@ -318,10 +351,9 @@ def EffectiveVolume(self): self.detection_efficiency = self.Efficiency.detection_efficiency self.RF_background_rate_per_eV = self.Experiment.RF_background_rate_per_eV - - #Radial efficiency - if self.Efficiency.unusable_dist_from_wall >= self.cyc_rad: - self.radial_efficiency = (self.cavity_radius - self.Efficiency.unusable_dist_from_wall)**2/self.cavity_radius**2 + #Radial efficiency - efficiency hit from Ioffe/Larmor bite + if self.unusable_dist_from_wall >= self.cyc_rad: + self.radial_efficiency = (self.cavity_radius - self.unusable_dist_from_wall)**2/self.cavity_radius**2 self.use_cyc_rad = False else: self.radial_efficiency = (self.cavity_radius - self.cyc_rad)**2/self.cavity_radius**2 @@ -777,7 +809,20 @@ def print_Efficiencies(self): logger.info("Efficiency from axial frequency cut: {}".format(self.fa_cut_efficiency)) logger.info("SRI factor: {}".format(self.Experiment.sri_factor)) - + def print_pumping_requirements(self): + if self.Efficiency.pumping_calculation: + self.turbopump_speed = turbopump_speed(self.Efficiency.number_turbopumps, self.Efficiency.pumping_speed_gas_T2) + logger.info("Turbopump Speed: {}".format(self.turbopump_speed * s / m**3)) + if self.Experiment.atomic: + self.pumping_speed_limit_atomic = pumping_speed_limit_atomic(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D) + self.cavity_termination_speed_atomic = cavity_termination_speed_atomic(self.Efficiency.pumping_speed_cavity_termination_air, self.Efficiency.cavity_top_plate_temperature) + logger.info("Pumping Speed Limit(Atomic): {}".format(self.pumping_speed_limit_atomic * s / m**3)) + logger.info("Cavity Termination Speed (Atomic): {}".format(self.cavity_termination_speed_atomic * s / m**3)) + else: + self.pumping_speed_limit_molecular = pumping_speed_limit_molecular(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D) + self.cavity_termination_speed_molecular = cavity_termination_speed_molecular(self.Efficiency.pumping_speed_cavity_termination_air, self.Efficiency.cavity_top_plate_temperature) + logger.info("Pumping Speed Limit(Molecular): {}".format(self.pumping_speed_limit_molecular * s / m**3)) + logger.info("Cavity Termination Speed (Molecular): {}".format(self.cavity_termination_speed_molecular * s / m**3)) """ # Cramer-Rao lower bound / how much worse are we than the lower bound From 6392c5656cb655ba7e30a964773389915eafbe62 Mon Sep 17 00:00:00 2001 From: Jin <65303994+ManjinderOueslati@users.noreply.github.com> Date: Fri, 30 Jan 2026 18:33:11 -0500 Subject: [PATCH 09/57] Refactor maxf assignment in FakeDataGenerator Refactor maxf assignment logic based on efficiency_dict and remove redundant checks. --- mermithid/processors/TritiumSpectrum/FakeDataGenerator.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/mermithid/processors/TritiumSpectrum/FakeDataGenerator.py b/mermithid/processors/TritiumSpectrum/FakeDataGenerator.py index 5aa760d0..4c67e5d5 100644 --- a/mermithid/processors/TritiumSpectrum/FakeDataGenerator.py +++ b/mermithid/processors/TritiumSpectrum/FakeDataGenerator.py @@ -274,17 +274,10 @@ def generate_unbinned_data(self, Q_mean, mass, ROIbound, S, B_1kev, nsteps=10**4 if len(ROIbound)==2: maxf = ROIbound[1] else: -<<<<<<< HEAD if efficiency_dict is not None: maxf = max(efficiency_dict['frequencies']) else: maxf = max(self.load_efficiency_curve()['frequencies']) -======= - if self.maxf is None: - maxf = max(self.load_efficiency_curve()['frequencies']) - else: - maxf = self.maxf ->>>>>>> origin Kmax, Kmin = Energy(minf, B_field), Energy(maxf, B_field) else: Kmin, Kmax = ROIbound[0], ROIbound[1] From 811b5b36d6d6b650f202460a142737fe346698a7 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Fri, 30 Jan 2026 19:00:42 -0500 Subject: [PATCH 10/57] Spaces and Tabs issue --- mermithid/sensitivity/SensitivityCavityFormulas.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index 5ac515ae..c5826308 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -818,12 +818,13 @@ def print_pumping_requirements(self): self.cavity_termination_speed_atomic = cavity_termination_speed_atomic(self.Efficiency.pumping_speed_cavity_termination_air, self.Efficiency.cavity_top_plate_temperature) logger.info("Pumping Speed Limit(Atomic): {}".format(self.pumping_speed_limit_atomic * s / m**3)) logger.info("Cavity Termination Speed (Atomic): {}".format(self.cavity_termination_speed_atomic * s / m**3)) + """ else: - self.pumping_speed_limit_molecular = pumping_speed_limit_molecular(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D) + self.pumping_speed_limit_molecular = pumping_speed_limit_molecular(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D) self.cavity_termination_speed_molecular = cavity_termination_speed_molecular(self.Efficiency.pumping_speed_cavity_termination_air, self.Efficiency.cavity_top_plate_temperature) logger.info("Pumping Speed Limit(Molecular): {}".format(self.pumping_speed_limit_molecular * s / m**3)) logger.info("Cavity Termination Speed (Molecular): {}".format(self.cavity_termination_speed_molecular * s / m**3)) - + """ """ # Cramer-Rao lower bound / how much worse are we than the lower bound ScalingFactorCRLB = self.FrequencyExtraction.CRLB_scaling_factor From 67eee46860196d1b7f9644ad3cdd9164fb0274c2 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Fri, 30 Jan 2026 19:08:39 -0500 Subject: [PATCH 11/57] Updated docker-compose.yaml file --- docker-compose.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yaml b/docker-compose.yaml index d019511f..d8f1326f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,6 +2,8 @@ #version: "3.9" #Check running containers docker ps (detached) -d #Attach to container docker attach [name] +# docker compose up -d +# docker attach mermithid_sensitivity services: mermithid: image: mermithid:sensitivity_branch From eac8946fa44127c4b4169d535940fde6b1f213d1 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Fri, 30 Jan 2026 19:10:36 -0500 Subject: [PATCH 12/57] Space and Tab issue --- mermithid/sensitivity/SensitivityCavityFormulas.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index c5826308..b32150c5 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -818,13 +818,12 @@ def print_pumping_requirements(self): self.cavity_termination_speed_atomic = cavity_termination_speed_atomic(self.Efficiency.pumping_speed_cavity_termination_air, self.Efficiency.cavity_top_plate_temperature) logger.info("Pumping Speed Limit(Atomic): {}".format(self.pumping_speed_limit_atomic * s / m**3)) logger.info("Cavity Termination Speed (Atomic): {}".format(self.cavity_termination_speed_atomic * s / m**3)) - """ else: self.pumping_speed_limit_molecular = pumping_speed_limit_molecular(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D) self.cavity_termination_speed_molecular = cavity_termination_speed_molecular(self.Efficiency.pumping_speed_cavity_termination_air, self.Efficiency.cavity_top_plate_temperature) logger.info("Pumping Speed Limit(Molecular): {}".format(self.pumping_speed_limit_molecular * s / m**3)) logger.info("Cavity Termination Speed (Molecular): {}".format(self.cavity_termination_speed_molecular * s / m**3)) - """ + """ # Cramer-Rao lower bound / how much worse are we than the lower bound ScalingFactorCRLB = self.FrequencyExtraction.CRLB_scaling_factor From d345d21718f9aaf5ee07a7e175474f4e26ebdac8 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Fri, 30 Jan 2026 19:14:34 -0500 Subject: [PATCH 13/57] Testing atomic effects config file --- test_analysis/Cavity_Sensitivity_analysis.py | 35 ++++++++++++-------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/test_analysis/Cavity_Sensitivity_analysis.py b/test_analysis/Cavity_Sensitivity_analysis.py index d0512997..d5e556a6 100644 --- a/test_analysis/Cavity_Sensitivity_analysis.py +++ b/test_analysis/Cavity_Sensitivity_analysis.py @@ -148,8 +148,8 @@ # Configuration for Sensitivity vs. density plot sens_config_dict = { # required - "config_file_path": "/termite/sensitivity_config_files/Config_LFA_Experiment_max_BNL_diam_threshold.cfg", #"/termite/sensitivity_config_files/Config_LFA_Experiment_1GHz.cfg", #Config_atomic_325MHz_Experiment_conservative.cfg", - "plot_path": "./LFA_and_PhaseIV_sensitivity_vs_density_target_and_threshold_April-18-2025.pdf", + "config_file_path": "/termite/sensitivity_config_files/Config_LFA_Experiment_max_BNL_diam.cfg", + "plot_path": "./LFA_and_PhaseIV_sensitivity_vs_density_target_and_threshold_Jan-18-2025.pdf", # optional "figsize": (7.5,6.4), "fontsize": 15, @@ -169,12 +169,16 @@ "goals_x_position": {"LFA threshold (0.7 eV)": 2.6e16, "Phase IV (0.04 eV)": 4e14}, #6e14, #3.3e14, #5.5e13, "goals_y_rel_position": {"LFA threshold (0.7 eV)": 1.1, "Phase IV (0.04 eV)": 0.79}, #0.755 "comparison_curve": True, + "comparison_config_file_path": ["/termite/sensitivity_config_files/Config_LFA_Experiment_Atomic_Effects.cfg"], + #"comparison_config_file_path": ["/termite/sensitivity_config_files/Config_LFA_Experiment_max_BNL_diam_threshold.cfg", "/termite/sensitivity_config_files/Config_PIVmodule1_150MHz_minpitch_87deg.cfg", "/termite/sensitivity_config_files/Config_LFA_Experiment_Atomic_Effects.cfg"], + #"comparison_curve_label": [r"LFA, target scenario", r"One full-size module", r"Phase IV: Ten full-size modules"], #: 150 MHz + "comparison_curve_label": [r"Atomic Calculator Effects"], "main_curve_color": "blue", - "comparison_curve_colors": ["blue", "darkred", "black"], + #"comparison_curve_colors": ["blue", "darkred", "black"], + "comparison_curve_colors": ["black"], "main_curve_linestyle": "dashed", - "comparison_curve_linestyles": ["solid", "dotted", "dashdot"], - "comparison_config_file_path": ["/termite/sensitivity_config_files/Config_LFA_Experiment_max_BNL_diam.cfg", "/termite/sensitivity_config_files/Config_PIVmodule1_150MHz_minpitch_87deg.cfg", "/termite/sensitivity_config_files/Config_atomic_150MHz_minpitch_87deg.cfg"], - "comparison_curve_label": [r"LFA, target scenario", r"One full-size module", r"Phase IV: Ten full-size modules"], #: 150 MHz + #"comparison_curve_linestyles": ["solid", "dotted", "dashdot"], + "comparison_curve_linestyles": ["solid"], "comparison_label_y_position": [2, 0.105, 0.046], #[2, 0.105, 0.046], "comparison_label_x_position": [4.5e15, 7e14, 7e14], #[4.5e15, 2.2e16, 1e15], #"sigmae_theta_r": 0.159, @@ -190,8 +194,8 @@ sens_config_dict = { # required - "config_file_path": "/termite/sensitivity_config_files/Config_LFA_Experiment_max_BNL_diam_threshold.cfg", #"/termite/sensitivity_config_files/Config_LFA_Experiment.cfg", - "plot_path": "./LFA_and_PhaseIV_sensitivity_vs_livetime_curve_target_and_threshold_April-18-2025.pdf", #ncav-eff-time + "config_file_path": "/termite/sensitivity_config_files/Config_LFA_Experiment_max_BNL_diam.cfg", + "plot_path": "./LFA_and_PhaseIV_sensitivity_vs_livetime_curve_target_and_threshold_Jan-18-2025.pdf", #ncav-eff-time "exposure_axis": True, # optional "figsize": (8.3, 6.3), #(10,6), @@ -218,14 +222,19 @@ "goals_x_position": {"LFA threshold (0.7 eV)": 4.5, "Phase IV (0.04 eV)": 0.108}, #6e14, #3.3e14, #5.5e13, "goals_y_rel_position": {"LFA threshold (0.7 eV)": 0.83, "Phase IV (0.04 eV)": 0.83}, #6e14, #3.3e14, #5.5e13, "comparison_curve": True, - "comparison_config_file_path": ["/termite/sensitivity_config_files/Config_LFA_Experiment_max_BNL_diam.cfg", "/termite/sensitivity_config_files/Config_PIVmodule1_150MHz_minpitch_87deg.cfg", "/termite/sensitivity_config_files/Config_atomic_150MHz_minpitch_87deg.cfg"], #"/termite/sensitivity_config_files/Config_LFA_Experiment_max_BNL_diam_threshold.cfg", - "comparison_curve_label": [r"LFA, target: $1.7\,$m$^3$, 1 yr", r'One full-size module: $94\,$m$^3$, 1 yr', r"Phase IV$-$Ten full-size modules: $940\,$m$^3$, 8 yrs"], #150 MHz, $V = 94\,$m$^3$ + "comparison_config_file_path": ["/termite/sensitivity_config_files/Config_LFA_Experiment_Atomic_Effects.cfg"], + #"comparison_config_file_path": ["/termite/sensitivity_config_files/Config_LFA_Experiment_max_BNL_diam_threshold.cfg", "/termite/sensitivity_config_files/Config_PIVmodule1_150MHz_minpitch_87deg.cfg", "/termite/sensitivity_config_files/Config_atomic_150MHz_minpitch_87deg.cfg"], #"/termite/sensitivity_config_files/Config_LFA_Experiment_max_BNL_diam_threshold.cfg", + "comparison_curve_label": [r"Atomic Calculator Effects"], + #"comparison_curve_label": [r"LFA, target: $1.7\,$m$^3$, 1 yr", r'One full-size module: $94\,$m$^3$, 1 yr', r"Phase IV$-$Ten full-size modules: $940\,$m$^3$, 8 yrs"], #150 MHz, $V = 94\,$m$^3$ "main_curve_color": "blue", - "comparison_curve_colors": ["blue", "darkred", "black"], + "comparison_curve_colors": ["black"], + #"comparison_curve_colors": ["blue", "darkred", "black"], "main_curve_linestyle": "dashed", - "comparison_curve_linestyles": ["solid", "dotted", "dashdot"], + #"comparison_curve_linestyles": ["solid", "dotted", "dashdot"], + "comparison_curve_linestyles": ["solid"], "main_curve_marker": "d", - "comparison_curve_markers": ["o", "^", "X"], + #"comparison_curve_markers": ["o", "^", "X"], + "comparison_curve_markers": ["X"], "optimize_main_density": False, "optimize_comparison_density": False, "lower_label_y_position": 0.17, From b365c42d79769471836d63bac3746852d00c3f03 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Fri, 30 Jan 2026 19:24:24 -0500 Subject: [PATCH 14/57] Ioffe calculation error fix --- mermithid/sensitivity/SensitivityCavityFormulas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index b32150c5..2e87697d 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -257,7 +257,7 @@ def CalcDefaults(self, overwrite=False): #Ioffe bite used in radial efficiency and effective volume calculation self.unusable_dist_from_wall = self.Efficiency.unusable_dist_from_wall - if calculate_ioffe_bite and all(hasattr(self.MagneticField, attr) for attr in ("nominal_field", "magnetic_inhomogenity", "ioffe_field", "ioffe_multipolarity")): + if self.Effficiency.calculate_ioffe_bite and all(hasattr(self.MagneticField, attr) for attr in ("nominal_field", "magnetic_inhomogenity", "ioffe_field", "ioffe_multipolarity")): self.unusable_dist_from_wall = ioffe_bite(self.MagneticField.nominal_field, self.MagneticField.magnetic_inhomogenity, self.MagneticField.ioffe_field, self.MagneticField.ioffe_multipolarity, self.cavity_radius) elif calculate_ioffe_bite: logger.info("Error: Did not specify all attributes for Ioffe bite calculation in config file") From db9445613d1c888eb506dc4c991a38b13a8c4578 Mon Sep 17 00:00:00 2001 From: Jin <65303994+ManjinderOueslati@users.noreply.github.com> Date: Fri, 30 Jan 2026 19:37:20 -0500 Subject: [PATCH 15/57] Fix typo in Efficiency reference for Ioffe bite calculation --- mermithid/sensitivity/SensitivityCavityFormulas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index 2e87697d..0c25e172 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -257,7 +257,7 @@ def CalcDefaults(self, overwrite=False): #Ioffe bite used in radial efficiency and effective volume calculation self.unusable_dist_from_wall = self.Efficiency.unusable_dist_from_wall - if self.Effficiency.calculate_ioffe_bite and all(hasattr(self.MagneticField, attr) for attr in ("nominal_field", "magnetic_inhomogenity", "ioffe_field", "ioffe_multipolarity")): + if self.Efficiency.calculate_ioffe_bite and all(hasattr(self.MagneticField, attr) for attr in ("nominal_field", "magnetic_inhomogenity", "ioffe_field", "ioffe_multipolarity")): self.unusable_dist_from_wall = ioffe_bite(self.MagneticField.nominal_field, self.MagneticField.magnetic_inhomogenity, self.MagneticField.ioffe_field, self.MagneticField.ioffe_multipolarity, self.cavity_radius) elif calculate_ioffe_bite: logger.info("Error: Did not specify all attributes for Ioffe bite calculation in config file") From fe1592d21fe61bba64c26942e2baa0bc943b4dcb Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Fri, 30 Jan 2026 19:57:24 -0500 Subject: [PATCH 16/57] Fixed python environment for local repository and typo errors in Formulas --- docker-compose.yaml | 3 +++ mermithid/sensitivity/SensitivityCavityFormulas.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index d8f1326f..c61f1f84 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -4,6 +4,7 @@ #Attach to container docker attach [name] # docker compose up -d # docker attach mermithid_sensitivity +# export PYTHONPATH=/mermithid/:$PYTHONPATH && services: mermithid: image: mermithid:sensitivity_branch @@ -14,6 +15,8 @@ services: - ~/repos/mermithid:/mermithid - ~/repos/mermithid_share:/host_data - ~/repos/termite:/termite + environment: + - PYTHONPATH=/mermithid:${PYTHONPATH} command: > bash -c " source /usr/local/p8/mermithid/v1.2.3/setup.sh && diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index 2e87697d..0f2eac03 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -257,9 +257,9 @@ def CalcDefaults(self, overwrite=False): #Ioffe bite used in radial efficiency and effective volume calculation self.unusable_dist_from_wall = self.Efficiency.unusable_dist_from_wall - if self.Effficiency.calculate_ioffe_bite and all(hasattr(self.MagneticField, attr) for attr in ("nominal_field", "magnetic_inhomogenity", "ioffe_field", "ioffe_multipolarity")): + if self.Efficiency.calculate_ioffe_bite and all(hasattr(self.MagneticField, attr) for attr in ("nominal_field", "magnetic_inhomogenity", "ioffe_field", "ioffe_multipolarity")): self.unusable_dist_from_wall = ioffe_bite(self.MagneticField.nominal_field, self.MagneticField.magnetic_inhomogenity, self.MagneticField.ioffe_field, self.MagneticField.ioffe_multipolarity, self.cavity_radius) - elif calculate_ioffe_bite: + elif self.Efficiency.calculate_ioffe_bite: logger.info("Error: Did not specify all attributes for Ioffe bite calculation in config file") #Assigning the background constant if it's not in the config file From ae2e449437969d3a12509bd1fe833b168fbbe919 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Mon, 2 Feb 2026 16:40:04 -0500 Subject: [PATCH 17/57] Merge sensitivity curve updates from feature/sensitivity_curve branch --- .../sensitivity/SensitivityCavityFormulas.py | 319 +++++++++++------- 1 file changed, 191 insertions(+), 128 deletions(-) diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index 0f2eac03..d2e461e2 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -10,6 +10,7 @@ import numpy as np from scipy.stats import ncx2, chi2 from scipy.special import roots_laguerre +import matplotlib.pyplot as plt from mermithid.misc.Constants_numericalunits import * from mermithid.misc.CRESFunctions_numericalunits import * @@ -24,30 +25,6 @@ except: print("Run without morpho!") -# Jins functions - Atomic Calculator -# [m] - Fiducial distance from the cavity wall between max(ioffe_bite, larmor_radius) -def ioffe_bite(nominal_field, magnetic_inhomogenity, ioffe_field, ioffe_multipolarity, cavity_radius): - return ((2 * nominal_field**2 * magnetic_inhomogenity / (2 * nominal_field**2 * magnetic_inhomogenity + ioffe_field**2))**(1/(ioffe_multipolarity-2)) * cavity_radius * -1) + cavity_radius - -# [m^3/s] - The mean speed of the cylinder connected to a perfect pump (Dushman). Uniform source, outputs mean density in cavity. -# may need to convert tritium mass atomic to eV : eV/amu = 931494100 -def pumping_speed_limit_molecular(cavity_radius, cavity_temperature, cavity_L_over_D): - #self.FrequencyExtraction.cavity_temperature - #self.Experiment.cavity_L_over_D - return (np.pi * cavity_radius**2 * c0 * np.sqrt(kB * cavity_temperature / (4 * np.pi * tritium_mass_atomic)) / (0.5 + cavity_L_over_D / 8)) -# Useful for Atomic and Helium-3 -def pumping_speed_limit_atomic(cavity_radius, cavity_temperature, cavity_L_over_D): - return (np.pi * cavity_radius**2 * c0 * np.sqrt(kB * cavity_temperature / (2 * np.pi * tritium_mass_atomic)) / (0.5 + cavity_L_over_D / 8)) - -def turbopump_speed(number_turbos, pumping_speed_gas): - return number_turbos * pumping_speed_gas - -# [m^3/s] - Assumed ambient room temperature and a pumping speed of 0.5 L/s -def cavity_termination_speed_molecular(pumping_speed, cavity_top_plate_temp): - return (pumping_speed * np.sqrt(cavity_top_plate_temp * 28 / (293 * 2 * tritium_mass_atomic))) -def cavity_termination_speed_atomic(): - return (pumping_speed * np.sqrt(cavity_top_plate_temp * 28 / (293 * tritium_mass_atomic))) - # Wouters functinos @@ -59,9 +36,9 @@ def axial_motion(magnetic_field, pitch, trap_length, minimum_trapped_pitch, kin_ # also return the average magnetic field seen by the electron # from z=0 to z=cavity_length/2 with npoints set by the trajectory variable # See LUCKEY write-up for a little more on Talia's "flat fraction" trap model - - #pitch = pitch/180*np.pi - #minimum_trapped_pitch = minimum_trapped_pitch/180*np.pi + + # Input parameters: + # pitch and minimum_trapped_pitch are in radians # Axial motion: z_w = trap_length/2 @@ -157,6 +134,58 @@ def t_effective(t_physical, cyclotron_frequency): else: return quantum*(1/2+1/(np.exp(quantum/t_physical)-1)) + +# Calculate threshold z to trap electrons born at some pitch angle theta_start +# Electrons are trapped if they start at z values less than this threshold +# (Considering one axial side of the trap) +def max_z_to_trap_vs_theta_start(theta_start, trap_length, minimum_trapped_pitch, flat_fraction=0.5): + z_w = trap_length/2 + sec_min = 1/np.cos(minimum_trapped_pitch) + sin2_min = np.sin(minimum_trapped_pitch)**2 + sin2_start = np.sin(theta_start)**2 + return z_w*flat_fraction + z_w*(1-flat_fraction)*sec_min*np.sqrt(sin2_start - sin2_min) + +def dist_of_theta_start_after_trapping(theta_start, trap_length, minimum_trapped_pitch, flat_fraction=0.5): + # Distribution of theta_start for electrons born uniformly along z + # Multiplied by sin(theta_start), to account for the birth pitch angles - is this + # correct? Is another normalization needed after multiplying by sin(theta_start)? + z_threshold = max_z_to_trap_vs_theta_start(theta_start, trap_length, minimum_trapped_pitch, flat_fraction) + return z_threshold/(trap_length/2)*np.sin(theta_start) + +def theta_bottom_from_theta_start(theta_start, B_min, B_start): + return np.arcsin(np.sin(theta_start)*np.sqrt(B_min/B_start)) + +def dist_of_theta_bottom_after_trapping(B_min, theta_start_array, trap_length, minimum_trapped_pitch, flat_fraction=0.5, n_z_start=1000, n_theta_bottom=10): + z_start_array = np.linspace(0, trap_length/2, n_z_start) + B_start_array = magnetic_field_flat_harmonic(z_start_array, B_min, trap_length, minimum_trapped_pitch, flat_fraction) + theta_bottoms = [] + for theta_start in theta_start_array: + theta_bottoms.append(theta_bottom_from_theta_start(theta_start, B_min, B_start_array)) + theta_bottoms = np.array(theta_bottoms) + theta_bottoms_bin_centers = np.linspace(minimum_trapped_pitch, np.pi/2, n_theta_bottom) + bin_size = (np.pi/2 - minimum_trapped_pitch)/n_theta_bottom + prob_theta_bottom = np.zeros(len(theta_bottoms_bin_centers)) + for i in range(len(theta_bottoms)): + for j in range(len(theta_bottoms[0])): + for k in range(len(theta_bottoms_bin_centers)): + if (theta_bottoms[i][j] >= theta_bottoms_bin_centers[k]-bin_size/2) and (theta_bottoms[i][j] < theta_bottoms_bin_centers[k]+bin_size/2): + prob_theta_bottom[k] += dist_of_theta_start_after_trapping(theta_start_array[i], trap_length, minimum_trapped_pitch, flat_fraction) + normalization = np.sum(prob_theta_bottom) + prob_theta_bottom = prob_theta_bottom/normalization #Is this the correct approach? + return theta_bottoms_bin_centers, prob_theta_bottom + +""" +figure = plt.figure() +theta_start_array = np.linspace(87*deg, np.pi/2, 1000) +prob_theta_start = dist_of_theta_start_after_trapping(theta_start_array, 4.05*m, 87*deg, flat_fraction=0.75) +plt.scatter(theta_start_array/deg, prob_theta_start) +plt.xlabel("Starting pitch angle $\\theta_{start}$ ($\degree$)", fontsize=14) +plt.ylabel("Probability (arb. units)", fontsize=14) +plt.savefig("test_theta_start_dist.png", dpi=300) +plt.show() +""" + + # Trapping efficiency from axial field variation. def trapping_efficiency(z_range, bg_magnetic_field, min_pitch_angle, trap_flat_fraction = 0.5): @@ -216,8 +245,8 @@ class CavitySensitivity(Sensitivity): * Nick's CRLB for frequency resolution: https://3.basecamp.com/3700981/buckets/3107037/uploads/2009854398 * Molecular contamination in atomic tritium: https://3.basecamp.com/3700981/buckets/3107037/documents/3151077016 """ - def __init__(self, config_path): - Sensitivity.__init__(self, config_path) + def __init__(self, config_path, verbose=True): + Sensitivity.__init__(self, config_path, verbose=verbose) # Calc non-config parameters outside of init function: ## Allows re-calcing params if config values changed later, e.g. param scans @@ -255,13 +284,6 @@ def CalcDefaults(self, overwrite=False): #Cyclotron radius is sometimes used in the effective volume calculation self.cyc_rad = cyclotron_radius(self.cavity_freq, self.T_endpoint) - #Ioffe bite used in radial efficiency and effective volume calculation - self.unusable_dist_from_wall = self.Efficiency.unusable_dist_from_wall - if self.Efficiency.calculate_ioffe_bite and all(hasattr(self.MagneticField, attr) for attr in ("nominal_field", "magnetic_inhomogenity", "ioffe_field", "ioffe_multipolarity")): - self.unusable_dist_from_wall = ioffe_bite(self.MagneticField.nominal_field, self.MagneticField.magnetic_inhomogenity, self.MagneticField.ioffe_field, self.MagneticField.ioffe_multipolarity, self.cavity_radius) - elif self.Efficiency.calculate_ioffe_bite: - logger.info("Error: Did not specify all attributes for Ioffe bite calculation in config file") - #Assigning the background constant if it's not in the config file if hasattr(self.Experiment, "bkgd_constant"): self.bkgd_constant = self.Experiment.bkgd_constant @@ -270,16 +292,59 @@ def CalcDefaults(self, overwrite=False): self.bkgd_constant = 1 logger.info("Using background rate constant of 1/eV/s") + # Need to get power fractions before calculating effective volume (given impact on detection efficiency) + # Power fractions are relative to the power of a 90° carrier electron + # If average_power_fractions==True, use carrier and sideband pitch power fractions averaged over the usable pitch angle range. + # If average_power_fractions==False, instead read in a csv file with power fractions vs. pitch angle from simulations. + # This file should have three columns: pitch angle (in degrees), carrier power, sideband power. + if hasattr(self.FrequencyExtraction, "use_average_power_fractions"): + if self.FrequencyExtraction.use_average_power_fractions: + logger.info("Using average carrier and sideband power fractions") + else: + logger.info("Using carrier and sideband (power fractions vs. pitch angle) from file") + # Read powers from the file and then scale them by power of maximum + # pitch angle to get power fractions. + theta_array, carrier_power_array, sideband_power_array = [], [], [] + power_file = open(self.FrequencyExtraction.powers_vs_theta_file, 'r') + for i in power_file.readlines()[1:]: # Skip header line + line = i.strip() + theta_array.append(float(line.split(",")[0])) # In degrees + carrier_power_array.append(float(line.split(",")[1])) + sideband_power_array.append(float(line.split(",")[2])) + power_file.close() + self.theta_array = np.array(theta_array)*deg # The "*deg" multiplies by np.pi/180 + carrier_power_array = np.array(carrier_power_array) + sideband_power_array = np.array(sideband_power_array) + # The calculation below assumes that the file contains a pitch angle very close to 90 degrees: + max_theta_index = np.argmax(self.theta_array) + self.carrier_power_fraction_array = carrier_power_array / carrier_power_array[max_theta_index] + self.sideband_power_fraction_array = sideband_power_array / carrier_power_array[max_theta_index] + + # Calculating distribution of pitch angles at the bottom of the trap, after trapping + theta_start_array = np.linspace(self.FrequencyExtraction.minimum_angle_in_bandwidth, np.pi/2, self.Efficiency.n_theta_start_for_trapped_pitch_dist) + self.theta_bottoms_bin_centers, self.prob_theta_bottom = dist_of_theta_bottom_after_trapping(self.MagneticField.nominal_field, theta_start_array, self.Experiment.trap_length, self.FrequencyExtraction.minimum_angle_in_bandwidth, flat_fraction=self.MagneticField.trap_flat_fraction, n_z_start=self.Efficiency.n_z_for_trapped_pitch_dist, n_theta_bottom=self.Efficiency.n_theta_bottom_for_trapped_pitch_dist) + + # Determining which probability corresponds to each pitch angle in self.theta_array + self.prob_theta_array = np.interp(self.theta_array, self.theta_bottoms_bin_centers, self.prob_theta_bottom) + + # Plotting pitch angle distribution + figure = plt.figure() + plt.scatter(self.theta_bottoms_bin_centers/deg, self.prob_theta_bottom, s=3, label="Binned distribution", color='red') + plt.scatter(self.theta_array/deg, self.prob_theta_array, s=2, marker='v', label="Interpolated to $\\theta_{bottom}$ values in simulations", color='blue') + plt.xlabel("Pitch angle at bottom of trap $\\theta_{bottom}$ ($\degree$)", fontsize=14) + plt.ylabel("Probability density", fontsize=14) + plt.legend(fontsize=12, loc='lower center') + plt.tight_layout() + plt.savefig("theta_bottom_dist_interpolated_{}.png".format(self.Experiment.exp_label), dpi=300) + #Calculate the effective volume and print out related quantities self.EffectiveVolume() logger.info("Trap radius: {} cm".format(round(self.cavity_radius/cm, 3), 2)) logger.info("Total trap volume: {} m^3".format(self.total_trap_volume/m**3)) - logger.info("Ioffe bite: {} m".format(self.unusable_dist_from_wall/m)) - logger.info("Cyclotron radius: {} m".format(self.cyc_rad/m)) + logger.info("Cyclotron radius: {}m".format(self.cyc_rad/m)) if self.use_cyc_rad: - logger.info("Using cyclotron (Larmor) radius as unusable distance from wall, for radial efficiency calculation") - else: - logger.info("Using ioffe bite as unusable distance from wall, for radial efficiency calculation") + logger.info("Using cyclotron radius as unusable distance from wall, for radial efficiency calculation") + #### #Initialization related to the energy resolution: #### @@ -289,7 +354,7 @@ def CalcDefaults(self, overwrite=False): self.CRLB_constant = self.FrequencyExtraction.crlb_constant logger.info("Using configured CRLB constant") - #Number of steps in pitch angle between min_pitch and pi/2 for the frequency noise uncertainty calculation + # Number of steps in pitch angle between min_pitch and pi/2 for the frequency noise uncertainty calculation self.pitch_steps = 100 if hasattr(self.FrequencyExtraction, "pitch_steps"): self.pitch_steps = self.FrequencyExtraction.pitch_steps @@ -297,7 +362,8 @@ def CalcDefaults(self, overwrite=False): #Just calculated for comparison self.larmor_power = rad_power(self.T_endpoint, np.pi/2, self.MagneticField.nominal_field) # currently not used - + + # Determining whether to use a fixed detection efficiency or calculate it from the detection threshold if not self.Efficiency.usefixedvalue: if self.Threshold.use_detection_threshold: logger.info("Overriding any detection eff and RF background in the config file; calculating these from the detection_threshold.") @@ -351,9 +417,10 @@ def EffectiveVolume(self): self.detection_efficiency = self.Efficiency.detection_efficiency self.RF_background_rate_per_eV = self.Experiment.RF_background_rate_per_eV - #Radial efficiency - efficiency hit from Ioffe/Larmor bite - if self.unusable_dist_from_wall >= self.cyc_rad: - self.radial_efficiency = (self.cavity_radius - self.unusable_dist_from_wall)**2/self.cavity_radius**2 + + #Radial efficiency + if self.Efficiency.unusable_dist_from_wall >= self.cyc_rad: + self.radial_efficiency = (self.cavity_radius - self.Efficiency.unusable_dist_from_wall)**2/self.cavity_radius**2 self.use_cyc_rad = False else: self.radial_efficiency = (self.cavity_radius - self.cyc_rad)**2/self.cavity_radius**2 @@ -545,9 +612,12 @@ def syst_frequency_extraction(self): self.time_window_slope_zero = abs(self.cavity_freq-frequency(self.T_endpoint+20*meV, self.MagneticField.nominal_field))/self.slope - tau_snr_full_length = self.calculate_tau_snr(self.time_window, self.FrequencyExtraction.carrier_power_fraction) - tau_snr_part_length = self.calculate_tau_snr(self.time_window_slope_zero, self.FrequencyExtraction.carrier_power_fraction) - + if self.FrequencyExtraction.use_average_power_fractions: + tau_snr_full_length = self.calculate_tau_snr(self.time_window, self.FrequencyExtraction.carrier_power_fraction) + else: + tau_snr_full_length = self.calculate_tau_snr(self.time_window, self.carrier_power_fraction_array) + tau_snr_full_length = tau_snr_full_length[:len(self.theta_array)-1] #Cut out theta=pi/2, since sideband power is 0 there, resulting in infinite tau_snr. + #Calculate the frequency variance from the CRLB self.var_f_c_CRLB = self.frequency_variance_from_CRLB(tau_snr_full_length) self.best_time_window = self.time_window @@ -557,16 +627,27 @@ def syst_frequency_extraction(self): #Calculate noise contribution to uncertainty, including energy correction for pitch angle. #This comes from section 6.1.9 of the CDR. - tau_snr_full_length_sideband = self.calculate_tau_snr(self.time_window, self.FrequencyExtraction.sideband_power_fraction) + if self.FrequencyExtraction.use_average_power_fractions: + tau_snr_full_length_sideband = self.calculate_tau_snr(self.time_window, self.FrequencyExtraction.sideband_power_fraction) + else: + tau_snr_full_length_sideband = self.calculate_tau_snr(self.time_window, self.sideband_power_fraction_array) + tau_snr_full_length_sideband = tau_snr_full_length_sideband[:len(self.theta_array)-1] #Cut out theta=pi/2, since sideband power is 0 there, resulting in infinite tau_snr. + # (sigmaf_lsb)^2: var_f_sideband_crlb = self.frequency_variance_from_CRLB(tau_snr_full_length_sideband) - #var_f_sideband_crlb = self.FrequencyExtraction.CRLB_scaling_factor*(self.CRLB_constant*tau_snr_full_length_sideband/self.time_window**3)/(2*np.pi)**2 - m = self.FrequencyExtraction.sideband_order #For convenience - #Define phi_max, corresponding to the minimum pitch angle - phi_max = np.pi/2 - self.FrequencyExtraction.minimum_angle_in_bandwidth - phis = np.linspace(0, phi_max, self.pitch_steps) + # Defining array of pitch angle complements (pi/2 - theta) used when calculating + # the parameters describing the track shape (p and q) + thetas_for_p_and_q_calc = np.linspace(self.FrequencyExtraction.minimum_angle_in_bandwidth, 90*deg, self.pitch_steps) + pitch_comps_for_p_and_q_calc = np.pi/2 - thetas_for_p_and_q_calc + + # Defining array of pitch angle complement values over which we calculate the + # resolution contribution from noise. + if self.FrequencyExtraction.use_average_power_fractions: + pitch_comps = pitch_comps_for_p_and_q_calc + else: + pitch_comps = np.pi/2 - self.theta_array[:len(self.theta_array)-1] #Cut out theta=pi/2 since sideband power is 0 there, resulting in infinite tau_snr. #Define the trap parameter p based on the relation between the trap length and the cavity mode #This p is for a box trap @@ -575,44 +656,48 @@ def syst_frequency_extraction(self): #Now find p for the actual trap that we have #Using the average p across the pitch angle range ax_freq_array, mean_field_array, z_t = axial_motion(self.MagneticField.nominal_field, - np.pi/2-phis, self.Experiment.trap_length, + thetas_for_p_and_q_calc, self.Experiment.trap_length, self.FrequencyExtraction.minimum_angle_in_bandwidth, self.T_endpoint, flat_fraction=self.MagneticField.trap_flat_fraction) fc0_endpoint = self.cavity_freq - p_array = ax_freq_array/fc0_endpoint/phis - self.p = np.mean(p_array[1:]) #Cut out theta=pi/2 (ill defined there) + p_array = ax_freq_array/fc0_endpoint/pitch_comps_for_p_and_q_calc #An array + if self.FrequencyExtraction.use_average_power_fractions: + p_array = p_array[:1] #Cut out theta=pi/2 (ill defined there) + self.p = np.mean(p_array) - #Now calculating q for the trap that we have - #Using the q for the minimum trapped pitch angle - fc_endpoint_min_theta = frequency(self.T_endpoint, mean_field_array[self.pitch_steps-1]) - self.q = (fc_endpoint_min_theta/fc0_endpoint - 1)/(phis[self.pitch_steps-1])**2 + # Now calculating q for the trap that we have + # Using the q for the minimum trapped pitch angle + fc_endpoint_min_theta = frequency(self.T_endpoint, mean_field_array[0]) + self.q = (fc_endpoint_min_theta/fc0_endpoint - 1)/(pitch_comps_for_p_and_q_calc[0])**2 - #Derivative of f_c0 (frequency corrected to B-field at bottom of the trap) with respect to f_c - dfc0_dfc_array = 0.5*(1 - (1 - 4*self.q*phis/m/self.p + self.q*phis**2)/(1 - self.q*phis**2)) + # Derivative of f_c0 (frequency corrected to B-field at bottom of the trap) with respect to f_c + dfc0_dfc_array = 0.5*(1 - (1 - 4*self.q*pitch_comps/m/self.p + self.q*pitch_comps**2)/(1 - self.q*pitch_comps**2)) - #Derivative of f_c0 with respect to f_lsb (lower sideband frequency) - dfc0_dlsb_array = 0.5 - 2*self.q*phis/m/self.p/(1 - self.q*phis**2) + # Derivative of f_c0 with respect to f_lsb (lower sideband frequency) + dfc0_dlsb_array = 0.5 - 2*self.q*pitch_comps/m/self.p/(1 - self.q*pitch_comps**2) - #Noise variance term from the carrier frequency uncertainty + # Noise variance term from the carrier frequency uncertainty var_noise_from_fc_array = dfc0_dfc_array**2*self.var_f_c_CRLB - #Noise variance term from the lower sideband frequency uncertainty + # Noise variance term from the lower sideband frequency uncertainty var_noise_from_flsb_array = dfc0_dlsb_array**2*var_f_sideband_crlb - #Total uncertainty for each pitch angle + # Total uncertainty for each pitch angle var_f_noise_array = var_noise_from_fc_array + var_noise_from_flsb_array - #Next, we average over sigma_noise values. - #This is a quadrature sum average, - #reflecting that the detector response function could be constructed by sampling - #from many normal distributions with different standard deviations (sigma_noise_array), - #then finding the standard deviation of the full group of sampled values. - self.sigma_f_noise = np.sqrt(np.sum(var_f_noise_array)/self.pitch_steps) + # Next, we average over sigma_noise values. + # This is a quadrature sum average weighted by the pitch angle distribution, + # reflecting that the detector response function could be constructed by sampling + # from many normal distributions with different standard deviations (sigma_noise_array), + # then finding the standard deviation of the full group of sampled values. + # IS THE BELOW CORRECT? + prob_theta_array_without_pi_over_2 = self.prob_theta_array[:len(self.theta_array)-1] #Cut out theta=pi/2 since sideband power is 0 there, resulting in infinite tau_snr. + self.sigma_f_noise = np.sqrt(np.sum(var_f_noise_array*prob_theta_array_without_pi_over_2)/np.sum(self.prob_theta_array)) else: self.sigma_f_noise = np.sqrt(self.var_f_c_CRLB) - #Convert uncertainty from frequency to energy + # Convert uncertainty from frequency to energy self.sigma_K_noise = e*self.MagneticField.nominal_field/(2*np.pi*endpoint_frequency**2)*self.sigma_f_noise*c0**2 # combined sigma_f in eV @@ -660,54 +745,32 @@ def det_efficiency_track_duration(self): https://3.basecamp.com/3700981/buckets/3107037/documents/8013439062 Gauss-Laguerre Quadrature: https://en.wikipedia.org/wiki/Gauss%E2%80%93Laguerre_quadrature - detection_eff_integration - ------------------------- - - The following changes were made to the original integral to fit the G-L method: - Original integrand: ∫[0 to \inf] ncx2(df=2, nc=t/τ).sf(thres) * (1/μ) * exp(-t/μ) dt - Where, - t = track_duration - μ (\mu) = mean_track_duration - τ (\tau) = tau_snr_ex_carrier - thres = detection_threshold + Where. t = track_duration, μ (\mu) = mean_track_duration, τ (\tau) = tau_snr_ex_carrier, thres = detection_threshold - We do the change of variable, x = t / μ - so, t = x μ - or, dt = μ dx + We do the change of variable, x = t / μ. So, t = x μ, or, dt = μ dx Substituting into the original integral: - ∫[0 to \inf] ncx2(df=2, nc=xμ/τ).sf(thres) * (1/μ) * exp(-x) μ dx - The μ's cancel out, and the integral takes the form: - ∫[0 to \inf] f(x) * exp(-x) dx - where, f(x) = ncx2(df=2, nc=xμ/τ).sf(thres) + Parameters: None - Parameters - ---------- - None - - - Returns - ------- - detection_efficiency : float - SNR and threshold dependent detection efficieny. + Returns: avg_efficiency (float): SNR and threshold dependent detection efficieny. - Notes - ----- - Also check the antenna paper for more details. - Especially the section on the signal detection with matched filtering. + Notes: Also check the antenna paper for more details. Especially the section on the signal detection with matched filtering. """ # Calculate the mean track duration - #FIX: Only do the lines below ones for a given density; don't repeat for each threshold being scanned ... + # TO-DO: Only do the lines below ones for a given density; don't repeat for each threshold being scanned ... mean_track_duration = track_length(self.Experiment.number_density, self.T_endpoint, molecular=(not self.Experiment.atomic)) - tau_snr_ex_total = self.calculate_tau_snr(mean_track_duration, self.FrequencyExtraction.carrier_power_fraction + self.FrequencyExtraction.sideband_power_fraction, tau_snr_array_for_radii=self.Efficiency.calculate_det_eff_for_sampled_radii) + if self.FrequencyExtraction.use_average_power_fractions: + tau_snr_ex_total = self.calculate_tau_snr(mean_track_duration, self.FrequencyExtraction.carrier_power_fraction + self.FrequencyExtraction.sideband_power_fraction, tau_snr_array_for_radii=self.Efficiency.calculate_det_eff_for_sampled_radii) + else: + tau_snr_ex_total = self.calculate_tau_snr(mean_track_duration, self.carrier_power_fraction_array + self.sideband_power_fraction_array, tau_snr_array_for_radii=self.Efficiency.calculate_det_eff_for_sampled_radii) if isinstance(tau_snr_ex_total, float): tau_snr_ex_total = [tau_snr_ex_total] @@ -721,10 +784,21 @@ def det_efficiency_track_duration(self): sf_values = np.array([ncx2(df=2, nc=2 * scaled_x / tau_snr).sf(self.Threshold.detection_threshold) for tau_snr in tau_snr_ex_total]) # Calculate and return the integration result from weighted sum - eff_for_each_r = np.sum(w * sf_values, axis=1) + eff_for_each_r_and_theta = np.sum(w * sf_values, axis=1) - #Average efficiencies over the sampled electron radii. Weighting for radial distribution is accounted for in sampling, earlier. - avg_efficiency = np.mean(eff_for_each_r) + # Average efficiencies over the sampled electron radii and pitch angles. + # Calculation below accounts for trapped pitch angle distribution (self.prob_theta_array). + # Weighting for radial distribution is accounted for in sampling, earlier. + if self.FrequencyExtraction.use_average_power_fractions: + avg_efficiency = np.mean(eff_for_each_r_and_theta) + else: + if not self.Efficiency.calculate_det_eff_for_sampled_radii: + avg_efficiency = np.sum(self.prob_theta_array * eff_for_each_r_and_theta)/sum(self.prob_theta_array) + else: + #Sum over radii with equal weights, and sum over pitch angles with probability weights + #I'm not sure if I get the axes right, below. + avg_efficiency = np.sum(eff_for_each_r_and_theta, axis=0)/len(self.signal_power_vs_r) + avg_efficiency = np.sum(self.prob_theta_array * avg_efficiency)/sum(self.prob_theta_array) return avg_efficiency def assign_detection_efficiency_from_threshold(self): @@ -759,9 +833,11 @@ def print_SNRs(self, rho=None): tau_snr_90deg = self.calculate_tau_snr(track_duration, power_fraction=1) #For an example carrier: - tau_snr_ex_carrier = self.calculate_tau_snr(track_duration, self.FrequencyExtraction.carrier_power_fraction) - - + if self.FrequencyExtraction.use_average_power_fractions: + tau_snr_ex_carrier = self.calculate_tau_snr(track_duration, self.FrequencyExtraction.carrier_power_fraction) + else: + tau_snr_ex_carrier = np.mean(self.calculate_tau_snr(track_duration, self.carrier_power_fraction_array)) + eV_bandwidth = np.abs(self.cavity_freq - frequency(self.T_endpoint + 1*eV, self.MagneticField.nominal_field)) SNR_1eV_90deg = 1/eV_bandwidth/tau_snr_90deg SNR_track_duration_90deg = track_duration/tau_snr_90deg @@ -809,20 +885,7 @@ def print_Efficiencies(self): logger.info("Efficiency from axial frequency cut: {}".format(self.fa_cut_efficiency)) logger.info("SRI factor: {}".format(self.Experiment.sri_factor)) - def print_pumping_requirements(self): - if self.Efficiency.pumping_calculation: - self.turbopump_speed = turbopump_speed(self.Efficiency.number_turbopumps, self.Efficiency.pumping_speed_gas_T2) - logger.info("Turbopump Speed: {}".format(self.turbopump_speed * s / m**3)) - if self.Experiment.atomic: - self.pumping_speed_limit_atomic = pumping_speed_limit_atomic(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D) - self.cavity_termination_speed_atomic = cavity_termination_speed_atomic(self.Efficiency.pumping_speed_cavity_termination_air, self.Efficiency.cavity_top_plate_temperature) - logger.info("Pumping Speed Limit(Atomic): {}".format(self.pumping_speed_limit_atomic * s / m**3)) - logger.info("Cavity Termination Speed (Atomic): {}".format(self.cavity_termination_speed_atomic * s / m**3)) - else: - self.pumping_speed_limit_molecular = pumping_speed_limit_molecular(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D) - self.cavity_termination_speed_molecular = cavity_termination_speed_molecular(self.Efficiency.pumping_speed_cavity_termination_air, self.Efficiency.cavity_top_plate_temperature) - logger.info("Pumping Speed Limit(Molecular): {}".format(self.pumping_speed_limit_molecular * s / m**3)) - logger.info("Cavity Termination Speed (Molecular): {}".format(self.cavity_termination_speed_molecular * s / m**3)) + """ # Cramer-Rao lower bound / how much worse are we than the lower bound From cd366a730238df958c7124de5dc65310a52f6bd2 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Mon, 2 Feb 2026 16:49:51 -0500 Subject: [PATCH 18/57] Merge Cavity_Sensitivity_analysis.py --- test_analysis/Cavity_Sensitivity_analysis.py | 37 ++++++++------------ 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/test_analysis/Cavity_Sensitivity_analysis.py b/test_analysis/Cavity_Sensitivity_analysis.py index d5e556a6..17e7a0c0 100644 --- a/test_analysis/Cavity_Sensitivity_analysis.py +++ b/test_analysis/Cavity_Sensitivity_analysis.py @@ -148,8 +148,8 @@ # Configuration for Sensitivity vs. density plot sens_config_dict = { # required - "config_file_path": "/termite/sensitivity_config_files/Config_LFA_Experiment_max_BNL_diam.cfg", - "plot_path": "./LFA_and_PhaseIV_sensitivity_vs_density_target_and_threshold_Jan-18-2025.pdf", + "config_file_path": "/termite/sensitivity_config_files/Config_LFA_Experiment_max_BNL_diam_threshold.cfg", #"/termite/sensitivity_config_files/Config_LFA_Experiment_1GHz.cfg", #Config_atomic_325MHz_Experiment_conservative.cfg", + "plot_path": "./LFA_and_PhaseIV_sensitivity_vs_density_target_and_threshold_Dec-19-2025.pdf", # optional "figsize": (7.5,6.4), "fontsize": 15, @@ -164,21 +164,19 @@ "y_limits": [2e-2, 6.5], "density_range": [3e14,3e18], #5e13 "det_thresh_range": [5, 115], + "add_point_at_configured_density": True, "main_curve_upper_label": r"LFA, threshold scenario", #560 MHz #Phase III scenario: 1 GHz", "goals": {"LFA threshold (0.7 eV)": 0.7, "Phase IV (0.04 eV)": 0.04}, #"Pilot T goal (0.1 eV)": 0.1, "goals_x_position": {"LFA threshold (0.7 eV)": 2.6e16, "Phase IV (0.04 eV)": 4e14}, #6e14, #3.3e14, #5.5e13, "goals_y_rel_position": {"LFA threshold (0.7 eV)": 1.1, "Phase IV (0.04 eV)": 0.79}, #0.755 "comparison_curve": True, - "comparison_config_file_path": ["/termite/sensitivity_config_files/Config_LFA_Experiment_Atomic_Effects.cfg"], - #"comparison_config_file_path": ["/termite/sensitivity_config_files/Config_LFA_Experiment_max_BNL_diam_threshold.cfg", "/termite/sensitivity_config_files/Config_PIVmodule1_150MHz_minpitch_87deg.cfg", "/termite/sensitivity_config_files/Config_LFA_Experiment_Atomic_Effects.cfg"], - #"comparison_curve_label": [r"LFA, target scenario", r"One full-size module", r"Phase IV: Ten full-size modules"], #: 150 MHz - "comparison_curve_label": [r"Atomic Calculator Effects"], + "verbose": False, "main_curve_color": "blue", - #"comparison_curve_colors": ["blue", "darkred", "black"], - "comparison_curve_colors": ["black"], + "comparison_curve_colors": ["blue", "darkred", "black"], "main_curve_linestyle": "dashed", - #"comparison_curve_linestyles": ["solid", "dotted", "dashdot"], - "comparison_curve_linestyles": ["solid"], + "comparison_curve_linestyles": ["solid", "dotted", "dashdot"], + "comparison_config_file_path": ["/termite/sensitivity_config_files/Config_LFA_Experiment_max_BNL_diam.cfg", "/termite/sensitivity_config_files/Config_PIVmodule1_150MHz_minpitch_87deg.cfg", "/termite/sensitivity_config_files/Config_atomic_150MHz_minpitch_87deg.cfg"], + "comparison_curve_label": [r"LFA, target scenario", r"One full-size module", r"Phase IV: Ten full-size modules"], #: 150 MHz "comparison_label_y_position": [2, 0.105, 0.046], #[2, 0.105, 0.046], "comparison_label_x_position": [4.5e15, 7e14, 7e14], #[4.5e15, 2.2e16, 1e15], #"sigmae_theta_r": 0.159, @@ -194,8 +192,8 @@ sens_config_dict = { # required - "config_file_path": "/termite/sensitivity_config_files/Config_LFA_Experiment_max_BNL_diam.cfg", - "plot_path": "./LFA_and_PhaseIV_sensitivity_vs_livetime_curve_target_and_threshold_Jan-18-2025.pdf", #ncav-eff-time + "config_file_path": "/termite/sensitivity_config_files/Config_LFA_Experiment_max_BNL_diam_threshold.cfg", #"/termite/sensitivity_config_files/Config_LFA_Experiment.cfg", + "plot_path": "./LFA_and_PhaseIV_sensitivity_vs_livetime_curve_target_and_threshold_Dec-19-2025.pdf", #ncav-eff-time "exposure_axis": True, # optional "figsize": (8.3, 6.3), #(10,6), @@ -222,19 +220,14 @@ "goals_x_position": {"LFA threshold (0.7 eV)": 4.5, "Phase IV (0.04 eV)": 0.108}, #6e14, #3.3e14, #5.5e13, "goals_y_rel_position": {"LFA threshold (0.7 eV)": 0.83, "Phase IV (0.04 eV)": 0.83}, #6e14, #3.3e14, #5.5e13, "comparison_curve": True, - "comparison_config_file_path": ["/termite/sensitivity_config_files/Config_LFA_Experiment_Atomic_Effects.cfg"], - #"comparison_config_file_path": ["/termite/sensitivity_config_files/Config_LFA_Experiment_max_BNL_diam_threshold.cfg", "/termite/sensitivity_config_files/Config_PIVmodule1_150MHz_minpitch_87deg.cfg", "/termite/sensitivity_config_files/Config_atomic_150MHz_minpitch_87deg.cfg"], #"/termite/sensitivity_config_files/Config_LFA_Experiment_max_BNL_diam_threshold.cfg", - "comparison_curve_label": [r"Atomic Calculator Effects"], - #"comparison_curve_label": [r"LFA, target: $1.7\,$m$^3$, 1 yr", r'One full-size module: $94\,$m$^3$, 1 yr', r"Phase IV$-$Ten full-size modules: $940\,$m$^3$, 8 yrs"], #150 MHz, $V = 94\,$m$^3$ + "comparison_config_file_path": ["/termite/sensitivity_config_files/Config_LFA_Experiment_max_BNL_diam.cfg", "/termite/sensitivity_config_files/Config_PIVmodule1_150MHz_minpitch_87deg.cfg", "/termite/sensitivity_config_files/Config_atomic_150MHz_minpitch_87deg.cfg"], #"/termite/sensitivity_config_files/Config_LFA_Experiment_max_BNL_diam_threshold.cfg", + "comparison_curve_label": [r"LFA, target: $1.7\,$m$^3$, 1 yr", r'One full-size module: $100\,$m$^3$, 1 yr', r"Phase IV$-$Ten full-size modules: $1000\,$m$^3$, 8 yrs"], #150 MHz, $V = 94\,$m$^3$ "main_curve_color": "blue", - "comparison_curve_colors": ["black"], - #"comparison_curve_colors": ["blue", "darkred", "black"], + "comparison_curve_colors": ["blue", "darkred", "black"], "main_curve_linestyle": "dashed", - #"comparison_curve_linestyles": ["solid", "dotted", "dashdot"], - "comparison_curve_linestyles": ["solid"], + "comparison_curve_linestyles": ["solid", "dotted", "dashdot"], "main_curve_marker": "d", - #"comparison_curve_markers": ["o", "^", "X"], - "comparison_curve_markers": ["X"], + "comparison_curve_markers": ["o", "^", "X"], "optimize_main_density": False, "optimize_comparison_density": False, "lower_label_y_position": 0.17, From 70c790b8906dd322eb207b122a68d1be882ac467 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Mon, 2 Feb 2026 16:51:01 -0500 Subject: [PATCH 19/57] Atomic Calculator pumping formulas --- .../sensitivity/SensitivityCavityFormulas.py | 60 ++++++++++++++++--- 1 file changed, 53 insertions(+), 7 deletions(-) diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index d2e461e2..b832fb0a 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -25,6 +25,29 @@ except: print("Run without morpho!") +# Jins functions - Atomic Calculator +# [m] - Fiducial distance from the cavity wall between max(ioffe_bite, larmor_radius) +def ioffe_bite(nominal_field, magnetic_inhomogenity, ioffe_field, ioffe_multipolarity, cavity_radius): + return ((2 * nominal_field**2 * magnetic_inhomogenity / (2 * nominal_field**2 * magnetic_inhomogenity + ioffe_field**2))**(1/(ioffe_multipolarity-2)) * cavity_radius * -1) + cavity_radius + +# [m^3/s] - The mean speed of the cylinder connected to a perfect pump (Dushman). Uniform source, outputs mean density in cavity. +# may need to convert tritium mass atomic to eV : eV/amu = 931494100 +def pumping_speed_limit_molecular(cavity_radius, cavity_temperature, cavity_L_over_D): + #self.FrequencyExtraction.cavity_temperature + #self.Experiment.cavity_L_over_D + return (np.pi * cavity_radius**2 * c0 * np.sqrt(kB * cavity_temperature / (4 * np.pi * tritium_mass_atomic)) / (0.5 + cavity_L_over_D / 8)) +# Useful for Atomic and Helium-3 +def pumping_speed_limit_atomic(cavity_radius, cavity_temperature, cavity_L_over_D): + return (np.pi * cavity_radius**2 * c0 * np.sqrt(kB * cavity_temperature / (2 * np.pi * tritium_mass_atomic)) / (0.5 + cavity_L_over_D / 8)) + +def turbopump_speed(number_turbos, pumping_speed_gas): + return number_turbos * pumping_speed_gas + +# [m^3/s] - Assumed ambient room temperature and a pumping speed of 0.5 L/s +def cavity_termination_speed_molecular(pumping_speed, cavity_top_plate_temp): + return (pumping_speed * np.sqrt(cavity_top_plate_temp * 28 / (293 * 2 * tritium_mass_atomic))) +def cavity_termination_speed_atomic(): + return (pumping_speed * np.sqrt(cavity_top_plate_temp * 28 / (293 * tritium_mass_atomic))) # Wouters functinos @@ -284,6 +307,13 @@ def CalcDefaults(self, overwrite=False): #Cyclotron radius is sometimes used in the effective volume calculation self.cyc_rad = cyclotron_radius(self.cavity_freq, self.T_endpoint) + #Ioffe bite used in radial efficiency and effective volume calculation + self.unusable_dist_from_wall = self.Efficiency.unusable_dist_from_wall + if self.Efficiency.calculate_ioffe_bite and all(hasattr(self.MagneticField, attr) for attr in ("nominal_field", "magnetic_inhomogenity", "ioffe_field", "ioffe_multipolarity")): + self.unusable_dist_from_wall = ioffe_bite(self.MagneticField.nominal_field, self.MagneticField.magnetic_inhomogenity, self.MagneticField.ioffe_field, self.MagneticField.ioffe_multipolarity, self.cavity_radius) + elif self.Efficiency.calculate_ioffe_bite: + logger.info("Error: Did not specify all attributes for Ioffe bite calculation in config file") + #Assigning the background constant if it's not in the config file if hasattr(self.Experiment, "bkgd_constant"): self.bkgd_constant = self.Experiment.bkgd_constant @@ -341,9 +371,12 @@ def CalcDefaults(self, overwrite=False): self.EffectiveVolume() logger.info("Trap radius: {} cm".format(round(self.cavity_radius/cm, 3), 2)) logger.info("Total trap volume: {} m^3".format(self.total_trap_volume/m**3)) - logger.info("Cyclotron radius: {}m".format(self.cyc_rad/m)) + logger.info("Ioffe bite: {} m".format(self.unusable_dist_from_wall/m)) + logger.info("Cyclotron radius: {} m".format(self.cyc_rad/m)) if self.use_cyc_rad: - logger.info("Using cyclotron radius as unusable distance from wall, for radial efficiency calculation") + logger.info("Using cyclotron (Larmor) radius as unusable distance from wall, for radial efficiency calculation") + else: + logger.info("Using ioffe bite as unusable distance from wall, for radial efficiency calculation") #### #Initialization related to the energy resolution: @@ -418,14 +451,14 @@ def EffectiveVolume(self): self.RF_background_rate_per_eV = self.Experiment.RF_background_rate_per_eV - #Radial efficiency - if self.Efficiency.unusable_dist_from_wall >= self.cyc_rad: - self.radial_efficiency = (self.cavity_radius - self.Efficiency.unusable_dist_from_wall)**2/self.cavity_radius**2 + #Radial efficiency - efficiency hit from Ioffe/Larmor bite + if self.unusable_dist_from_wall >= self.cyc_rad: + self.radial_efficiency = (self.cavity_radius - self.unusable_dist_from_wall)**2/self.cavity_radius**2 self.use_cyc_rad = False else: self.radial_efficiency = (self.cavity_radius - self.cyc_rad)**2/self.cavity_radius**2 self.use_cyc_rad = True - + #Efficiency from a cut during analysis on the axial frequency self.fa_cut_efficiency = trapping_efficiency(z_range = self.Experiment.trap_length /2, bg_magnetic_field = self.MagneticField.nominal_field, @@ -885,7 +918,20 @@ def print_Efficiencies(self): logger.info("Efficiency from axial frequency cut: {}".format(self.fa_cut_efficiency)) logger.info("SRI factor: {}".format(self.Experiment.sri_factor)) - + def print_pumping_requirements(self): + if self.Efficiency.pumping_calculation: + self.turbopump_speed = turbopump_speed(self.Efficiency.number_turbopumps, self.Efficiency.pumping_speed_gas_T2) + logger.info("Turbopump Speed: {}".format(self.turbopump_speed * s / m**3)) + if self.Experiment.atomic: + self.pumping_speed_limit_atomic = pumping_speed_limit_atomic(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D) + self.cavity_termination_speed_atomic = cavity_termination_speed_atomic(self.Efficiency.pumping_speed_cavity_termination_air, self.Efficiency.cavity_top_plate_temperature) + logger.info("Pumping Speed Limit(Atomic): {}".format(self.pumping_speed_limit_atomic * s / m**3)) + logger.info("Cavity Termination Speed (Atomic): {}".format(self.cavity_termination_speed_atomic * s / m**3)) + else: + self.pumping_speed_limit_molecular = pumping_speed_limit_molecular(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D) + self.cavity_termination_speed_molecular = cavity_termination_speed_molecular(self.Efficiency.pumping_speed_cavity_termination_air, self.Efficiency.cavity_top_plate_temperature) + logger.info("Pumping Speed Limit(Molecular): {}".format(self.pumping_speed_limit_molecular * s / m**3)) + logger.info("Cavity Termination Speed (Molecular): {}".format(self.cavity_termination_speed_molecular * s / m**3)) """ # Cramer-Rao lower bound / how much worse are we than the lower bound From 95a4690d74d8ee0ec97600cba16b57fc418d1142 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Mon, 2 Feb 2026 18:10:00 -0500 Subject: [PATCH 20/57] Fixed PYTHONPATH implementation issue --- docker-compose.yaml | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index c61f1f84..72e69c20 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,10 +1,17 @@ #version: "3.9" -#Check running containers docker ps (detached) -d -#Attach to container docker attach [name] -# docker compose up -d -# docker attach mermithid_sensitivity -# export PYTHONPATH=/mermithid/:$PYTHONPATH && +# Check running containers docker ps (detached) -d +# Attach to container docker attach [name] +# Mermithid is a bind mount, this guarantees: live code edits on host, Python imports always resolve to your branch & no accidental use of the installed site-packages version + +#docker compose up -d +#docker attach mermithid_sensitivity +#export PYTHONPATH=/mermithid/:$PYTHONPATH && + +# /mermithid is on sys.path. Python should import from it but it does not, because mermithid is already imported earlier by ROOT’s import hook. Once a module is loaded, Python never re-resolves it. +# environment: is already working. It just can’t solve this class of problem. PYTHONPATH is correct; ROOT imports mermithid first and locks it in — only an editable install can override that. +#environment: #- PYTHONPATH=/mermithid:${PYTHONPATH} +# Overriding sys for PYTHONPATH services: mermithid: image: mermithid:sensitivity_branch @@ -15,12 +22,11 @@ services: - ~/repos/mermithid:/mermithid - ~/repos/mermithid_share:/host_data - ~/repos/termite:/termite - environment: - - PYTHONPATH=/mermithid:${PYTHONPATH} command: > - bash -c " + bash -lic " source /usr/local/p8/mermithid/v1.2.3/setup.sh && source /usr/local/p8/mermithid/v1.2.3/bin/this_phylloxera.sh && source /usr/local/p8/mermithid/v1.2.3/bin/this_cicada.sh && - exec bash + pip install -e /mermithid && + bash " From 1309d3f157787505fd43a0836233e202f1984d55 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Wed, 4 Feb 2026 12:42:56 -0500 Subject: [PATCH 21/57] Added Inelastic + Elastic Cross-section for atomic T from Hamish Atomic Calc --- mermithid/misc/Constants_numericalunits.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mermithid/misc/Constants_numericalunits.py b/mermithid/misc/Constants_numericalunits.py index e1577c41..9430bf13 100644 --- a/mermithid/misc/Constants_numericalunits.py +++ b/mermithid/misc/Constants_numericalunits.py @@ -16,7 +16,9 @@ tritium_livetime = 5.605e8*s tritium_mass_atomic = 3.016* amu *c0**2 -tritium_electron_crosssection_atomic = 9.e-23*m**2 #Hamish extrapolated to 18.6keV using Shah et al. (1987): https://iopscience.iop.org/article/10.1088/0022-3700/20/14/022 +tritium_electron_crosssection_atomic = 9.e-23*m**2 #Inelastic cross-section Hamish extrapolated to 18.6keV using Shah et al. (1987): https://iopscience.iop.org/article/10.1088/0022-3700/20/14/022. Full Bethe formula must be exactly right (down to magnetic and QED corrections) for the hydrogen atom. +#tritium_electron_crosssection_atomic = 1.32e-22*m**2 #Inelastic cross-section + Elastic cross-section for T + tritium_endpoint_atomic = 18563.251*eV last_1ev_fraction_atomic = 2.067914e-13/eV**3 @@ -43,4 +45,4 @@ # radian and degree which are also not really units rad = 1 -deg = np.pi/180 \ No newline at end of file +deg = np.pi/180 From f752bdbcd32272f26ed9c3ebc8714fb009a6de4b Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Wed, 4 Feb 2026 21:24:51 -0500 Subject: [PATCH 22/57] Updated numerical units to include pressure, mass, etc. --- mermithid/misc/Constants_numericalunits.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/mermithid/misc/Constants_numericalunits.py b/mermithid/misc/Constants_numericalunits.py index 9430bf13..fdebde26 100644 --- a/mermithid/misc/Constants_numericalunits.py +++ b/mermithid/misc/Constants_numericalunits.py @@ -5,12 +5,14 @@ import numpy as np -from numericalunits import e, me, c0, eps0, kB, hbar -from numericalunits import meV, eV, keV, MeV, mm, cm, m, ns, s, Hz, kHz, MHz, GHz, amu, nJ -from numericalunits import nT, uT, mT, T, mK, K, C, F, g, W -from numericalunits import hour, year, day, s, ms -from numericalunits import mu0, NA, kB, hbar, me, c0, e, eps0, hPlanck - +from numericalunits import e, me, c0, eps0, mu0, kB, hbar, Rgas +from numericalunits import meV, eV, keV, MeV, mm, cm, m +from numericalunits import nT, uT, mT, T, mK, K, C, F, g, W, nJ, J +from numericalunits import hour, year, day, s, ms, ns, kHz, MHz, GHz +from numericalunits import NA, hPlanck, L, mL +from numericalunits import kg, amu +from numericalunits import A, mA, uA, V, mV, nV, kV +from numericalunits import Pa, bar, mbar, atm, torr, mtorr T0 = -273.15*K From 74fd7889769aa7dbb4b77d6158a2424cc1d27103 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Wed, 4 Feb 2026 21:33:15 -0500 Subject: [PATCH 23/57] Updated numerical units to include pressure, mass, etc. with documentation --- mermithid/misc/Constants_numericalunits.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/mermithid/misc/Constants_numericalunits.py b/mermithid/misc/Constants_numericalunits.py index fdebde26..528ca776 100644 --- a/mermithid/misc/Constants_numericalunits.py +++ b/mermithid/misc/Constants_numericalunits.py @@ -5,14 +5,13 @@ import numpy as np -from numericalunits import e, me, c0, eps0, mu0, kB, hbar, Rgas -from numericalunits import meV, eV, keV, MeV, mm, cm, m -from numericalunits import nT, uT, mT, T, mK, K, C, F, g, W, nJ, J -from numericalunits import hour, year, day, s, ms, ns, kHz, MHz, GHz -from numericalunits import NA, hPlanck, L, mL -from numericalunits import kg, amu -from numericalunits import A, mA, uA, V, mV, nV, kV -from numericalunits import Pa, bar, mbar, atm, torr, mtorr +from numericalunits import e, me, c0, eps0, mu0, kB, hbar, Rgas, NA, hPlanck # Constants +from numericalunits import meV, eV, keV, MeV, nJ, J, mm, cm, m # Energy, Length +from numericalunits import nT, uT, mT, T, mK, K, F, W # Magnetic field, Temp, Power +from numericalunits import hour, year, day, s, ms, ns, kHz, MHz, GHz # Time, Frequency +from numericalunits import kg, g, amu # Mass +from numericalunits import C, A, mA, uA, V, mV, nV, kV # Charge, Amps, Voltage +from numericalunits import Pa, bar, mbar, atm, torr, mtorr, L, mL # Pressure, Volume T0 = -273.15*K From 256989ccc0bddb0f840d8354b37db26a935a1c1b Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Wed, 4 Feb 2026 21:34:59 -0500 Subject: [PATCH 24/57] Updated numerical units to include pressure, mass, etc. with documentation + moles --- mermithid/misc/Constants_numericalunits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mermithid/misc/Constants_numericalunits.py b/mermithid/misc/Constants_numericalunits.py index 528ca776..dec02ce1 100644 --- a/mermithid/misc/Constants_numericalunits.py +++ b/mermithid/misc/Constants_numericalunits.py @@ -9,7 +9,7 @@ from numericalunits import meV, eV, keV, MeV, nJ, J, mm, cm, m # Energy, Length from numericalunits import nT, uT, mT, T, mK, K, F, W # Magnetic field, Temp, Power from numericalunits import hour, year, day, s, ms, ns, kHz, MHz, GHz # Time, Frequency -from numericalunits import kg, g, amu # Mass +from numericalunits import kg, g, amu, mol # Mass from numericalunits import C, A, mA, uA, V, mV, nV, kV # Charge, Amps, Voltage from numericalunits import Pa, bar, mbar, atm, torr, mtorr, L, mL # Pressure, Volume From a4c947e9060d239fc8cf5e78d45af9e034415e48 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Wed, 4 Feb 2026 21:37:07 -0500 Subject: [PATCH 25/57] Updated numerical units to include pressure, mass, etc. with documentation --- mermithid/misc/Constants_numericalunits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mermithid/misc/Constants_numericalunits.py b/mermithid/misc/Constants_numericalunits.py index dec02ce1..672fac6f 100644 --- a/mermithid/misc/Constants_numericalunits.py +++ b/mermithid/misc/Constants_numericalunits.py @@ -8,7 +8,7 @@ from numericalunits import e, me, c0, eps0, mu0, kB, hbar, Rgas, NA, hPlanck # Constants from numericalunits import meV, eV, keV, MeV, nJ, J, mm, cm, m # Energy, Length from numericalunits import nT, uT, mT, T, mK, K, F, W # Magnetic field, Temp, Power -from numericalunits import hour, year, day, s, ms, ns, kHz, MHz, GHz # Time, Frequency +from numericalunits import hour, year, day, s, ms, ns, Hz, kHz, MHz, GHz # Time, Frequency from numericalunits import kg, g, amu, mol # Mass from numericalunits import C, A, mA, uA, V, mV, nV, kV # Charge, Amps, Voltage from numericalunits import Pa, bar, mbar, atm, torr, mtorr, L, mL # Pressure, Volume From 079a0d59cef71fca8c80994b7445020a1093af33 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Wed, 4 Feb 2026 23:56:56 -0500 Subject: [PATCH 26/57] Edited Processor to include pumping calculation logger info --- .../processors/Sensitivity/CavitySensitivityCurveProcessor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mermithid/processors/Sensitivity/CavitySensitivityCurveProcessor.py b/mermithid/processors/Sensitivity/CavitySensitivityCurveProcessor.py index ffcd0385..cbac3ded 100644 --- a/mermithid/processors/Sensitivity/CavitySensitivityCurveProcessor.py +++ b/mermithid/processors/Sensitivity/CavitySensitivityCurveProcessor.py @@ -238,6 +238,8 @@ def InternalRun(self): self.sens_main.BackgroundRate() logger.info('RF background: {}/eV/s'.format(self.sens_main.RF_background_rate_per_eV*eV*s)) logger.info('Total background: {}/eV/s'.format(self.sens_main.background_rate*eV*s)) + logger.info("***Pumping Requirements:***") + self.sens_main.print_pumping_requirements() logger.info("***Done printing pre-optimization***") From 506e9b943b0a71a1ca7beaa470b9bff4667306ba Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Thu, 5 Feb 2026 00:40:17 -0500 Subject: [PATCH 27/57] Adding T2 background in atomic trap to Processor --- .../processors/Sensitivity/CavitySensitivityCurveProcessor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mermithid/processors/Sensitivity/CavitySensitivityCurveProcessor.py b/mermithid/processors/Sensitivity/CavitySensitivityCurveProcessor.py index cbac3ded..4dd76eb7 100644 --- a/mermithid/processors/Sensitivity/CavitySensitivityCurveProcessor.py +++ b/mermithid/processors/Sensitivity/CavitySensitivityCurveProcessor.py @@ -240,9 +240,10 @@ def InternalRun(self): logger.info('Total background: {}/eV/s'.format(self.sens_main.background_rate*eV*s)) logger.info("***Pumping Requirements:***") self.sens_main.print_pumping_requirements() + logger.info("***T2 background in atomic trap:***") + self.sens_main.print_T2_background_atomic_trap() logger.info("***Done printing pre-optimization***") - #Optimizing the detection threshold for the comparison config files #Before the density optimization for i in range(len(self.sens_ref)): From f3b8ef256c6fe9b53104cab1155d477c04558f56 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Thu, 5 Feb 2026 00:48:11 -0500 Subject: [PATCH 28/57] T2 background in atomic trap --- .../sensitivity/SensitivityCavityFormulas.py | 123 ++++++++++++++---- 1 file changed, 95 insertions(+), 28 deletions(-) diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index b832fb0a..e6823e98 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -29,25 +29,84 @@ # [m] - Fiducial distance from the cavity wall between max(ioffe_bite, larmor_radius) def ioffe_bite(nominal_field, magnetic_inhomogenity, ioffe_field, ioffe_multipolarity, cavity_radius): return ((2 * nominal_field**2 * magnetic_inhomogenity / (2 * nominal_field**2 * magnetic_inhomogenity + ioffe_field**2))**(1/(ioffe_multipolarity-2)) * cavity_radius * -1) + cavity_radius +''' +There are two possible methods for pumping away the tritium, all of which is eventually in molecular form. +One is to keep the tritium in circulation by using turbopumps and avoiding temperatures below 10K in the trap region, and the second is to cool parts of the trap region outside the magnetic wall to < 3K in order to cryopump the tritium. +The mechanical pumping method is impractical because of the pumping speed required, a fraction of a billion L/s, where an achievable upper limit is 4 orders of magnitude smaller. +The speed requirement is driven by the combination of the input atomic current and the need to maintain the molecular fraction below 10−4. +The cryopumping method ties up very large amounts of tritium, tens to hundreds of kCi, by the end of a day, and the need to warm up and recycle that tritium on such a short time scale would lead to low statistical precision and instabilities. +''' + +def calculate_T2_background_atomic_trap(cavity_radius, cavity_wall_temp, cavity_L_over_D, max_ratio_T2_T, number_density): + # volume of cone + top_plate_cavity = 5.30*m + top_cone = 0.60*m + # C156 [m^3] + physical_volume = np.pi * cavity_radius**2 * (top_plate_cavity - 2 * top_cone / 3) + # C278 = C271/sqrt(2) [m/s] + average_velocity = (c0 / np.sqrt(2)) * np.sqrt((8 * 0.025*(eV/K) * cavity_wall_temp / 273) / (np.pi * tritium_mass_atomic)) + # C334 [m^2] + trap_wall_area = 2 * np.pi * (2 * cavity_radius**2) * cavity_L_over_D + # C118 - Total activity on wall at recyling [s^-1] + activity = 100/s # Ci (Bq = 1 decay per second) + # C49 + molecules_desorbed_wall_beta = 1000 # per beta decay + # C28 + Ci_Bq = 3.7*10**10 # 1 Ci = 3.7e10 Bq + # C36 [eV] + atomic_tritium_recoil_energy = 3.409*eV + # C306 - Vapor Pressure of T2 w/ constants for saturated T2 vapor from Souers et al. [mbar] + T2_vapor_pressure = mbar * np.exp(5.84605 + (-160.7*K/cavity_wall_temp) + 2.3235 * np.log(cavity_wall_temp/K)) / 0.76 + # C307 - Density of saturated vapor [m^-3] + molar_volume = 2.24*10**-2*m**3 # Volume of 1mol of ideal gas at 1atm. Higher pressure --> smaller volume; higher temp --> lower volume. + sat_vapor_density = NA * (T2_vapor_pressure/mbar) * 273*K / (1000 * molar_volume * cavity_wall_temp) + # C308 - T2 density form desorption at end of a cycle: [m^-3] = s^-1 * sqrt(kg/eV) / m^2 + T2_density_desorp = 4 * molecules_desorbed_wall_beta * (Ci_Bq) * activity * np.sqrt(molecules_desorbed_wall_beta * (tritium_mass_atomic / c0**2) / (2 * atomic_tritium_recoil_energy * 1.6*10**-19 * (J/eV) )) / trap_wall_area + # C309 + T2_total_density = sat_vapor_density + T2_density_desorp + T2_T_ratio = T2_total_density / number_density + return T2_total_density, T2_T_ratio + +''' +# T2 Heat Leak: +# He Heat Leak: +''' + +''' # C328 - made into config file instead +#def turbopumping_speed_required(atom_current, molecular_density): +# return atom_current / 2 / molecular_density +''' + +# Turbopump Calculations: +# [m^3/s] - The mean speed of the cylinder connected to a perfect pump (Dushman). Uniform source, outputs mean density in cavity. Cannot exceed (obstruction not included). +# Need to convert tritium mass amu to eV : 1 amu ~ 931 MeV/c^2 and kB = 8.6E-05 eV/K. Multiply by sqrt(2) if atomic; # Useful for Atomic and Helium-3 +def turbopumping_speed_limit(cavity_radius, cavity_temperature, cavity_L_over_D): + return (np.pi * cavity_radius**2 * c0 * np.sqrt((8.6*10**-5 * eV / K) * cavity_temperature / (4 * np.pi * tritium_mass_atomic)) / (0.5 + cavity_L_over_D / 8)) + +def turbopump_speed(number_turbos, turbopumping_speed_gas): + return number_turbos * turbopumping_speed_gas + +# [m^3/s] - Assumed ambient room air (28-29 amu) temperature (293 K, and a pumping speed of 0.5 L/s +def cavity_termination_speed(turbopumping_speed_air, cavity_top_plate_temp): + return (turbopumping_speed_air * np.sqrt(cavity_top_plate_temp * 28 * amu * c0**2 / (293 * K * 2 * tritium_mass_atomic))) + +def ratio_required_theoretical_turbopumping_speed(pumping_speed_required, pumping_speed_limit, cavity_termination_speed, turbopump_speed): + return pumping_speed_required * ((1 / pumping_speed_limit) + (1 / cavity_termination_speed) + (1 / turbopump_speed)) + +# Atom Supply into Trap: +# Total current is atom current (d state only) + He heat leak + T2 heat leak +#def total_current(): +# return +# (No c state = 1, Include c state = 2). For total gas into trap; trap is always d-state only. The ‘atomic current required’ includes the c-state atoms as well as d-state, although those are lost almost immediately to spin exchange. +#def atom_current (c_state_flag = 1, total_current): +# return c_state_flag * total_current +# Molecular density allowed by molecular/atomic assuming total atom density in all of physical volume +def molecular_density_allowed(atom_density=1.5*10**17/m**3, max_ratio_nM_nA = 10**-4, GS_atomic_branch = 7.02*10**-1): + return atom_density * max_ratio_nM_nA * GS_atomic_branch / 2 + -# [m^3/s] - The mean speed of the cylinder connected to a perfect pump (Dushman). Uniform source, outputs mean density in cavity. -# may need to convert tritium mass atomic to eV : eV/amu = 931494100 -def pumping_speed_limit_molecular(cavity_radius, cavity_temperature, cavity_L_over_D): - #self.FrequencyExtraction.cavity_temperature - #self.Experiment.cavity_L_over_D - return (np.pi * cavity_radius**2 * c0 * np.sqrt(kB * cavity_temperature / (4 * np.pi * tritium_mass_atomic)) / (0.5 + cavity_L_over_D / 8)) -# Useful for Atomic and Helium-3 -def pumping_speed_limit_atomic(cavity_radius, cavity_temperature, cavity_L_over_D): - return (np.pi * cavity_radius**2 * c0 * np.sqrt(kB * cavity_temperature / (2 * np.pi * tritium_mass_atomic)) / (0.5 + cavity_L_over_D / 8)) -def turbopump_speed(number_turbos, pumping_speed_gas): - return number_turbos * pumping_speed_gas -# [m^3/s] - Assumed ambient room temperature and a pumping speed of 0.5 L/s -def cavity_termination_speed_molecular(pumping_speed, cavity_top_plate_temp): - return (pumping_speed * np.sqrt(cavity_top_plate_temp * 28 / (293 * 2 * tritium_mass_atomic))) -def cavity_termination_speed_atomic(): - return (pumping_speed * np.sqrt(cavity_top_plate_temp * 28 / (293 * tritium_mass_atomic))) # Wouters functinos @@ -269,7 +328,7 @@ class CavitySensitivity(Sensitivity): * Molecular contamination in atomic tritium: https://3.basecamp.com/3700981/buckets/3107037/documents/3151077016 """ def __init__(self, config_path, verbose=True): - Sensitivity.__init__(self, config_path, verbose=verbose) + Sensitivity.__init__(self, config_path) # Calc non-config parameters outside of init function: ## Allows re-calcing params if config values changed later, e.g. param scans @@ -862,7 +921,7 @@ def print_SNRs(self, rho=None): track_duration = self.time_window logger.info("SNR-related parameters are printed for pre-set number density.") else: - track_duration = track_length(rho, self.T_endpoint, molecular=(not self.Experiment.atomic)) + track_duration = track_length(rho, self.T_endpoint, molecular=(not self.Experiment.atomic)) tau_snr_90deg = self.calculate_tau_snr(track_duration, power_fraction=1) #For an example carrier: @@ -919,19 +978,27 @@ def print_Efficiencies(self): logger.info("SRI factor: {}".format(self.Experiment.sri_factor)) def print_pumping_requirements(self): + #logger.info("Pumping Calculation: {}".format(self.Efficiency.pumping_calculation)) if self.Efficiency.pumping_calculation: - self.turbopump_speed = turbopump_speed(self.Efficiency.number_turbopumps, self.Efficiency.pumping_speed_gas_T2) - logger.info("Turbopump Speed: {}".format(self.turbopump_speed * s / m**3)) + self.turbopump_speed = turbopump_speed(self.Efficiency.number_turbopumps, self.Efficiency.turbopumping_speed_gas_T2) + logger.info("Turbopump Speed: {} m^3/s".format(self.turbopump_speed * s / m**3)) + self.turbopumping_speed_limit = turbopumping_speed_limit(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D) + self.cavity_termination_speed = cavity_termination_speed(self.Efficiency.turbopumping_speed_cavity_termination_air, self.Efficiency.cavity_top_plate_temperature) if self.Experiment.atomic: - self.pumping_speed_limit_atomic = pumping_speed_limit_atomic(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D) - self.cavity_termination_speed_atomic = cavity_termination_speed_atomic(self.Efficiency.pumping_speed_cavity_termination_air, self.Efficiency.cavity_top_plate_temperature) - logger.info("Pumping Speed Limit(Atomic): {}".format(self.pumping_speed_limit_atomic * s / m**3)) - logger.info("Cavity Termination Speed (Atomic): {}".format(self.cavity_termination_speed_atomic * s / m**3)) + logger.info("Pumping Speed Limit(Atomic): {} m^3/s".format((self.turbopumping_speed_limit * np.sqrt(2)) * s / m**3)) + logger.info("Cavity Termination Speed (Atomic): {} m^3/s".format((self.cavity_termination_speed * np.sqrt(2)) * s / m**3)) else: - self.pumping_speed_limit_molecular = pumping_speed_limit_molecular(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D) - self.cavity_termination_speed_molecular = cavity_termination_speed_molecular(self.Efficiency.pumping_speed_cavity_termination_air, self.Efficiency.cavity_top_plate_temperature) - logger.info("Pumping Speed Limit(Molecular): {}".format(self.pumping_speed_limit_molecular * s / m**3)) - logger.info("Cavity Termination Speed (Molecular): {}".format(self.cavity_termination_speed_molecular * s / m**3)) + logger.info("Pumping Speed Limit(Molecular): {} m^3/s".format(self.turbopumping_speed_limit * s / m**3)) + logger.info("Cavity Termination Speed (Molecular): {} m^3/s".format(self.cavity_termination_speed * s / m**3)) + self.ratio_required_theoretical_turbopumping_speed = ratio_required_theoretical_turbopumping_speed(self.Efficiency.pumping_speed_required, self.turbopumping_speed_limit, self.cavity_termination_speed, self.turbopump_speed) + logger.info("Ratio of Turbopump Speed Limit: {}".format(self.ratio_required_theoretical_turbopumping_speed)) + + def print_T2_background_atomic_trap(self): + #logger.info("T2 background: {}".format(self.Efficiency.T2_background_atomic_trap)) + if self.Efficiency.T2_background_atomic_trap: + T2_total_density, T2_T_ratio = calculate_T2_background_atomic_trap(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D, self.Efficiency.max_ratio_T2_T, self.Experiment.number_density) + logger.info("T2_total_density: {} m^-3".format(T2_total_density*m**3)) + logger.info("Ratio T2/T: {}".format(T2_T_ratio)) """ # Cramer-Rao lower bound / how much worse are we than the lower bound From d4b9bcfdd26bdc8f96667899c3801f644fcdef6b Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Fri, 6 Feb 2026 16:22:58 -0500 Subject: [PATCH 29/57] Modified logger output for T2 bkg and pumping calc --- .../Sensitivity/CavitySensitivityCurveProcessor.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mermithid/processors/Sensitivity/CavitySensitivityCurveProcessor.py b/mermithid/processors/Sensitivity/CavitySensitivityCurveProcessor.py index 4dd76eb7..725b0bf6 100644 --- a/mermithid/processors/Sensitivity/CavitySensitivityCurveProcessor.py +++ b/mermithid/processors/Sensitivity/CavitySensitivityCurveProcessor.py @@ -238,10 +238,12 @@ def InternalRun(self): self.sens_main.BackgroundRate() logger.info('RF background: {}/eV/s'.format(self.sens_main.RF_background_rate_per_eV*eV*s)) logger.info('Total background: {}/eV/s'.format(self.sens_main.background_rate*eV*s)) - logger.info("***Pumping Requirements:***") - self.sens_main.print_pumping_requirements() - logger.info("***T2 background in atomic trap:***") - self.sens_main.print_T2_background_atomic_trap() + if self.sens_main.Efficiency.pumping_calculation: + logger.info("***Pumping Requirements:***") + self.sens_main.print_pumping_requirements() + if self.sens_main.Efficiency.T2_background_atomic_trap: + logger.info("***T2 background in atomic trap:***") + self.sens_main.print_T2_background_atomic_trap() logger.info("***Done printing pre-optimization***") #Optimizing the detection threshold for the comparison config files From cedf93b735c8a71ea5bf5da3979f5aa762def1b7 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Mon, 9 Feb 2026 18:11:39 -0500 Subject: [PATCH 30/57] Gas density documentation update + logger info update --- documentation/sensitivity_configurable_parameters.rst | 2 +- .../processors/Sensitivity/CavitySensitivityCurveProcessor.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/documentation/sensitivity_configurable_parameters.rst b/documentation/sensitivity_configurable_parameters.rst index 93a1e8b4..5e9f3662 100644 --- a/documentation/sensitivity_configurable_parameters.rst +++ b/documentation/sensitivity_configurable_parameters.rst @@ -38,7 +38,7 @@ Below is a list of the parameters with a short description of what role they pla * ``livetime``: Run duration of the experiment. Together with the total volume, efficiency, and the gas density, this determines the statistical power of the experiment. * ``n_cavities``: Number of identical cavities in the experimennt. This parameter multiplies the single cavity volume to give the total experimental volume. * ``background_rate_per_ev``: Background rate per electronvolt for the entire experiment. It is not automultiplied by the number of channels (cavities) in the experiment. -* ``number_density``: The gas number density together with the total volume, livetime, and the efficiency determines the statistical power of the experiment. Gas density also determines the track length and therefore the frequency resolution. The sensitivity curve processor can optimize this parameter to maximize the sensitivity. In that case this number is overwritten in the calculation. +* ``number_density``: (Mean density in cavity b/w trap coils) The gas number density together with the total volume, livetime, and the efficiency determines the statistical power of the experiment. Gas density also determines the track length and therefore the frequency resolution. The sensitivity curve processor can optimize this parameter to maximize the sensitivity. In that case this number is overwritten in the calculation. * ``sri_factor``: The statistical rate increase factor articifially increases the number of observed events (it multiplies the total efficiency). It is highly recommended to set it to 1. * ``atomic``: If true, the calculation is done for atomic tritium. If false, moecular tritium is assumed. This affects the number of decays per gas molecule/atom (2 for molecular 1 for atomic), the track length in a given gas density (via electron scattering cross section), and the width of the final ground state. * ``active gas fraction``: The fraction of the gas within the CRES volume that's tritium. Very high (~1) in at atomic experiment. Lower in a molecular experiment due to presence of HT, H2, DT, and 3He, and possibly other gasses, too. diff --git a/mermithid/processors/Sensitivity/CavitySensitivityCurveProcessor.py b/mermithid/processors/Sensitivity/CavitySensitivityCurveProcessor.py index 4dd76eb7..206be130 100644 --- a/mermithid/processors/Sensitivity/CavitySensitivityCurveProcessor.py +++ b/mermithid/processors/Sensitivity/CavitySensitivityCurveProcessor.py @@ -238,10 +238,10 @@ def InternalRun(self): self.sens_main.BackgroundRate() logger.info('RF background: {}/eV/s'.format(self.sens_main.RF_background_rate_per_eV*eV*s)) logger.info('Total background: {}/eV/s'.format(self.sens_main.background_rate*eV*s)) - logger.info("***Pumping Requirements:***") - self.sens_main.print_pumping_requirements() logger.info("***T2 background in atomic trap:***") self.sens_main.print_T2_background_atomic_trap() + logger.info("***Pumping Requirements:***") + self.sens_main.print_pumping_requirements() logger.info("***Done printing pre-optimization***") #Optimizing the detection threshold for the comparison config files From 9d311e819ae612c37afd13e9af7043dce3ad785a Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Mon, 9 Feb 2026 22:13:14 -0500 Subject: [PATCH 31/57] Added constants and cross-sections from atomic calculator --- mermithid/misc/Constants_numericalunits.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/mermithid/misc/Constants_numericalunits.py b/mermithid/misc/Constants_numericalunits.py index 672fac6f..9344f1d0 100644 --- a/mermithid/misc/Constants_numericalunits.py +++ b/mermithid/misc/Constants_numericalunits.py @@ -33,6 +33,21 @@ gyro_mag_ratio_proton = 42.577*MHz/T +# Atomic Calculator +# C008 - [m^3] Volume of 1mol of ideal gas at 1atm. Higher pressure --> smaller volume; higher temp --> lower volume +molar_volume = 2.24*10**-2*m**3 +# C009 - [m^2] H-He Cross-section at low temp but > 5K (Berlinsky) +H_He_cross_section = 2e-19 * m**2 +# C012 - [m^2] Hard Spheres cross-section (289 pm kinetic diameter +H_H2_cross_section = 2.62e-19*m**2 +# C049 - per beta decay +molecules_desorbed_wall_beta = 1000 +# C028 - 1 Ci = 3.7e10 Bq +Ci_Bq = 3.7*10**10 +# C036 - [eV] Bodine +atomic_tritium_recoil_energy = 3.409*eV + + # units that do not show up in numericalunits # missing pre-factors fW = W*1e-15 From c889f106b8cbd2ff3fbf711d54ffb496673eb545 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Tue, 10 Feb 2026 00:46:53 -0500 Subject: [PATCH 32/57] Constants for gravity + dipolar trap loss calculations --- mermithid/misc/Constants_numericalunits.py | 12 +- .../sensitivity/SensitivityCavityFormulas.py | 197 +++++++++++++----- 2 files changed, 155 insertions(+), 54 deletions(-) diff --git a/mermithid/misc/Constants_numericalunits.py b/mermithid/misc/Constants_numericalunits.py index 9344f1d0..5256a9ba 100644 --- a/mermithid/misc/Constants_numericalunits.py +++ b/mermithid/misc/Constants_numericalunits.py @@ -14,12 +14,12 @@ from numericalunits import Pa, bar, mbar, atm, torr, mtorr, L, mL # Pressure, Volume T0 = -273.15*K - +gravity = 9.80 * m/s**2 tritium_livetime = 5.605e8*s tritium_mass_atomic = 3.016* amu *c0**2 tritium_electron_crosssection_atomic = 9.e-23*m**2 #Inelastic cross-section Hamish extrapolated to 18.6keV using Shah et al. (1987): https://iopscience.iop.org/article/10.1088/0022-3700/20/14/022. Full Bethe formula must be exactly right (down to magnetic and QED corrections) for the hydrogen atom. #tritium_electron_crosssection_atomic = 1.32e-22*m**2 #Inelastic cross-section + Elastic cross-section for T - +tritium_tritium_crosssection_atomic = 4.40e-16*m**2 # T-T cross-section at T=0 tritium_endpoint_atomic = 18563.251*eV last_1ev_fraction_atomic = 2.067914e-13/eV**3 @@ -37,17 +37,17 @@ # C008 - [m^3] Volume of 1mol of ideal gas at 1atm. Higher pressure --> smaller volume; higher temp --> lower volume molar_volume = 2.24*10**-2*m**3 # C009 - [m^2] H-He Cross-section at low temp but > 5K (Berlinsky) -H_He_cross_section = 2e-19 * m**2 +H_He_crosssection = 2e-19 * m**2 # C012 - [m^2] Hard Spheres cross-section (289 pm kinetic diameter -H_H2_cross_section = 2.62e-19*m**2 +H_H2_crosssection = 2.62e-19*m**2 # C049 - per beta decay molecules_desorbed_wall_beta = 1000 # C028 - 1 Ci = 3.7e10 Bq Ci_Bq = 3.7*10**10 # C036 - [eV] Bodine atomic_tritium_recoil_energy = 3.409*eV - - +# C048 - Multiplier for Lagendijk G^d rates: Ben Jones, Morgan Elliott CM presentation 10/24 +LGd_rates = 50 # units that do not show up in numericalunits # missing pre-factors fW = W*1e-15 diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index e6823e98..b1704b52 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -1,6 +1,6 @@ ''' Class calculating neutrino mass sensitivities based on analytic formulas from CDR. -Author: R. Reimann, C. Claessens, T. E. Weiss, W. Van De Pontseele +Author: R. Reimann, C. Claessens, T. E. Weiss, W. Van De Pontseele, M. Oueslati Date: 06/07/2023 Updated: December 2024 @@ -26,73 +26,153 @@ print("Run without morpho!") # Jins functions - Atomic Calculator +# Python decides whether a name is local or global at compile time (not runtime). The name must already exist in the module’s global namespace by the time the function runs, or Python has nowhere to bind it. +area_loading_aperture = (None) + # [m] - Fiducial distance from the cavity wall between max(ioffe_bite, larmor_radius) def ioffe_bite(nominal_field, magnetic_inhomogenity, ioffe_field, ioffe_multipolarity, cavity_radius): return ((2 * nominal_field**2 * magnetic_inhomogenity / (2 * nominal_field**2 * magnetic_inhomogenity + ioffe_field**2))**(1/(ioffe_multipolarity-2)) * cavity_radius * -1) + cavity_radius ''' -There are two possible methods for pumping away the tritium, all of which is eventually in molecular form. -One is to keep the tritium in circulation by using turbopumps and avoiding temperatures below 10K in the trap region, and the second is to cool parts of the trap region outside the magnetic wall to < 3K in order to cryopump the tritium. +There are two possible methods for pumping away the tritium, all of which is eventually in molecular form. One is to keep the tritium in circulation by using turbopumps and avoiding temperatures below 10K in the trap region, and the second is to cool parts of the trap region outside the magnetic wall to < 3K in order to cryopump the tritium. The mechanical pumping method is impractical because of the pumping speed required, a fraction of a billion L/s, where an achievable upper limit is 4 orders of magnitude smaller. The speed requirement is driven by the combination of the input atomic current and the need to maintain the molecular fraction below 10−4. The cryopumping method ties up very large amounts of tritium, tens to hundreds of kCi, by the end of a day, and the need to warm up and recycle that tritium on such a short time scale would lead to low statistical precision and instabilities. ''' - -def calculate_T2_background_atomic_trap(cavity_radius, cavity_wall_temp, cavity_L_over_D, max_ratio_T2_T, number_density): - # volume of cone - top_plate_cavity = 5.30*m - top_cone = 0.60*m - # C156 [m^3] - physical_volume = np.pi * cavity_radius**2 * (top_plate_cavity - 2 * top_cone / 3) - # C278 = C271/sqrt(2) [m/s] - average_velocity = (c0 / np.sqrt(2)) * np.sqrt((8 * 0.025*(eV/K) * cavity_wall_temp / 273) / (np.pi * tritium_mass_atomic)) - # C334 [m^2] +# C271 - [m/s] Atomic thermal speed of 3T or 3He (C270) +def calculate_average_velocity(temp): + average_velocity = c0 * np.sqrt((8 * 0.025*(eV/K) * temp / 273) / (np.pi * tritium_mass_atomic)) + return average_velocity + +# (Cavity length is cavity top plate - top of cone / 2) +# C334 - [m^2] Cylinder wall; no endcaps included +def calculate_trap_wall_area(cavity_radius, cavity_L_over_D): trap_wall_area = 2 * np.pi * (2 * cavity_radius**2) * cavity_L_over_D - # C118 - Total activity on wall at recyling [s^-1] - activity = 100/s # Ci (Bq = 1 decay per second) - # C49 - molecules_desorbed_wall_beta = 1000 # per beta decay - # C28 - Ci_Bq = 3.7*10**10 # 1 Ci = 3.7e10 Bq - # C36 [eV] - atomic_tritium_recoil_energy = 3.409*eV - # C306 - Vapor Pressure of T2 w/ constants for saturated T2 vapor from Souers et al. [mbar] + return trap_wall_area + +# C204 - [atoms] Inventory in physical volume +def calculate_inventory(design_density, volume): + return design_density * volume + +# C208 - [decays/s] Radioactivity in trap per cavity +def calculate_activity_in_trap(number_density, cavity_radius): + # C090 + coil_1_height = 0.75*m + # C091 + coil_2_height = 4.80*m + # C206 - [atoms] + atoms_between_trap_coils = number_density * np.pi * cavity_radius**2 * np.absolute(coil_2_height - coil_1_height) + return atoms_between_trap_coils / tritium_livetime + +# Background T2 in atomic trap +def calculate_T2_background_atomic_trap(cavity_radius, cavity_wall_temp, cavity_L_over_D, max_ratio_T2_T, number_density, wall_activity): + trap_wall_area = calculate_trap_wall_area(cavity_radius, cavity_L_over_D) + # C306 - [mbar] Vapor Pressure of T2 w/ constants for saturated T2 vapor from Souers et al. T2_vapor_pressure = mbar * np.exp(5.84605 + (-160.7*K/cavity_wall_temp) + 2.3235 * np.log(cavity_wall_temp/K)) / 0.76 - # C307 - Density of saturated vapor [m^-3] - molar_volume = 2.24*10**-2*m**3 # Volume of 1mol of ideal gas at 1atm. Higher pressure --> smaller volume; higher temp --> lower volume. + # C307 - [m^-3] Density of saturated vapor sat_vapor_density = NA * (T2_vapor_pressure/mbar) * 273*K / (1000 * molar_volume * cavity_wall_temp) - # C308 - T2 density form desorption at end of a cycle: [m^-3] = s^-1 * sqrt(kg/eV) / m^2 - T2_density_desorp = 4 * molecules_desorbed_wall_beta * (Ci_Bq) * activity * np.sqrt(molecules_desorbed_wall_beta * (tritium_mass_atomic / c0**2) / (2 * atomic_tritium_recoil_energy * 1.6*10**-19 * (J/eV) )) / trap_wall_area - # C309 + # C308 - [m^-3] = s^-1 * sqrt(kg/eV) / m^2 T2 density form desorption at end of a cycle + T2_density_desorp = 4 * molecules_desorbed_wall_beta * Ci_Bq * wall_activity * np.sqrt(molecules_desorbed_wall_beta * (tritium_mass_atomic / c0**2) / (2 * atomic_tritium_recoil_energy * 1.6*10**-19 * (J/eV) )) / trap_wall_area + # C309 [m^-3] Total T2 density T2_total_density = sat_vapor_density + T2_density_desorp + # C310 T2/T number ratio. Activity ratio is 1.64 times bigger + # Number density b/w trap coils varies between Mermithid and Atomic Calculator due to inelastic + elastic T2-e cross-section T2_T_ratio = T2_total_density / number_density return T2_total_density, T2_T_ratio -''' -# T2 Heat Leak: -# He Heat Leak: -''' +# He Heat Leak: Notable difference comes from pumping speed, physical volume, and design density +def calculate_He_heat_leak(pumping_speed_theoretical, pumping_cavity_termination, turbopump_speed, cavity_temperature, design_density, volume, wall_activity): + He_velocity = calculate_average_velocity(cavity_temperature) + # C266 - [atoms/s] Not including flow for He heat leak itself + He_production_rate = wall_activity * Ci_Bq + # C269 - [m^-3] Number density of Helium + He_density = He_production_rate * (1 / pumping_speed_theoretical + 1 / pumping_cavity_termination + 1 / turbopump_speed) + # C272 - [s] Time constant for loss due to He-3 heat leak + time_constant_He = 1 / (He_density * He_velocity * H_He_crosssection) + # C273 - [s^-1] T atom current required to keep up with 3He heat leak + T_current_He_heat = calculate_inventory(design_density, volume) / time_constant_He + return time_constant_He, T_current_He_heat + +# Aperture Heat Leak: +def calculate_aperture_heat_leak(trapped_gas_temp, design_density, volume): + global area_loading_aperture + # C108 - [m^2] Choose an area. (determines injection speed and density.) + area_loading_aperture = 0.001*m**2 + # C250 - [m/s] average velocity of trapped gas + trapped_gas_velocity = calculate_average_velocity(trapped_gas_temp) + # C251 - [atom/s] inward atom current required through aperture to balance losses (j_aperture) + current_aperture = area_loading_aperture * design_density * trapped_gas_velocity / 4 + # C252 - [s] Time constant for loss through aperture + time_constant_aperture = calculate_inventory(design_density, volume) / current_aperture + return time_constant_aperture, current_aperture + +# Radioactivity Heat Leak: +def calculate_rad_heat_leak(cavity_radius, number_density, cavity_L_over_D, design_density, volume, net_efficiency): + # C256 [s] Input from Ben Clark's thesis with a mirror ratio of 0.5 + half_life = 11 * 24 * 3600 * s + trap_wall_area = calculate_trap_wall_area(cavity_radius, cavity_L_over_D) + trap_activity = calculate_activity_in_trap(number_density, cavity_radius) + # C257 - [atoms/s] Atom current required to balance loss due to radioactivity + current_rad = trap_activity * np.log(2) * tritium_livetime / (net_efficiency * half_life) + # C258 - [s] Time constant for loss due to radioactivty (tau_rad) + time_constant_rad = calculate_inventory(design_density, volume) / current_rad + return time_constant_rad, current_rad + +# T2 Desoprtion from the wall +def calculate_T2_desorption_from_wall(cavity_radius, cavity_L_over_D, wall_activity, design_density, volume): + trap_wall_area = calculate_trap_wall_area(cavity_radius, cavity_L_over_D) + # C261 - [s] mean lifetime of atom in trap from desorption + time_constant_desorp = trap_wall_area / (2 * molecules_desorbed_wall_beta * wall_activity * Ci_Bq * H_H2_crosssection) + # C262 - [atoms/s] current required to keep up + current_desorp = calculate_inventory(design_density, volume) / time_constant_desorp + return time_constant_desorp, current_desorp + +# Evaporation + + +# Dipolar +def calculate_dipolar_loss(nominal_field, cavity_radius, design_density, volume, trapped_gas_temp, cavity_L_over_D, pure_magnetic_flag): + # C089 + top_of_cone = 0.60*m + # C092 + top_plate_cavity = 5.30*m + # C201 + gravity_temperature_scale = ((tritium_mass_atomic/c0**2 * gravity / (kB * trapped_gas_temp)) * np.absolute(pure_magnetic_flag * (1 + 1 / cavity_L_over_D) - 1)) + # C202 surface_density_scale = 8.024e17/m**2 + surface_density_scale = design_density * top_plate_cavity / (1 - np.exp(-gravity_temperature_scale * top_plate_cavity)) + # C298 - [m^3/s] Dipolar spin-flip rate (G_dd). Depends on field (Lagendijk et al). Polynomial-log fit used now. + dipolar_spin_flip_rate = (60.106 + 13.812 * np.log(nominal_field) - 4.7867 * np.log(nominal_field)**2 - 2.3192 * np.log(nominal_field)**3 \ + - 0.32663 * np.log(nominal_field)**4 - 0.015775 * np.log(nominal_field)**5) * 1e-22 * LGd_rates * m**3/s + # C299 - [atoms/s] Flow to keep up with dipolar losses + current_dipolar = dipolar_spin_flip_rate * np.pi * cavity_radius**2 * surface_density_scale**2 * (gravity_temperature_scale * (np.exp(-gravity_temperature_scale * top_of_cone) \ + - np.exp(-gravity_temperature_scale * top_plate_cavity)) / 2 + (1 - np.exp(-2 * gravity_temperature_scale * top_of_cone) * (2 * gravity_temperature_scale**2 \ + * top_of_cone**2 + 2 * gravity_temperature_scale * top_of_cone + 1)) / (4 * top_of_cone**2 * gravity_temperature_scale)) + # C300 - Time constant for loss due to dipolar spin-flip loss + time_constant_dipolar = calculate_inventory(design_density, volume) / current_dipolar + return time_constant_dipolar, current_dipolar +# T2 Heat Leak: +#def calculate_T2_heat_leak(trapped_gas_temp, design_density, volume): + # C277 - [atoms/s] T2 production rate (all atoms entering, except for T2 heat leak itself) + #T2_production_rate = ''' # C328 - made into config file instead #def turbopumping_speed_required(atom_current, molecular_density): # return atom_current / 2 / molecular_density ''' -# Turbopump Calculations: -# [m^3/s] - The mean speed of the cylinder connected to a perfect pump (Dushman). Uniform source, outputs mean density in cavity. Cannot exceed (obstruction not included). -# Need to convert tritium mass amu to eV : 1 amu ~ 931 MeV/c^2 and kB = 8.6E-05 eV/K. Multiply by sqrt(2) if atomic; # Useful for Atomic and Helium-3 +# Turbopump Calculations: Need to convert tritium mass amu to eV : 1 amu ~ 931 MeV/c^2 and kB = 8.6E-05 eV/K. Multiply by sqrt(2) if atomic; # Useful for Atomic and Helium-3 +# C233/C236 - [m^3/s] (molecular/atomic) theoretical pumping speed. The mean speed of the cylinder connected to a perfect pump (Dushman). Uniform source, outputs mean density in cavity. Cannot exceed (obstruction not included). def turbopumping_speed_limit(cavity_radius, cavity_temperature, cavity_L_over_D): return (np.pi * cavity_radius**2 * c0 * np.sqrt((8.6*10**-5 * eV / K) * cavity_temperature / (4 * np.pi * tritium_mass_atomic)) / (0.5 + cavity_L_over_D / 8)) - +# C239 - [m^3/s] Turbopump in series speed (2.5 m^3/s * 2) def turbopump_speed(number_turbos, turbopumping_speed_gas): return number_turbos * turbopumping_speed_gas - -# [m^3/s] - Assumed ambient room air (28-29 amu) temperature (293 K, and a pumping speed of 0.5 L/s +# C235/C238 - [m^3/s] (molecular/atomic) Assumed ambient room air (28-29 amu) temperature (293 K, and a pumping speed of 0.5 L/s def cavity_termination_speed(turbopumping_speed_air, cavity_top_plate_temp): return (turbopumping_speed_air * np.sqrt(cavity_top_plate_temp * 28 * amu * c0**2 / (293 * K * 2 * tritium_mass_atomic))) - +# C331 - If above 1000, critical def ratio_required_theoretical_turbopumping_speed(pumping_speed_required, pumping_speed_limit, cavity_termination_speed, turbopump_speed): return pumping_speed_required * ((1 / pumping_speed_limit) + (1 / cavity_termination_speed) + (1 / turbopump_speed)) - +''' # Atom Supply into Trap: # Total current is atom current (d state only) + He heat leak + T2 heat leak #def total_current(): @@ -103,7 +183,7 @@ def ratio_required_theoretical_turbopumping_speed(pumping_speed_required, pumpin # Molecular density allowed by molecular/atomic assuming total atom density in all of physical volume def molecular_density_allowed(atom_density=1.5*10**17/m**3, max_ratio_nM_nA = 10**-4, GS_atomic_branch = 7.02*10**-1): return atom_density * max_ratio_nM_nA * GS_atomic_branch / 2 - +''' @@ -977,6 +1057,13 @@ def print_Efficiencies(self): logger.info("Efficiency from axial frequency cut: {}".format(self.fa_cut_efficiency)) logger.info("SRI factor: {}".format(self.Experiment.sri_factor)) + def print_T2_background_atomic_trap(self): + #logger.info("T2 background: {}".format(self.Efficiency.T2_background_atomic_trap)) + if self.Efficiency.T2_background_atomic_trap: + T2_total_density, T2_T_ratio = calculate_T2_background_atomic_trap(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D, self.Efficiency.max_ratio_T2_T, self.Experiment.number_density, self.Efficiency.wall_activity) + logger.info("T2_total_density: {} m^-3".format(T2_total_density*m**3)) + logger.info("Ratio T2/T: {}".format(T2_T_ratio)) + def print_pumping_requirements(self): #logger.info("Pumping Calculation: {}".format(self.Efficiency.pumping_calculation)) if self.Efficiency.pumping_calculation: @@ -992,14 +1079,28 @@ def print_pumping_requirements(self): logger.info("Cavity Termination Speed (Molecular): {} m^3/s".format(self.cavity_termination_speed * s / m**3)) self.ratio_required_theoretical_turbopumping_speed = ratio_required_theoretical_turbopumping_speed(self.Efficiency.pumping_speed_required, self.turbopumping_speed_limit, self.cavity_termination_speed, self.turbopump_speed) logger.info("Ratio of Turbopump Speed Limit: {}".format(self.ratio_required_theoretical_turbopumping_speed)) - - def print_T2_background_atomic_trap(self): - #logger.info("T2 background: {}".format(self.Efficiency.T2_background_atomic_trap)) - if self.Efficiency.T2_background_atomic_trap: - T2_total_density, T2_T_ratio = calculate_T2_background_atomic_trap(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D, self.Efficiency.max_ratio_T2_T, self.Experiment.number_density) - logger.info("T2_total_density: {} m^-3".format(T2_total_density*m**3)) - logger.info("Ratio T2/T: {}".format(T2_T_ratio)) - + if self.Efficiency.He_heat_leak: + self.time_constant_He, self.T_current_He_heat = calculate_He_heat_leak(self.turbopumping_speed_limit, self.cavity_termination_speed, self.turbopump_speed, self.FrequencyExtraction.cavity_temperature, self.Experiment.design_density, self.total_cavity_volume, self.Efficiency.wall_activity) + #self.time_constant_He, self.T_current_He_heat = calculate_He_heat_leak(9.68*m**3/s, 0.78*m**3/s, 5*m**3/s, self.FrequencyExtraction.cavity_temperature, 1.5e17*m**-3, 1.645*m**3, self.Efficiency.wall_activity) + logger.info("He Time Constant: {} s".format(self.time_constant_He/s)) + logger.info("T atom current required to keep up with He-3 leak: {} atoms/s".format(self.T_current_He_heat*s)) + if self.Efficiency.T2_heat_leak: + self.time_constant_aperture, self.current_aperture = calculate_aperture_heat_leak(self.DopplerBroadening.gas_temperature, self.Experiment.design_density, self.total_cavity_volume) + logger.info("Aperture Time Constant: {} s".format(self.time_constant_aperture/s)) + logger.info("Current through aperture leak: {} atoms/s".format(self.current_aperture*s)) + self.time_constant_rad, self.current_rad = calculate_rad_heat_leak(self.cavity_radius, self.Experiment.number_density, self.Experiment.cavity_L_over_D, self.Experiment.design_density, self.total_cavity_volume, self.Efficiency.net_rad_efficiency) + logger.info("Radiation Time Constant: {} s".format(self.time_constant_rad/s)) + logger.info("Current through radiation leak: {} atoms/s".format(self.current_rad*s)) + self.time_constant_desorp, self.current_desorp = calculate_T2_desorption_from_wall(self.cavity_radius, self.Experiment.cavity_L_over_D, self.Efficiency.wall_activity, self.Experiment.design_density, self.total_cavity_volume) + logger.info("Desorption Time Constant: {} s".format(self.time_constant_desorp/s)) + logger.info("Current through desorption: {} atoms/s".format(self.current_desorp*s)) + self.time_constant_dipolar, self.current_dipolar = calculate_dipolar_loss(self.MagneticField.nominal_field/T, self.cavity_radius, self.Experiment.design_density, self.total_cavity_volume, self.DopplerBroadening.gas_temperature, self.Experiment.cavity_L_over_D, self.Experiment.pure_magnetic_flag) + if not self.Experiment.pure_magnetic_flag: + logger.info("***Magnetogravitional Trap***") # 0 is false in python + else: + logger.info("***Magnetic Trap***") + logger.info("Dipolar Time Constant: {} s".format(self.time_constant_dipolar/s)) + logger.info("Current Dipolar: {} atoms/s".format(self.current_dipolar*s)) """ # Cramer-Rao lower bound / how much worse are we than the lower bound ScalingFactorCRLB = self.FrequencyExtraction.CRLB_scaling_factor From 8c8d78ce0be15aa9779a00e1b9f2cefb62d99411 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Tue, 10 Feb 2026 16:54:41 -0500 Subject: [PATCH 33/57] Edited cavity formula to incorporate volume with cone --- .../sensitivity/SensitivityCavityFormulas.py | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index b1704b52..da70fa9a 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -28,6 +28,16 @@ # Jins functions - Atomic Calculator # Python decides whether a name is local or global at compile time (not runtime). The name must already exist in the module’s global namespace by the time the function runs, or Python has nowhere to bind it. area_loading_aperture = (None) +''' +Molecular experiment updated – no change but tidier. +Add the gravity-temperature scale parameter “b” for magnetogravitational trap +Add the surface density scale parameter “a/b” for magnetogravitational trap +Use exponential density vertical gradient, calculate mean density between trap coils. +Calculate atoms and activity between the trap coils. +Correct the T2/T ratio for the revised T activity in the trap. +Evaporation now in detail, separately for up (gravitation) and magnetic. +Dipolar loss rate. Calculation with z-dependent density, cylinder & cone. +''' # [m] - Fiducial distance from the cavity wall between max(ioffe_bite, larmor_radius) def ioffe_bite(nominal_field, magnetic_inhomogenity, ioffe_field, ioffe_multipolarity, cavity_radius): @@ -548,11 +558,18 @@ def CavityRadius(self): axial_mode_index = 1 self.cavity_radius = c0/(2*np.pi*self.cavity_freq)*np.sqrt(self.Jprime_0**2+axial_mode_index**2*np.pi**2/(4*self.Experiment.cavity_L_over_D**2)) return self.cavity_radius - def CavityVolume(self): - #radius = 0.5*wavelength(self.T_endpoint, self.MagneticField.nominal_field) - self.total_cavity_volume = 2*self.cavity_radius*self.Experiment.cavity_L_over_D*np.pi*(self.cavity_radius)**2*self.Experiment.n_cavities - + #Calculate vacuum volume including cone and top service volume + if self.Experiment.cavity_cone_flag: + self.top_of_cone = 0.60*m + self.first_trap_coil_height = 0.75*m + self.second_trap_coil_height = 4.80*m + self.top_plate_cavity = 5.30*m + self.top_vacuum_system = 7.50*m + self.total_cavity_volume = np.pi * self.cavity_radius**2 * (self.top_plate_cavity - (2/3) * self.top_of_cone) * self.Experiment.n_cavities + else: + #radius = 0.5*wavelength(self.T_endpoint, self.MagneticField.nominal_field) + self.total_cavity_volume = 2*self.cavity_radius*self.Experiment.cavity_L_over_D*np.pi*(self.cavity_radius)**2*self.Experiment.n_cavities logger.info("Frequency: {} MHz".format(round(self.cavity_freq/MHz, 3))) logger.info("Wavelength: {} cm".format(round(wavelength(self.T_endpoint, self.MagneticField.nominal_field)/cm, 3))) logger.info("Cavity radius: {} cm".format(round(self.cavity_radius/cm, 3))) @@ -1081,26 +1098,25 @@ def print_pumping_requirements(self): logger.info("Ratio of Turbopump Speed Limit: {}".format(self.ratio_required_theoretical_turbopumping_speed)) if self.Efficiency.He_heat_leak: self.time_constant_He, self.T_current_He_heat = calculate_He_heat_leak(self.turbopumping_speed_limit, self.cavity_termination_speed, self.turbopump_speed, self.FrequencyExtraction.cavity_temperature, self.Experiment.design_density, self.total_cavity_volume, self.Efficiency.wall_activity) - #self.time_constant_He, self.T_current_He_heat = calculate_He_heat_leak(9.68*m**3/s, 0.78*m**3/s, 5*m**3/s, self.FrequencyExtraction.cavity_temperature, 1.5e17*m**-3, 1.645*m**3, self.Efficiency.wall_activity) logger.info("He Time Constant: {} s".format(self.time_constant_He/s)) - logger.info("T atom current required to keep up with He-3 leak: {} atoms/s".format(self.T_current_He_heat*s)) + logger.info("Atom current required for He-3 leak: {} atoms/s".format(self.T_current_He_heat*s)) if self.Efficiency.T2_heat_leak: self.time_constant_aperture, self.current_aperture = calculate_aperture_heat_leak(self.DopplerBroadening.gas_temperature, self.Experiment.design_density, self.total_cavity_volume) logger.info("Aperture Time Constant: {} s".format(self.time_constant_aperture/s)) - logger.info("Current through aperture leak: {} atoms/s".format(self.current_aperture*s)) + logger.info("Atom current required for aperture leak: {} atoms/s".format(self.current_aperture*s)) self.time_constant_rad, self.current_rad = calculate_rad_heat_leak(self.cavity_radius, self.Experiment.number_density, self.Experiment.cavity_L_over_D, self.Experiment.design_density, self.total_cavity_volume, self.Efficiency.net_rad_efficiency) logger.info("Radiation Time Constant: {} s".format(self.time_constant_rad/s)) - logger.info("Current through radiation leak: {} atoms/s".format(self.current_rad*s)) + logger.info("Atom current required for radiation leak: {} atoms/s".format(self.current_rad*s)) self.time_constant_desorp, self.current_desorp = calculate_T2_desorption_from_wall(self.cavity_radius, self.Experiment.cavity_L_over_D, self.Efficiency.wall_activity, self.Experiment.design_density, self.total_cavity_volume) logger.info("Desorption Time Constant: {} s".format(self.time_constant_desorp/s)) - logger.info("Current through desorption: {} atoms/s".format(self.current_desorp*s)) + logger.info("Atom current required for desorption: {} atoms/s".format(self.current_desorp*s)) self.time_constant_dipolar, self.current_dipolar = calculate_dipolar_loss(self.MagneticField.nominal_field/T, self.cavity_radius, self.Experiment.design_density, self.total_cavity_volume, self.DopplerBroadening.gas_temperature, self.Experiment.cavity_L_over_D, self.Experiment.pure_magnetic_flag) if not self.Experiment.pure_magnetic_flag: logger.info("***Magnetogravitional Trap***") # 0 is false in python else: - logger.info("***Magnetic Trap***") + logger.info("***Pure Magnetic Trap***") logger.info("Dipolar Time Constant: {} s".format(self.time_constant_dipolar/s)) - logger.info("Current Dipolar: {} atoms/s".format(self.current_dipolar*s)) + logger.info("Atom current required for dipolar loss: {} atoms/s".format(self.current_dipolar*s)) """ # Cramer-Rao lower bound / how much worse are we than the lower bound ScalingFactorCRLB = self.FrequencyExtraction.CRLB_scaling_factor From ca73c2d972c9896306c00e2ebc62ae6482a28d2e Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Wed, 11 Feb 2026 14:05:20 -0500 Subject: [PATCH 34/57] Fixed He leak calculaton and added more constants --- mermithid/misc/Constants_numericalunits.py | 38 +++++++-- .../sensitivity/SensitivityCavityFormulas.py | 79 ++++++++++--------- 2 files changed, 70 insertions(+), 47 deletions(-) diff --git a/mermithid/misc/Constants_numericalunits.py b/mermithid/misc/Constants_numericalunits.py index 5256a9ba..c7244563 100644 --- a/mermithid/misc/Constants_numericalunits.py +++ b/mermithid/misc/Constants_numericalunits.py @@ -14,11 +14,10 @@ from numericalunits import Pa, bar, mbar, atm, torr, mtorr, L, mL # Pressure, Volume T0 = -273.15*K -gravity = 9.80 * m/s**2 tritium_livetime = 5.605e8*s tritium_mass_atomic = 3.016* amu *c0**2 tritium_electron_crosssection_atomic = 9.e-23*m**2 #Inelastic cross-section Hamish extrapolated to 18.6keV using Shah et al. (1987): https://iopscience.iop.org/article/10.1088/0022-3700/20/14/022. Full Bethe formula must be exactly right (down to magnetic and QED corrections) for the hydrogen atom. -#tritium_electron_crosssection_atomic = 1.32e-22*m**2 #Inelastic cross-section + Elastic cross-section for T +#tritium_electron_crosssection_atomic = 1.32e-22*m**2 #Inelastic cross-section + Elastic cross-section for T-e tritium_tritium_crosssection_atomic = 4.40e-16*m**2 # T-T cross-section at T=0 tritium_endpoint_atomic = 18563.251*eV last_1ev_fraction_atomic = 2.067914e-13/eV**3 @@ -35,19 +34,42 @@ # Atomic Calculator # C008 - [m^3] Volume of 1mol of ideal gas at 1atm. Higher pressure --> smaller volume; higher temp --> lower volume -molar_volume = 2.24*10**-2*m**3 +molar_volume = 2.24*e-2*m**3 # C009 - [m^2] H-He Cross-section at low temp but > 5K (Berlinsky) -H_He_crosssection = 2e-19 * m**2 +H_He_crosssection = 2e-19*m**2 +# C010 - [m^2] H-He Cross-section at low temps (Berlinsky) +H_He_crosssection_low_temp = 3e-20*m**2 # C012 - [m^2] Hard Spheres cross-section (289 pm kinetic diameter H_H2_crosssection = 2.62e-19*m**2 -# C049 - per beta decay -molecules_desorbed_wall_beta = 1000 +# C21 - Boltzmann Constant [eV/K] = kB * (1 eV / 1.61e-19 J) +kB_eV = 8.6e-5 * eV/K +# C027 - [eV/T] Bohr Magneton +bohr_magneton = 5.78e-5*eV/T # C028 - 1 Ci = 3.7e10 Bq -Ci_Bq = 3.7*10**10 -# C036 - [eV] Bodine +Ci_Bq = 3.7e10 +# C031 - Ground state branch (atomic) +ground_state_branch_atomic = 0.702 +# C032 - Ground state branch (molecular) +ground_state_branch_atomic = 0.570 +# C036 - [eV] Recoil energy of tritium atom (Bodine) atomic_tritium_recoil_energy = 3.409*eV +# C038 - [eV] Binding energy of tritium molecule (Bodine) +molecular_tritium_binding_energy = 4.59*eV +# C039 - [eV] Molecular final-state g.s. manifold standard deviation +molecular_final_state_manifold = 0.436*eV +# C40 - Constant for saturated T2 vapor: A, Souers et al. +T2_vapor_A = 5.84605 +# C41 - Constant for saturated T2 vapor: B +T2_vapor_B = -160.7 +# C42 - Constant for saturated T2 vapor: B' +T2_vapor_B_prime = 2.3235 +# C043 - [m/s^2] gravitational constant +gravity = 9.80 *m/s**2 # C048 - Multiplier for Lagendijk G^d rates: Ben Jones, Morgan Elliott CM presentation 10/24 LGd_rates = 50 +# C049 - per beta decay: see: https://www.overleaf.com/2817746228snnghrnzfthk +molecules_desorbed_wall_beta = 1000 + # units that do not show up in numericalunits # missing pre-factors fW = W*1e-15 diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index da70fa9a..45b4e901 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -27,30 +27,14 @@ # Jins functions - Atomic Calculator # Python decides whether a name is local or global at compile time (not runtime). The name must already exist in the module’s global namespace by the time the function runs, or Python has nowhere to bind it. -area_loading_aperture = (None) -''' -Molecular experiment updated – no change but tidier. -Add the gravity-temperature scale parameter “b” for magnetogravitational trap -Add the surface density scale parameter “a/b” for magnetogravitational trap -Use exponential density vertical gradient, calculate mean density between trap coils. -Calculate atoms and activity between the trap coils. -Correct the T2/T ratio for the revised T activity in the trap. -Evaporation now in detail, separately for up (gravitation) and magnetic. -Dipolar loss rate. Calculation with z-dependent density, cylinder & cone. -''' +area_loading_aperture, gravity_temperature_scale, surface_density_scale = (None, None, None) # [m] - Fiducial distance from the cavity wall between max(ioffe_bite, larmor_radius) def ioffe_bite(nominal_field, magnetic_inhomogenity, ioffe_field, ioffe_multipolarity, cavity_radius): return ((2 * nominal_field**2 * magnetic_inhomogenity / (2 * nominal_field**2 * magnetic_inhomogenity + ioffe_field**2))**(1/(ioffe_multipolarity-2)) * cavity_radius * -1) + cavity_radius -''' -There are two possible methods for pumping away the tritium, all of which is eventually in molecular form. One is to keep the tritium in circulation by using turbopumps and avoiding temperatures below 10K in the trap region, and the second is to cool parts of the trap region outside the magnetic wall to < 3K in order to cryopump the tritium. -The mechanical pumping method is impractical because of the pumping speed required, a fraction of a billion L/s, where an achievable upper limit is 4 orders of magnitude smaller. -The speed requirement is driven by the combination of the input atomic current and the need to maintain the molecular fraction below 10−4. -The cryopumping method ties up very large amounts of tritium, tens to hundreds of kCi, by the end of a day, and the need to warm up and recycle that tritium on such a short time scale would lead to low statistical precision and instabilities. -''' # C271 - [m/s] Atomic thermal speed of 3T or 3He (C270) def calculate_average_velocity(temp): - average_velocity = c0 * np.sqrt((8 * 0.025*(eV/K) * temp / 273) / (np.pi * tritium_mass_atomic)) + average_velocity = c0 * np.sqrt((8 * 0.025*eV * temp / np.absolute(T0)) / (np.pi * tritium_mass_atomic)) return average_velocity # (Cavity length is cavity top plate - top of cone / 2) @@ -63,6 +47,7 @@ def calculate_trap_wall_area(cavity_radius, cavity_L_over_D): def calculate_inventory(design_density, volume): return design_density * volume +# Use exponential density vertical gradient, calculate mean density between trap coils. Calculate atoms and activity between the trap coils. # C208 - [decays/s] Radioactivity in trap per cavity def calculate_activity_in_trap(number_density, cavity_radius): # C090 @@ -77,9 +62,9 @@ def calculate_activity_in_trap(number_density, cavity_radius): def calculate_T2_background_atomic_trap(cavity_radius, cavity_wall_temp, cavity_L_over_D, max_ratio_T2_T, number_density, wall_activity): trap_wall_area = calculate_trap_wall_area(cavity_radius, cavity_L_over_D) # C306 - [mbar] Vapor Pressure of T2 w/ constants for saturated T2 vapor from Souers et al. - T2_vapor_pressure = mbar * np.exp(5.84605 + (-160.7*K/cavity_wall_temp) + 2.3235 * np.log(cavity_wall_temp/K)) / 0.76 + T2_vapor_pressure = mbar * np.exp(T2_vapor_A + (T2_vapor_B*K/cavity_wall_temp) + T2_vapor_B_prime * np.log(cavity_wall_temp/K)) / 0.76 # C307 - [m^-3] Density of saturated vapor - sat_vapor_density = NA * (T2_vapor_pressure/mbar) * 273*K / (1000 * molar_volume * cavity_wall_temp) + sat_vapor_density = NA * (T2_vapor_pressure/mbar) * np.absolute(T0) / (1000 * molar_volume * cavity_wall_temp) # C308 - [m^-3] = s^-1 * sqrt(kg/eV) / m^2 T2 density form desorption at end of a cycle T2_density_desorp = 4 * molecules_desorbed_wall_beta * Ci_Bq * wall_activity * np.sqrt(molecules_desorbed_wall_beta * (tritium_mass_atomic / c0**2) / (2 * atomic_tritium_recoil_energy * 1.6*10**-19 * (J/eV) )) / trap_wall_area # C309 [m^-3] Total T2 density @@ -95,7 +80,7 @@ def calculate_He_heat_leak(pumping_speed_theoretical, pumping_cavity_termination # C266 - [atoms/s] Not including flow for He heat leak itself He_production_rate = wall_activity * Ci_Bq # C269 - [m^-3] Number density of Helium - He_density = He_production_rate * (1 / pumping_speed_theoretical + 1 / pumping_cavity_termination + 1 / turbopump_speed) + He_density = He_production_rate * ((1/pumping_speed_theoretical) + (1/pumping_cavity_termination) + (1/turbopump_speed)) # C272 - [s] Time constant for loss due to He-3 heat leak time_constant_He = 1 / (He_density * He_velocity * H_He_crosssection) # C273 - [s^-1] T atom current required to keep up with 3He heat leak @@ -105,7 +90,7 @@ def calculate_He_heat_leak(pumping_speed_theoretical, pumping_cavity_termination # Aperture Heat Leak: def calculate_aperture_heat_leak(trapped_gas_temp, design_density, volume): global area_loading_aperture - # C108 - [m^2] Choose an area. (determines injection speed and density.) + # C108 - [m^2] Choose an area to determine injection speed and density. area_loading_aperture = 0.001*m**2 # C250 - [m/s] average velocity of trapped gas trapped_gas_velocity = calculate_average_velocity(trapped_gas_temp) @@ -136,11 +121,10 @@ def calculate_T2_desorption_from_wall(cavity_radius, cavity_L_over_D, wall_activ current_desorp = calculate_inventory(design_density, volume) / time_constant_desorp return time_constant_desorp, current_desorp -# Evaporation - -# Dipolar +# Dipolar loss rate - Calculation with z-dependent density, cylinder & cone. def calculate_dipolar_loss(nominal_field, cavity_radius, design_density, volume, trapped_gas_temp, cavity_L_over_D, pure_magnetic_flag): + global gravity_temperature_scale, surface_density_scale # C089 top_of_cone = 0.60*m # C092 @@ -159,6 +143,19 @@ def calculate_dipolar_loss(nominal_field, cavity_radius, design_density, volume, # C300 - Time constant for loss due to dipolar spin-flip loss time_constant_dipolar = calculate_inventory(design_density, volume) / current_dipolar return time_constant_dipolar, current_dipolar +''' +# Evaporation loss rate - does not take into account density of states with height +def calculate_evaporation_loss(pure_magnetic_flag, trapped_gas_temp): + # C283 - [m] Mean free path at the base of cavity + mfp_cavity_base = 1 / (gravity_temperature_scale * surface_density_scale * tritium_tritium_crosssection_atomic) + + if not pure_magnetic_flag: + # C285 - Magnetogravitational eta + eta = + else: + # C284 - Pure Magnetic eta + eta = bohr_magneton * (trapped_gas_temp * 0.025*eV / np.absolute(T0)) +''' # T2 Heat Leak: #def calculate_T2_heat_leak(trapped_gas_temp, design_density, volume): @@ -169,16 +166,22 @@ def calculate_dipolar_loss(nominal_field, cavity_radius, design_density, volume, # return atom_current / 2 / molecular_density ''' +''' +There are two possible methods for pumping away the tritium, all of which is eventually in molecular form. One is to keep the tritium in circulation by using turbopumps and avoiding temperatures below 10K in the trap region, and the second is to cool parts of the trap region outside the magnetic wall to < 3K in order to cryopump the tritium. +The mechanical pumping method is impractical because of the pumping speed required, a fraction of a billion L/s, where an achievable upper limit is 4 orders of magnitude smaller. +The speed requirement is driven by the combination of the input atomic current and the need to maintain the molecular fraction below 10−4. +The cryopumping method ties up very large amounts of tritium, tens to hundreds of kCi, by the end of a day, and the need to warm up and recycle that tritium on such a short time scale would lead to low statistical precision and instabilities. +''' # Turbopump Calculations: Need to convert tritium mass amu to eV : 1 amu ~ 931 MeV/c^2 and kB = 8.6E-05 eV/K. Multiply by sqrt(2) if atomic; # Useful for Atomic and Helium-3 # C233/C236 - [m^3/s] (molecular/atomic) theoretical pumping speed. The mean speed of the cylinder connected to a perfect pump (Dushman). Uniform source, outputs mean density in cavity. Cannot exceed (obstruction not included). -def turbopumping_speed_limit(cavity_radius, cavity_temperature, cavity_L_over_D): - return (np.pi * cavity_radius**2 * c0 * np.sqrt((8.6*10**-5 * eV / K) * cavity_temperature / (4 * np.pi * tritium_mass_atomic)) / (0.5 + cavity_L_over_D / 8)) +def turbopumping_speed_limit(cavity_radius, cavity_temperature, cavity_L_over_D, atomic_flag): + return (np.pi * cavity_radius**2 * c0 * np.sqrt(kB_eV * cavity_temperature / (4 * np.pi * tritium_mass_atomic)) / (0.5 + cavity_L_over_D / 8)) * (np.sqrt(2) * atomic_flag) +# C235/C238 - [m^3/s] (molecular/atomic) Assumed ambient room air (28-29 amu) temperature (293 K, and a pumping speed of 0.5 L/s +def cavity_termination_speed(turbopumping_speed_air, cavity_top_plate_temp, atomic_flag): + return (turbopumping_speed_air * np.sqrt(cavity_top_plate_temp * 28 * amu * c0**2 / (293 * K * 2 * tritium_mass_atomic))) * (np.sqrt(2) * atomic_flag) # C239 - [m^3/s] Turbopump in series speed (2.5 m^3/s * 2) def turbopump_speed(number_turbos, turbopumping_speed_gas): return number_turbos * turbopumping_speed_gas -# C235/C238 - [m^3/s] (molecular/atomic) Assumed ambient room air (28-29 amu) temperature (293 K, and a pumping speed of 0.5 L/s -def cavity_termination_speed(turbopumping_speed_air, cavity_top_plate_temp): - return (turbopumping_speed_air * np.sqrt(cavity_top_plate_temp * 28 * amu * c0**2 / (293 * K * 2 * tritium_mass_atomic))) # C331 - If above 1000, critical def ratio_required_theoretical_turbopumping_speed(pumping_speed_required, pumping_speed_limit, cavity_termination_speed, turbopump_speed): return pumping_speed_required * ((1 / pumping_speed_limit) + (1 / cavity_termination_speed) + (1 / turbopump_speed)) @@ -561,11 +564,9 @@ def CavityRadius(self): def CavityVolume(self): #Calculate vacuum volume including cone and top service volume if self.Experiment.cavity_cone_flag: + #self.top_vacuum_system = 7.50*m self.top_of_cone = 0.60*m - self.first_trap_coil_height = 0.75*m - self.second_trap_coil_height = 4.80*m self.top_plate_cavity = 5.30*m - self.top_vacuum_system = 7.50*m self.total_cavity_volume = np.pi * self.cavity_radius**2 * (self.top_plate_cavity - (2/3) * self.top_of_cone) * self.Experiment.n_cavities else: #radius = 0.5*wavelength(self.T_endpoint, self.MagneticField.nominal_field) @@ -1086,20 +1087,20 @@ def print_pumping_requirements(self): if self.Efficiency.pumping_calculation: self.turbopump_speed = turbopump_speed(self.Efficiency.number_turbopumps, self.Efficiency.turbopumping_speed_gas_T2) logger.info("Turbopump Speed: {} m^3/s".format(self.turbopump_speed * s / m**3)) - self.turbopumping_speed_limit = turbopumping_speed_limit(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D) - self.cavity_termination_speed = cavity_termination_speed(self.Efficiency.turbopumping_speed_cavity_termination_air, self.Efficiency.cavity_top_plate_temperature) + self.turbopumping_speed_limit = turbopumping_speed_limit(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D, self.Experiment.atomic) + self.cavity_termination_speed = cavity_termination_speed(self.Efficiency.turbopumping_speed_cavity_termination_air, self.Efficiency.cavity_top_plate_temperature, self.Experiment.atomic) if self.Experiment.atomic: - logger.info("Pumping Speed Limit(Atomic): {} m^3/s".format((self.turbopumping_speed_limit * np.sqrt(2)) * s / m**3)) - logger.info("Cavity Termination Speed (Atomic): {} m^3/s".format((self.cavity_termination_speed * np.sqrt(2)) * s / m**3)) + logger.info("Pumping Speed Limit(Atomic): {} m^3/s".format(self.turbopumping_speed_limit * s / m**3)) + logger.info("Cavity Termination Speed (Atomic): {} m^3/s".format(self.cavity_termination_speed * s / m**3)) else: logger.info("Pumping Speed Limit(Molecular): {} m^3/s".format(self.turbopumping_speed_limit * s / m**3)) logger.info("Cavity Termination Speed (Molecular): {} m^3/s".format(self.cavity_termination_speed * s / m**3)) self.ratio_required_theoretical_turbopumping_speed = ratio_required_theoretical_turbopumping_speed(self.Efficiency.pumping_speed_required, self.turbopumping_speed_limit, self.cavity_termination_speed, self.turbopump_speed) logger.info("Ratio of Turbopump Speed Limit: {}".format(self.ratio_required_theoretical_turbopumping_speed)) if self.Efficiency.He_heat_leak: - self.time_constant_He, self.T_current_He_heat = calculate_He_heat_leak(self.turbopumping_speed_limit, self.cavity_termination_speed, self.turbopump_speed, self.FrequencyExtraction.cavity_temperature, self.Experiment.design_density, self.total_cavity_volume, self.Efficiency.wall_activity) + self.time_constant_He, self.current_He = calculate_He_heat_leak(self.turbopumping_speed_limit, self.cavity_termination_speed, self.turbopump_speed, self.FrequencyExtraction.cavity_temperature, self.Experiment.design_density, self.total_cavity_volume, self.Efficiency.wall_activity) logger.info("He Time Constant: {} s".format(self.time_constant_He/s)) - logger.info("Atom current required for He-3 leak: {} atoms/s".format(self.T_current_He_heat*s)) + logger.info("Atom current required for He-3 leak: {} atoms/s".format(self.current_He*s)) if self.Efficiency.T2_heat_leak: self.time_constant_aperture, self.current_aperture = calculate_aperture_heat_leak(self.DopplerBroadening.gas_temperature, self.Experiment.design_density, self.total_cavity_volume) logger.info("Aperture Time Constant: {} s".format(self.time_constant_aperture/s)) From 8cd9c9cf21e93ab12e44dc0aebf8925b0ca3763b Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Wed, 11 Feb 2026 19:53:16 -0500 Subject: [PATCH 35/57] Added conversions from atomic calculator to constants and begun evaporation calculations --- mermithid/misc/Constants_numericalunits.py | 14 ++++--- .../sensitivity/SensitivityCavityFormulas.py | 40 ++++++++++--------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/mermithid/misc/Constants_numericalunits.py b/mermithid/misc/Constants_numericalunits.py index c7244563..c3f63969 100644 --- a/mermithid/misc/Constants_numericalunits.py +++ b/mermithid/misc/Constants_numericalunits.py @@ -41,8 +41,12 @@ H_He_crosssection_low_temp = 3e-20*m**2 # C012 - [m^2] Hard Spheres cross-section (289 pm kinetic diameter H_H2_crosssection = 2.62e-19*m**2 -# C21 - Boltzmann Constant [eV/K] = kB * (1 eV / 1.61e-19 J) -kB_eV = 8.6e-5 * eV/K +# C019 - [eV/amu] Conversion +eV_amu = 931494100*eV/amu +# C020 - [kg/amu] Conversion +kg_amu = 1.66e-27*kg/amu +# C021 - Boltzmann Constant [eV/K] = kB * (1 eV / 1.61e-19 J) +kB_eV = 8.6e-5*eV/K # C027 - [eV/T] Bohr Magneton bohr_magneton = 5.78e-5*eV/T # C028 - 1 Ci = 3.7e10 Bq @@ -57,11 +61,11 @@ molecular_tritium_binding_energy = 4.59*eV # C039 - [eV] Molecular final-state g.s. manifold standard deviation molecular_final_state_manifold = 0.436*eV -# C40 - Constant for saturated T2 vapor: A, Souers et al. +# C040 - Constant for saturated T2 vapor: A, Souers et al. T2_vapor_A = 5.84605 -# C41 - Constant for saturated T2 vapor: B +# C041 - Constant for saturated T2 vapor: B T2_vapor_B = -160.7 -# C42 - Constant for saturated T2 vapor: B' +# C042 - Constant for saturated T2 vapor: B' T2_vapor_B_prime = 2.3235 # C043 - [m/s^2] gravitational constant gravity = 9.80 *m/s**2 diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index 45b4e901..7bc6d803 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -29,7 +29,7 @@ # Python decides whether a name is local or global at compile time (not runtime). The name must already exist in the module’s global namespace by the time the function runs, or Python has nowhere to bind it. area_loading_aperture, gravity_temperature_scale, surface_density_scale = (None, None, None) -# [m] - Fiducial distance from the cavity wall between max(ioffe_bite, larmor_radius) +# C157 - [m] Fiducial distance from the cavity wall between max(ioffe_bite, larmor_radius) def ioffe_bite(nominal_field, magnetic_inhomogenity, ioffe_field, ioffe_multipolarity, cavity_radius): return ((2 * nominal_field**2 * magnetic_inhomogenity / (2 * nominal_field**2 * magnetic_inhomogenity + ioffe_field**2))**(1/(ioffe_multipolarity-2)) * cavity_radius * -1) + cavity_radius # C271 - [m/s] Atomic thermal speed of 3T or 3He (C270) @@ -50,9 +50,9 @@ def calculate_inventory(design_density, volume): # Use exponential density vertical gradient, calculate mean density between trap coils. Calculate atoms and activity between the trap coils. # C208 - [decays/s] Radioactivity in trap per cavity def calculate_activity_in_trap(number_density, cavity_radius): - # C090 + # C090 - [m] coil_1_height = 0.75*m - # C091 + # C091 - [m] coil_2_height = 4.80*m # C206 - [atoms] atoms_between_trap_coils = number_density * np.pi * cavity_radius**2 * np.absolute(coil_2_height - coil_1_height) @@ -69,7 +69,7 @@ def calculate_T2_background_atomic_trap(cavity_radius, cavity_wall_temp, cavity_ T2_density_desorp = 4 * molecules_desorbed_wall_beta * Ci_Bq * wall_activity * np.sqrt(molecules_desorbed_wall_beta * (tritium_mass_atomic / c0**2) / (2 * atomic_tritium_recoil_energy * 1.6*10**-19 * (J/eV) )) / trap_wall_area # C309 [m^-3] Total T2 density T2_total_density = sat_vapor_density + T2_density_desorp - # C310 T2/T number ratio. Activity ratio is 1.64 times bigger + # C310 - T2/T number ratio. Activity ratio is 1.64 times bigger # Number density b/w trap coils varies between Mermithid and Atomic Calculator due to inelastic + elastic T2-e cross-section T2_T_ratio = T2_total_density / number_density return T2_total_density, T2_T_ratio @@ -102,7 +102,7 @@ def calculate_aperture_heat_leak(trapped_gas_temp, design_density, volume): # Radioactivity Heat Leak: def calculate_rad_heat_leak(cavity_radius, number_density, cavity_L_over_D, design_density, volume, net_efficiency): - # C256 [s] Input from Ben Clark's thesis with a mirror ratio of 0.5 + # C256 - [s] Input from Ben Clark's thesis with a mirror ratio of 0.5 half_life = 11 * 24 * 3600 * s trap_wall_area = calculate_trap_wall_area(cavity_radius, cavity_L_over_D) trap_activity = calculate_activity_in_trap(number_density, cavity_radius) @@ -125,13 +125,13 @@ def calculate_T2_desorption_from_wall(cavity_radius, cavity_L_over_D, wall_activ # Dipolar loss rate - Calculation with z-dependent density, cylinder & cone. def calculate_dipolar_loss(nominal_field, cavity_radius, design_density, volume, trapped_gas_temp, cavity_L_over_D, pure_magnetic_flag): global gravity_temperature_scale, surface_density_scale - # C089 + # C089 - [m] top_of_cone = 0.60*m - # C092 + # C092 - [m] top_plate_cavity = 5.30*m - # C201 + # C201 - [m^-1] gravity_temperature_scale = ((tritium_mass_atomic/c0**2 * gravity / (kB * trapped_gas_temp)) * np.absolute(pure_magnetic_flag * (1 + 1 / cavity_L_over_D) - 1)) - # C202 surface_density_scale = 8.024e17/m**2 + # C202 - [m^-2] surface_density_scale = design_density * top_plate_cavity / (1 - np.exp(-gravity_temperature_scale * top_plate_cavity)) # C298 - [m^3/s] Dipolar spin-flip rate (G_dd). Depends on field (Lagendijk et al). Polynomial-log fit used now. dipolar_spin_flip_rate = (60.106 + 13.812 * np.log(nominal_field) - 4.7867 * np.log(nominal_field)**2 - 2.3192 * np.log(nominal_field)**3 \ @@ -140,22 +140,21 @@ def calculate_dipolar_loss(nominal_field, cavity_radius, design_density, volume, current_dipolar = dipolar_spin_flip_rate * np.pi * cavity_radius**2 * surface_density_scale**2 * (gravity_temperature_scale * (np.exp(-gravity_temperature_scale * top_of_cone) \ - np.exp(-gravity_temperature_scale * top_plate_cavity)) / 2 + (1 - np.exp(-2 * gravity_temperature_scale * top_of_cone) * (2 * gravity_temperature_scale**2 \ * top_of_cone**2 + 2 * gravity_temperature_scale * top_of_cone + 1)) / (4 * top_of_cone**2 * gravity_temperature_scale)) - # C300 - Time constant for loss due to dipolar spin-flip loss + # C300 - [s] Time constant for loss due to dipolar spin-flip loss time_constant_dipolar = calculate_inventory(design_density, volume) / current_dipolar return time_constant_dipolar, current_dipolar -''' -# Evaporation loss rate - does not take into account density of states with height -def calculate_evaporation_loss(pure_magnetic_flag, trapped_gas_temp): + +# Evaporation loss rate - does not take into account density of states with height. Magnetic potential limits evaporation. +def calculate_evaporation_loss(pure_magnetic_flag, trapped_gas_temp, cavity_length, ioffe_field, nominal_field): # C283 - [m] Mean free path at the base of cavity mfp_cavity_base = 1 / (gravity_temperature_scale * surface_density_scale * tritium_tritium_crosssection_atomic) - if not pure_magnetic_flag: # C285 - Magnetogravitational eta - eta = + eta = (tritium_mass_atomic/c0**2) * kg_amu * gravity * cavity_length / (kB * trapped_gas_temp) else: # C284 - Pure Magnetic eta - eta = bohr_magneton * (trapped_gas_temp * 0.025*eV / np.absolute(T0)) -''' + eta = bohr_magneton * (np.sqrt(ioffe_field**2 + nominal_field**2) - nominal_field) / (trapped_gas_temp * 0.025*eV / np.absolute(T0)) + return eta # T2 Heat Leak: #def calculate_T2_heat_leak(trapped_gas_temp, design_density, volume): @@ -1111,13 +1110,16 @@ def print_pumping_requirements(self): self.time_constant_desorp, self.current_desorp = calculate_T2_desorption_from_wall(self.cavity_radius, self.Experiment.cavity_L_over_D, self.Efficiency.wall_activity, self.Experiment.design_density, self.total_cavity_volume) logger.info("Desorption Time Constant: {} s".format(self.time_constant_desorp/s)) logger.info("Atom current required for desorption: {} atoms/s".format(self.current_desorp*s)) - self.time_constant_dipolar, self.current_dipolar = calculate_dipolar_loss(self.MagneticField.nominal_field/T, self.cavity_radius, self.Experiment.design_density, self.total_cavity_volume, self.DopplerBroadening.gas_temperature, self.Experiment.cavity_L_over_D, self.Experiment.pure_magnetic_flag) if not self.Experiment.pure_magnetic_flag: - logger.info("***Magnetogravitional Trap***") # 0 is false in python + logger.info("***Magnetogravitional Trap***") else: logger.info("***Pure Magnetic Trap***") + self.time_constant_dipolar, self.current_dipolar = calculate_dipolar_loss(self.MagneticField.nominal_field/T, self.cavity_radius, self.Experiment.design_density, self.total_cavity_volume, self.DopplerBroadening.gas_temperature, self.Experiment.cavity_L_over_D, self.Experiment.pure_magnetic_flag) logger.info("Dipolar Time Constant: {} s".format(self.time_constant_dipolar/s)) logger.info("Atom current required for dipolar loss: {} atoms/s".format(self.current_dipolar*s)) + self.eta = calculate_evaporation_loss(self.Experiment.pure_magnetic_flag, self.DopplerBroadening.gas_temperature, self.cavity_radius*2*self.Experiment.cavity_L_over_D, self.MagneticField.ioffe_field, self.MagneticField.nominal_field) + logger.info("eta: {} ".format(self.eta)) + """ # Cramer-Rao lower bound / how much worse are we than the lower bound ScalingFactorCRLB = self.FrequencyExtraction.CRLB_scaling_factor From 04bde55a5f5ecb309e246c80ab89e763ae7c4da0 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Mon, 16 Feb 2026 18:41:40 -0500 Subject: [PATCH 36/57] evaporation out the top --- .../sensitivity/SensitivityCavityFormulas.py | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index 7bc6d803..4d3cb5ba 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -27,7 +27,7 @@ # Jins functions - Atomic Calculator # Python decides whether a name is local or global at compile time (not runtime). The name must already exist in the module’s global namespace by the time the function runs, or Python has nowhere to bind it. -area_loading_aperture, gravity_temperature_scale, surface_density_scale = (None, None, None) +area_loading_aperture, gravity_temperature_scale, surface_density_scale, top_plate_cavity = (None, None, None, None) # C157 - [m] Fiducial distance from the cavity wall between max(ioffe_bite, larmor_radius) def ioffe_bite(nominal_field, magnetic_inhomogenity, ioffe_field, ioffe_multipolarity, cavity_radius): @@ -38,7 +38,7 @@ def calculate_average_velocity(temp): return average_velocity # (Cavity length is cavity top plate - top of cone / 2) -# C334 - [m^2] Cylinder wall; no endcaps included +# C334 - [m^2] Cylinder wall; no endcaps included (2*pi*R*L) def calculate_trap_wall_area(cavity_radius, cavity_L_over_D): trap_wall_area = 2 * np.pi * (2 * cavity_radius**2) * cavity_L_over_D return trap_wall_area @@ -124,7 +124,7 @@ def calculate_T2_desorption_from_wall(cavity_radius, cavity_L_over_D, wall_activ # Dipolar loss rate - Calculation with z-dependent density, cylinder & cone. def calculate_dipolar_loss(nominal_field, cavity_radius, design_density, volume, trapped_gas_temp, cavity_L_over_D, pure_magnetic_flag): - global gravity_temperature_scale, surface_density_scale + global top_plate_cavity, gravity_temperature_scale, surface_density_scale # C089 - [m] top_of_cone = 0.60*m # C092 - [m] @@ -133,7 +133,7 @@ def calculate_dipolar_loss(nominal_field, cavity_radius, design_density, volume, gravity_temperature_scale = ((tritium_mass_atomic/c0**2 * gravity / (kB * trapped_gas_temp)) * np.absolute(pure_magnetic_flag * (1 + 1 / cavity_L_over_D) - 1)) # C202 - [m^-2] surface_density_scale = design_density * top_plate_cavity / (1 - np.exp(-gravity_temperature_scale * top_plate_cavity)) - # C298 - [m^3/s] Dipolar spin-flip rate (G_dd). Depends on field (Lagendijk et al). Polynomial-log fit used now. + # C298 - [m^3/s] Polynomial fit for Dipolar spin-flip rate (G_dd). Depends on field (Lagendijk et al). Polynomial-log fit used now. dipolar_spin_flip_rate = (60.106 + 13.812 * np.log(nominal_field) - 4.7867 * np.log(nominal_field)**2 - 2.3192 * np.log(nominal_field)**3 \ - 0.32663 * np.log(nominal_field)**4 - 0.015775 * np.log(nominal_field)**5) * 1e-22 * LGd_rates * m**3/s # C299 - [atoms/s] Flow to keep up with dipolar losses @@ -145,16 +145,22 @@ def calculate_dipolar_loss(nominal_field, cavity_radius, design_density, volume, return time_constant_dipolar, current_dipolar # Evaporation loss rate - does not take into account density of states with height. Magnetic potential limits evaporation. -def calculate_evaporation_loss(pure_magnetic_flag, trapped_gas_temp, cavity_length, ioffe_field, nominal_field): +def calculate_evaporation_loss(pure_magnetic_flag, trapped_gas_temp, cavity_L_over_D, cavity_radius, ioffe_field, nominal_field): # C283 - [m] Mean free path at the base of cavity mfp_cavity_base = 1 / (gravity_temperature_scale * surface_density_scale * tritium_tritium_crosssection_atomic) if not pure_magnetic_flag: # C285 - Magnetogravitational eta - eta = (tritium_mass_atomic/c0**2) * kg_amu * gravity * cavity_length / (kB * trapped_gas_temp) + eta = (tritium_mass_atomic/c0**2) * kg_amu * gravity * (cavity_L_over_D * cavity_radius * 2) / (kB * trapped_gas_temp) + # C286 - [s^-1] Evaporation out the top; set to 1/1000 for pure magnetic trap + evaporation_out_top = (np.log(1 + np.log(2) * np.exp(gravity_temperature_scale * top_plate_cavity) / (surface_density_scale * tritium_tritium_crosssection_atomic)) \ + * surface_density_scale * np.exp(-gravity_temperature_scale * top_plate_cavity) * np.sqrt(np.pi * kB * trapped_gas_temp / (2 * tritium_mass_atomic / c0**2)) * cavity_radius**2) \ + * np.absolute(pure_magnetic_flag * (1 + 0.001) -1) + #time_constant_evaporation = else: # C284 - Pure Magnetic eta eta = bohr_magneton * (np.sqrt(ioffe_field**2 + nominal_field**2) - nominal_field) / (trapped_gas_temp * 0.025*eV / np.absolute(T0)) - return eta + evaporation_out_top = 1/1000 + return eta, evaporation_out_top # T2 Heat Leak: #def calculate_T2_heat_leak(trapped_gas_temp, design_density, volume): @@ -1117,8 +1123,9 @@ def print_pumping_requirements(self): self.time_constant_dipolar, self.current_dipolar = calculate_dipolar_loss(self.MagneticField.nominal_field/T, self.cavity_radius, self.Experiment.design_density, self.total_cavity_volume, self.DopplerBroadening.gas_temperature, self.Experiment.cavity_L_over_D, self.Experiment.pure_magnetic_flag) logger.info("Dipolar Time Constant: {} s".format(self.time_constant_dipolar/s)) logger.info("Atom current required for dipolar loss: {} atoms/s".format(self.current_dipolar*s)) - self.eta = calculate_evaporation_loss(self.Experiment.pure_magnetic_flag, self.DopplerBroadening.gas_temperature, self.cavity_radius*2*self.Experiment.cavity_L_over_D, self.MagneticField.ioffe_field, self.MagneticField.nominal_field) + self.eta, self.evaporation_top = calculate_evaporation_loss(self.Experiment.pure_magnetic_flag, self.DopplerBroadening.gas_temperature, self.Experiment.cavity_L_over_D, self.cavity_radius, self.MagneticField.ioffe_field, self.MagneticField.nominal_field) logger.info("eta: {} ".format(self.eta)) + logger.info("evaporation : {} m/s".format(self.evaporation_top*s/m)) """ # Cramer-Rao lower bound / how much worse are we than the lower bound From 5df987cecd9357341d0b2f03e558c0c3c3a689e7 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Tue, 17 Feb 2026 12:08:10 -0500 Subject: [PATCH 37/57] Evaporation Loss Calculations --- .../sensitivity/SensitivityCavityFormulas.py | 110 ++++++++++-------- 1 file changed, 61 insertions(+), 49 deletions(-) diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index 4d3cb5ba..d3ca9b2f 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -27,19 +27,20 @@ # Jins functions - Atomic Calculator # Python decides whether a name is local or global at compile time (not runtime). The name must already exist in the module’s global namespace by the time the function runs, or Python has nowhere to bind it. -area_loading_aperture, gravity_temperature_scale, surface_density_scale, top_plate_cavity = (None, None, None, None) +area_loading_aperture, gravity_temperature_scale, surface_density_scale, top_plate_cavity, top_cone = (None, None, None, None, None) # C157 - [m] Fiducial distance from the cavity wall between max(ioffe_bite, larmor_radius) def ioffe_bite(nominal_field, magnetic_inhomogenity, ioffe_field, ioffe_multipolarity, cavity_radius): return ((2 * nominal_field**2 * magnetic_inhomogenity / (2 * nominal_field**2 * magnetic_inhomogenity + ioffe_field**2))**(1/(ioffe_multipolarity-2)) * cavity_radius * -1) + cavity_radius + # C271 - [m/s] Atomic thermal speed of 3T or 3He (C270) def calculate_average_velocity(temp): average_velocity = c0 * np.sqrt((8 * 0.025*eV * temp / np.absolute(T0)) / (np.pi * tritium_mass_atomic)) return average_velocity -# (Cavity length is cavity top plate - top of cone / 2) # C334 - [m^2] Cylinder wall; no endcaps included (2*pi*R*L) def calculate_trap_wall_area(cavity_radius, cavity_L_over_D): + # (Cavity length is cavity top plate - top of cone / 2) trap_wall_area = 2 * np.pi * (2 * cavity_radius**2) * cavity_L_over_D return trap_wall_area @@ -47,18 +48,17 @@ def calculate_trap_wall_area(cavity_radius, cavity_L_over_D): def calculate_inventory(design_density, volume): return design_density * volume -# Use exponential density vertical gradient, calculate mean density between trap coils. Calculate atoms and activity between the trap coils. # C208 - [decays/s] Radioactivity in trap per cavity def calculate_activity_in_trap(number_density, cavity_radius): # C090 - [m] coil_1_height = 0.75*m # C091 - [m] coil_2_height = 4.80*m - # C206 - [atoms] + # C206 - [atoms] Use exponential density vertical gradient, calculate mean density between trap coils. Calculate atoms and activity between the trap coils. atoms_between_trap_coils = number_density * np.pi * cavity_radius**2 * np.absolute(coil_2_height - coil_1_height) return atoms_between_trap_coils / tritium_livetime -# Background T2 in atomic trap +# Background T2 in atomic trap: def calculate_T2_background_atomic_trap(cavity_radius, cavity_wall_temp, cavity_L_over_D, max_ratio_T2_T, number_density, wall_activity): trap_wall_area = calculate_trap_wall_area(cavity_radius, cavity_L_over_D) # C306 - [mbar] Vapor Pressure of T2 w/ constants for saturated T2 vapor from Souers et al. @@ -83,18 +83,18 @@ def calculate_He_heat_leak(pumping_speed_theoretical, pumping_cavity_termination He_density = He_production_rate * ((1/pumping_speed_theoretical) + (1/pumping_cavity_termination) + (1/turbopump_speed)) # C272 - [s] Time constant for loss due to He-3 heat leak time_constant_He = 1 / (He_density * He_velocity * H_He_crosssection) - # C273 - [s^-1] T atom current required to keep up with 3He heat leak - T_current_He_heat = calculate_inventory(design_density, volume) / time_constant_He - return time_constant_He, T_current_He_heat + # C273 - [s^-1] Atom current required to keep up with He-3 heat leak + current_He_leak = calculate_inventory(design_density, volume) / time_constant_He + return time_constant_He, current_He_leak # Aperture Heat Leak: def calculate_aperture_heat_leak(trapped_gas_temp, design_density, volume): global area_loading_aperture # C108 - [m^2] Choose an area to determine injection speed and density. area_loading_aperture = 0.001*m**2 - # C250 - [m/s] average velocity of trapped gas + # C250 - [m/s] Average velocity of trapped gas trapped_gas_velocity = calculate_average_velocity(trapped_gas_temp) - # C251 - [atom/s] inward atom current required through aperture to balance losses (j_aperture) + # C251 - [atom/s] Inward atom current required through aperture to balance losses (j_aperture) current_aperture = area_loading_aperture * design_density * trapped_gas_velocity / 4 # C252 - [s] Time constant for loss through aperture time_constant_aperture = calculate_inventory(design_density, volume) / current_aperture @@ -107,26 +107,25 @@ def calculate_rad_heat_leak(cavity_radius, number_density, cavity_L_over_D, desi trap_wall_area = calculate_trap_wall_area(cavity_radius, cavity_L_over_D) trap_activity = calculate_activity_in_trap(number_density, cavity_radius) # C257 - [atoms/s] Atom current required to balance loss due to radioactivity - current_rad = trap_activity * np.log(2) * tritium_livetime / (net_efficiency * half_life) + current_rad_leak = trap_activity * np.log(2) * tritium_livetime / (net_efficiency * half_life) # C258 - [s] Time constant for loss due to radioactivty (tau_rad) - time_constant_rad = calculate_inventory(design_density, volume) / current_rad - return time_constant_rad, current_rad + time_constant_rad = calculate_inventory(design_density, volume) / current_rad_leak + return time_constant_rad, current_rad_leak -# T2 Desoprtion from the wall +# T2 Desorption from the wall: def calculate_T2_desorption_from_wall(cavity_radius, cavity_L_over_D, wall_activity, design_density, volume): trap_wall_area = calculate_trap_wall_area(cavity_radius, cavity_L_over_D) - # C261 - [s] mean lifetime of atom in trap from desorption + # C261 - [s] Mean lifetime of atom in trap from desorption time_constant_desorp = trap_wall_area / (2 * molecules_desorbed_wall_beta * wall_activity * Ci_Bq * H_H2_crosssection) - # C262 - [atoms/s] current required to keep up + # C262 - [atoms/s] Atom current required to keep up with desorption losses current_desorp = calculate_inventory(design_density, volume) / time_constant_desorp return time_constant_desorp, current_desorp - -# Dipolar loss rate - Calculation with z-dependent density, cylinder & cone. +# Dipolar loss rate: Calculation with z-dependent density, cylinder & cone. def calculate_dipolar_loss(nominal_field, cavity_radius, design_density, volume, trapped_gas_temp, cavity_L_over_D, pure_magnetic_flag): - global top_plate_cavity, gravity_temperature_scale, surface_density_scale + global top_cone, top_plate_cavity, gravity_temperature_scale, surface_density_scale # C089 - [m] - top_of_cone = 0.60*m + top_cone = 0.60*m # C092 - [m] top_plate_cavity = 5.30*m # C201 - [m^-1] @@ -137,30 +136,46 @@ def calculate_dipolar_loss(nominal_field, cavity_radius, design_density, volume, dipolar_spin_flip_rate = (60.106 + 13.812 * np.log(nominal_field) - 4.7867 * np.log(nominal_field)**2 - 2.3192 * np.log(nominal_field)**3 \ - 0.32663 * np.log(nominal_field)**4 - 0.015775 * np.log(nominal_field)**5) * 1e-22 * LGd_rates * m**3/s # C299 - [atoms/s] Flow to keep up with dipolar losses - current_dipolar = dipolar_spin_flip_rate * np.pi * cavity_radius**2 * surface_density_scale**2 * (gravity_temperature_scale * (np.exp(-gravity_temperature_scale * top_of_cone) \ - - np.exp(-gravity_temperature_scale * top_plate_cavity)) / 2 + (1 - np.exp(-2 * gravity_temperature_scale * top_of_cone) * (2 * gravity_temperature_scale**2 \ - * top_of_cone**2 + 2 * gravity_temperature_scale * top_of_cone + 1)) / (4 * top_of_cone**2 * gravity_temperature_scale)) + current_dipolar = dipolar_spin_flip_rate * np.pi * cavity_radius**2 * surface_density_scale**2 * (gravity_temperature_scale * (np.exp(-gravity_temperature_scale * top_cone) \ + - np.exp(-gravity_temperature_scale * top_plate_cavity)) / 2 + (1 - np.exp(-2 * gravity_temperature_scale * top_cone) * (2 * gravity_temperature_scale**2 \ + * top_cone**2 + 2 * gravity_temperature_scale * top_cone + 1)) / (4 * top_cone**2 * gravity_temperature_scale)) # C300 - [s] Time constant for loss due to dipolar spin-flip loss time_constant_dipolar = calculate_inventory(design_density, volume) / current_dipolar return time_constant_dipolar, current_dipolar -# Evaporation loss rate - does not take into account density of states with height. Magnetic potential limits evaporation. -def calculate_evaporation_loss(pure_magnetic_flag, trapped_gas_temp, cavity_L_over_D, cavity_radius, ioffe_field, nominal_field): +# Evaporation loss rate: does not take into account density of states with height. Magnetic potential limits evaporation. +def calculate_evaporation_loss(pure_magnetic_flag, trapped_gas_temp, cavity_L_over_D, cavity_radius, ioffe_field, nominal_field, design_density, relative_loss_rate_cone_wall, volume): # C283 - [m] Mean free path at the base of cavity mfp_cavity_base = 1 / (gravity_temperature_scale * surface_density_scale * tritium_tritium_crosssection_atomic) - if not pure_magnetic_flag: - # C285 - Magnetogravitational eta - eta = (tritium_mass_atomic/c0**2) * kg_amu * gravity * (cavity_L_over_D * cavity_radius * 2) / (kB * trapped_gas_temp) - # C286 - [s^-1] Evaporation out the top; set to 1/1000 for pure magnetic trap - evaporation_out_top = (np.log(1 + np.log(2) * np.exp(gravity_temperature_scale * top_plate_cavity) / (surface_density_scale * tritium_tritium_crosssection_atomic)) \ - * surface_density_scale * np.exp(-gravity_temperature_scale * top_plate_cavity) * np.sqrt(np.pi * kB * trapped_gas_temp / (2 * tritium_mass_atomic / c0**2)) * cavity_radius**2) \ - * np.absolute(pure_magnetic_flag * (1 + 0.001) -1) - #time_constant_evaporation = - else: - # C284 - Pure Magnetic eta - eta = bohr_magneton * (np.sqrt(ioffe_field**2 + nominal_field**2) - nominal_field) / (trapped_gas_temp * 0.025*eV / np.absolute(T0)) - evaporation_out_top = 1/1000 - return eta, evaporation_out_top + # C284 - Pure Magnetic eta + eta = bohr_magneton * (np.sqrt(ioffe_field**2 + nominal_field**2) - nominal_field) / (trapped_gas_temp * 0.025*eV / np.absolute(T0)) + # C285 - Magnetogravitational eta + eta_grav = (tritium_mass_atomic/c0**2) * kg_amu * gravity * (cavity_L_over_D * cavity_radius * 2) / (kB * trapped_gas_temp) + # C286 - [s^-1] Evaporation out the top; set to 1/1000 for pure magnetic trap + evaporation_top = (np.log(1 + np.log(2) * np.exp(gravity_temperature_scale * top_plate_cavity) / (surface_density_scale * tritium_tritium_crosssection_atomic)) \ + * surface_density_scale * np.exp(-gravity_temperature_scale * top_plate_cavity) * np.sqrt(np.pi * kB * trapped_gas_temp / (2 * tritium_mass_atomic / c0**2)) * cavity_radius**2) \ + * np.absolute(pure_magnetic_flag * (1 + 0.001) -1) + # C287 - [s/m????] Time constant for evaporation out of the top SKIP FOR NOW + #time_constant_evap_top = + # C288 - [s^-1] Evaporation to the sides + evaporation_sides = (2 * np.pi * cavity_radius * (2 * cavity_radius * cavity_L_over_D) * design_density * np.sqrt(kB * trapped_gas_temp / (2 * np.pi * tritium_mass_atomic / c0**2)) \ + * np.exp(-eta) * top_plate_cavity * (np.exp(-gravity_temperature_scale * top_cone) - np.exp(-gravity_temperature_scale * top_plate_cavity)) \ + / ((1 - np.exp(-gravity_temperature_scale * top_plate_cavity)) * (top_plate_cavity - top_cone))) + # C289 [Bq - Decay/s] - Accumulated activity in the sides by end of cycle + #accumulated_activity_sides = evaporation_sides * + # C290 - [s^-1] Evaporation in cone; for pure magnetic, double it to account for both ends + evaporation_cone = (relative_loss_rate_cone_wall * np.pi * cavity_radius * np.sqrt(cavity_radius**2 + top_cone**2) * design_density \ + * np.sqrt(kB * trapped_gas_temp / (2 * np.pi * tritium_mass_atomic/c0**2)) * np.exp(-eta) * top_plate_cavity * (1 - np.exp(-gravity_temperature_scale * top_cone)) \ + / ((1 - np.exp(-gravity_temperature_scale * top_plate_cavity)) * top_cone)) * np.absolute(pure_magnetic_flag + 1) + # C291 [Bq - Decay/s] - Accumulated activity in the cone by end of cycle + #accumulated_activity_cone = evaporation_cone * + # C292 - [s] Time constant for evaporation loss of cone and sides + time_constant_evap_cone_sides = calculate_inventory(design_density, volume) / (evaporation_cone + evaporation_sides) + # C295 - [atom/s] Flow to keep up with evaporation losses aka Total evaporation_sum + current_evaporation = (evaporation_top/m) + evaporation_sides + evaporation_cone + # C294 - [s] Time constant for total evaporation loss + time_constant_total_evaporation = calculate_inventory(design_density, volume) / current_evaporation + return time_constant_total_evaporation, current_evaporation # T2 Heat Leak: #def calculate_T2_heat_leak(trapped_gas_temp, design_density, volume): @@ -205,8 +220,6 @@ def molecular_density_allowed(atom_density=1.5*10**17/m**3, max_ratio_nM_nA = 10 - - # Wouters functinos def db_to_pwr_ratio(q_db): return 10**(q_db/10) @@ -570,9 +583,9 @@ def CavityVolume(self): #Calculate vacuum volume including cone and top service volume if self.Experiment.cavity_cone_flag: #self.top_vacuum_system = 7.50*m - self.top_of_cone = 0.60*m + self.top_cone = 0.60*m self.top_plate_cavity = 5.30*m - self.total_cavity_volume = np.pi * self.cavity_radius**2 * (self.top_plate_cavity - (2/3) * self.top_of_cone) * self.Experiment.n_cavities + self.total_cavity_volume = np.pi * self.cavity_radius**2 * (self.top_plate_cavity - (2/3) * self.top_cone) * self.Experiment.n_cavities else: #radius = 0.5*wavelength(self.T_endpoint, self.MagneticField.nominal_field) self.total_cavity_volume = 2*self.cavity_radius*self.Experiment.cavity_L_over_D*np.pi*(self.cavity_radius)**2*self.Experiment.n_cavities @@ -1102,11 +1115,10 @@ def print_pumping_requirements(self): logger.info("Cavity Termination Speed (Molecular): {} m^3/s".format(self.cavity_termination_speed * s / m**3)) self.ratio_required_theoretical_turbopumping_speed = ratio_required_theoretical_turbopumping_speed(self.Efficiency.pumping_speed_required, self.turbopumping_speed_limit, self.cavity_termination_speed, self.turbopump_speed) logger.info("Ratio of Turbopump Speed Limit: {}".format(self.ratio_required_theoretical_turbopumping_speed)) - if self.Efficiency.He_heat_leak: + if self.Efficiency.T2_heat_leak: self.time_constant_He, self.current_He = calculate_He_heat_leak(self.turbopumping_speed_limit, self.cavity_termination_speed, self.turbopump_speed, self.FrequencyExtraction.cavity_temperature, self.Experiment.design_density, self.total_cavity_volume, self.Efficiency.wall_activity) logger.info("He Time Constant: {} s".format(self.time_constant_He/s)) logger.info("Atom current required for He-3 leak: {} atoms/s".format(self.current_He*s)) - if self.Efficiency.T2_heat_leak: self.time_constant_aperture, self.current_aperture = calculate_aperture_heat_leak(self.DopplerBroadening.gas_temperature, self.Experiment.design_density, self.total_cavity_volume) logger.info("Aperture Time Constant: {} s".format(self.time_constant_aperture/s)) logger.info("Atom current required for aperture leak: {} atoms/s".format(self.current_aperture*s)) @@ -1115,17 +1127,17 @@ def print_pumping_requirements(self): logger.info("Atom current required for radiation leak: {} atoms/s".format(self.current_rad*s)) self.time_constant_desorp, self.current_desorp = calculate_T2_desorption_from_wall(self.cavity_radius, self.Experiment.cavity_L_over_D, self.Efficiency.wall_activity, self.Experiment.design_density, self.total_cavity_volume) logger.info("Desorption Time Constant: {} s".format(self.time_constant_desorp/s)) - logger.info("Atom current required for desorption: {} atoms/s".format(self.current_desorp*s)) + logger.info("Atom current required for desorption losses: {} atoms/s".format(self.current_desorp*s)) if not self.Experiment.pure_magnetic_flag: logger.info("***Magnetogravitional Trap***") else: logger.info("***Pure Magnetic Trap***") self.time_constant_dipolar, self.current_dipolar = calculate_dipolar_loss(self.MagneticField.nominal_field/T, self.cavity_radius, self.Experiment.design_density, self.total_cavity_volume, self.DopplerBroadening.gas_temperature, self.Experiment.cavity_L_over_D, self.Experiment.pure_magnetic_flag) logger.info("Dipolar Time Constant: {} s".format(self.time_constant_dipolar/s)) - logger.info("Atom current required for dipolar loss: {} atoms/s".format(self.current_dipolar*s)) - self.eta, self.evaporation_top = calculate_evaporation_loss(self.Experiment.pure_magnetic_flag, self.DopplerBroadening.gas_temperature, self.Experiment.cavity_L_over_D, self.cavity_radius, self.MagneticField.ioffe_field, self.MagneticField.nominal_field) - logger.info("eta: {} ".format(self.eta)) - logger.info("evaporation : {} m/s".format(self.evaporation_top*s/m)) + logger.info("Atom current required for dipolar losses: {} atoms/s".format(self.current_dipolar*s)) + self.time_constant_evaporation, self.current_evaporation = calculate_evaporation_loss(self.Experiment.pure_magnetic_flag, self.DopplerBroadening.gas_temperature, self.Experiment.cavity_L_over_D, self.cavity_radius, self.MagneticField.ioffe_field, self.MagneticField.nominal_field, self.Experiment.design_density, self.Efficiency.relative_loss_rate_cone_wall, self.total_cavity_volume) + logger.info("Evaporation Time Constant: {} s".format(self.time_constant_evaporation/s)) + logger.info("Atom current required for Evaporation losses: {} atoms/s".format(self.current_evaporation*s)) """ # Cramer-Rao lower bound / how much worse are we than the lower bound From 219fb972c527cd9a2b4cf39492b6043148c7d3a8 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Tue, 17 Feb 2026 18:20:20 -0500 Subject: [PATCH 38/57] Included last of evaporation calculations, fixed logger info output, added calculations for cavity length, trap length, and cavity L/D --- .../sensitivity/SensitivityCavityFormulas.py | 133 ++++++++++-------- 1 file changed, 77 insertions(+), 56 deletions(-) diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index d3ca9b2f..02959158 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -27,10 +27,10 @@ # Jins functions - Atomic Calculator # Python decides whether a name is local or global at compile time (not runtime). The name must already exist in the module’s global namespace by the time the function runs, or Python has nowhere to bind it. -area_loading_aperture, gravity_temperature_scale, surface_density_scale, top_plate_cavity, top_cone = (None, None, None, None, None) +area_loading_aperture, gravity_temperature_scale, surface_density_scale = (None, None, None) # C157 - [m] Fiducial distance from the cavity wall between max(ioffe_bite, larmor_radius) -def ioffe_bite(nominal_field, magnetic_inhomogenity, ioffe_field, ioffe_multipolarity, cavity_radius): +def calculate_ioffe_bite(nominal_field, magnetic_inhomogenity, ioffe_field, ioffe_multipolarity, cavity_radius): return ((2 * nominal_field**2 * magnetic_inhomogenity / (2 * nominal_field**2 * magnetic_inhomogenity + ioffe_field**2))**(1/(ioffe_multipolarity-2)) * cavity_radius * -1) + cavity_radius # C271 - [m/s] Atomic thermal speed of 3T or 3He (C270) @@ -39,9 +39,9 @@ def calculate_average_velocity(temp): return average_velocity # C334 - [m^2] Cylinder wall; no endcaps included (2*pi*R*L) -def calculate_trap_wall_area(cavity_radius, cavity_L_over_D): +def calculate_trap_wall_area(cavity_radius, cavity_length): # (Cavity length is cavity top plate - top of cone / 2) - trap_wall_area = 2 * np.pi * (2 * cavity_radius**2) * cavity_L_over_D + trap_wall_area = 2 * np.pi * cavity_radius * cavity_length return trap_wall_area # C204 - [atoms] Inventory in physical volume @@ -59,8 +59,8 @@ def calculate_activity_in_trap(number_density, cavity_radius): return atoms_between_trap_coils / tritium_livetime # Background T2 in atomic trap: -def calculate_T2_background_atomic_trap(cavity_radius, cavity_wall_temp, cavity_L_over_D, max_ratio_T2_T, number_density, wall_activity): - trap_wall_area = calculate_trap_wall_area(cavity_radius, cavity_L_over_D) +def calculate_T2_background_atomic_trap(cavity_radius, cavity_length, cavity_wall_temp, max_ratio_T2_T, number_density, wall_activity): + trap_wall_area = calculate_trap_wall_area(cavity_radius, cavity_length) # C306 - [mbar] Vapor Pressure of T2 w/ constants for saturated T2 vapor from Souers et al. T2_vapor_pressure = mbar * np.exp(T2_vapor_A + (T2_vapor_B*K/cavity_wall_temp) + T2_vapor_B_prime * np.log(cavity_wall_temp/K)) / 0.76 # C307 - [m^-3] Density of saturated vapor @@ -101,10 +101,10 @@ def calculate_aperture_heat_leak(trapped_gas_temp, design_density, volume): return time_constant_aperture, current_aperture # Radioactivity Heat Leak: -def calculate_rad_heat_leak(cavity_radius, number_density, cavity_L_over_D, design_density, volume, net_efficiency): +def calculate_rad_heat_leak(cavity_radius, cavity_length, number_density, design_density, volume, net_efficiency): # C256 - [s] Input from Ben Clark's thesis with a mirror ratio of 0.5 half_life = 11 * 24 * 3600 * s - trap_wall_area = calculate_trap_wall_area(cavity_radius, cavity_L_over_D) + trap_wall_area = calculate_trap_wall_area(cavity_radius, cavity_length) trap_activity = calculate_activity_in_trap(number_density, cavity_radius) # C257 - [atoms/s] Atom current required to balance loss due to radioactivity current_rad_leak = trap_activity * np.log(2) * tritium_livetime / (net_efficiency * half_life) @@ -113,8 +113,8 @@ def calculate_rad_heat_leak(cavity_radius, number_density, cavity_L_over_D, desi return time_constant_rad, current_rad_leak # T2 Desorption from the wall: -def calculate_T2_desorption_from_wall(cavity_radius, cavity_L_over_D, wall_activity, design_density, volume): - trap_wall_area = calculate_trap_wall_area(cavity_radius, cavity_L_over_D) +def calculate_T2_desorption_from_wall(cavity_radius, cavity_length, wall_activity, design_density, volume): + trap_wall_area = calculate_trap_wall_area(cavity_radius, cavity_length) # C261 - [s] Mean lifetime of atom in trap from desorption time_constant_desorp = trap_wall_area / (2 * molecules_desorbed_wall_beta * wall_activity * Ci_Bq * H_H2_crosssection) # C262 - [atoms/s] Atom current required to keep up with desorption losses @@ -122,12 +122,8 @@ def calculate_T2_desorption_from_wall(cavity_radius, cavity_L_over_D, wall_activ return time_constant_desorp, current_desorp # Dipolar loss rate: Calculation with z-dependent density, cylinder & cone. -def calculate_dipolar_loss(nominal_field, cavity_radius, design_density, volume, trapped_gas_temp, cavity_L_over_D, pure_magnetic_flag): - global top_cone, top_plate_cavity, gravity_temperature_scale, surface_density_scale - # C089 - [m] - top_cone = 0.60*m - # C092 - [m] - top_plate_cavity = 5.30*m +def calculate_dipolar_loss(nominal_field, cavity_radius, design_density, volume, trapped_gas_temp, cavity_L_over_D, top_cone, top_plate_cavity, pure_magnetic_flag): + global gravity_temperature_scale, surface_density_scale # C201 - [m^-1] gravity_temperature_scale = ((tritium_mass_atomic/c0**2 * gravity / (kB * trapped_gas_temp)) * np.absolute(pure_magnetic_flag * (1 + 1 / cavity_L_over_D) - 1)) # C202 - [m^-2] @@ -144,21 +140,21 @@ def calculate_dipolar_loss(nominal_field, cavity_radius, design_density, volume, return time_constant_dipolar, current_dipolar # Evaporation loss rate: does not take into account density of states with height. Magnetic potential limits evaporation. -def calculate_evaporation_loss(pure_magnetic_flag, trapped_gas_temp, cavity_L_over_D, cavity_radius, ioffe_field, nominal_field, design_density, relative_loss_rate_cone_wall, volume): +def calculate_evaporation_loss(pure_magnetic_flag, trapped_gas_temp, cavity_radius, ioffe_field, nominal_field, cavity_length, top_cone, top_plate_cavity, design_density, relative_loss_rate_cone_wall, volume): # C283 - [m] Mean free path at the base of cavity mfp_cavity_base = 1 / (gravity_temperature_scale * surface_density_scale * tritium_tritium_crosssection_atomic) # C284 - Pure Magnetic eta eta = bohr_magneton * (np.sqrt(ioffe_field**2 + nominal_field**2) - nominal_field) / (trapped_gas_temp * 0.025*eV / np.absolute(T0)) # C285 - Magnetogravitational eta - eta_grav = (tritium_mass_atomic/c0**2) * kg_amu * gravity * (cavity_L_over_D * cavity_radius * 2) / (kB * trapped_gas_temp) + eta_grav = (tritium_mass_atomic/c0**2) * kg_amu * gravity * cavity_length / (kB * trapped_gas_temp) # C286 - [s^-1] Evaporation out the top; set to 1/1000 for pure magnetic trap evaporation_top = (np.log(1 + np.log(2) * np.exp(gravity_temperature_scale * top_plate_cavity) / (surface_density_scale * tritium_tritium_crosssection_atomic)) \ - * surface_density_scale * np.exp(-gravity_temperature_scale * top_plate_cavity) * np.sqrt(np.pi * kB * trapped_gas_temp / (2 * tritium_mass_atomic / c0**2)) * cavity_radius**2) \ + * surface_density_scale * gravity_temperature_scale * np.exp(-gravity_temperature_scale * top_plate_cavity) * np.sqrt(np.pi * kB * trapped_gas_temp / (2 * tritium_mass_atomic / c0**2)) * cavity_radius**2) \ * np.absolute(pure_magnetic_flag * (1 + 0.001) -1) - # C287 - [s/m????] Time constant for evaporation out of the top SKIP FOR NOW - #time_constant_evap_top = + # C287 - [s] Time constant for evaporation out of the top + time_constant_evap_top = calculate_inventory(design_density, volume) / evaporation_top # C288 - [s^-1] Evaporation to the sides - evaporation_sides = (2 * np.pi * cavity_radius * (2 * cavity_radius * cavity_L_over_D) * design_density * np.sqrt(kB * trapped_gas_temp / (2 * np.pi * tritium_mass_atomic / c0**2)) \ + evaporation_sides = (2 * np.pi * cavity_radius * cavity_length * design_density * np.sqrt(kB * trapped_gas_temp / (2 * np.pi * tritium_mass_atomic / c0**2)) \ * np.exp(-eta) * top_plate_cavity * (np.exp(-gravity_temperature_scale * top_cone) - np.exp(-gravity_temperature_scale * top_plate_cavity)) \ / ((1 - np.exp(-gravity_temperature_scale * top_plate_cavity)) * (top_plate_cavity - top_cone))) # C289 [Bq - Decay/s] - Accumulated activity in the sides by end of cycle @@ -172,7 +168,7 @@ def calculate_evaporation_loss(pure_magnetic_flag, trapped_gas_temp, cavity_L_ov # C292 - [s] Time constant for evaporation loss of cone and sides time_constant_evap_cone_sides = calculate_inventory(design_density, volume) / (evaporation_cone + evaporation_sides) # C295 - [atom/s] Flow to keep up with evaporation losses aka Total evaporation_sum - current_evaporation = (evaporation_top/m) + evaporation_sides + evaporation_cone + current_evaporation = evaporation_top + evaporation_sides + evaporation_cone # C294 - [s] Time constant for total evaporation loss time_constant_total_evaporation = calculate_inventory(design_density, volume) / current_evaporation return time_constant_total_evaporation, current_evaporation @@ -453,16 +449,25 @@ def CalcDefaults(self, overwrite=False): self.Jprime_0 = 3.8317 self.cavity_freq = frequency(self.T_endpoint, self.MagneticField.nominal_field) self.CavityRadius() + self.trap_geometry() + self.CavityLength() #Get trap length from cavity length if not specified if ((not hasattr(self.Experiment, 'trap_length')) or overwrite): self.Experiment.trap_length = 0.8 * 2 * self.cavity_radius * self.Experiment.cavity_L_over_D logger.info("Calc'd trap length: {} m".format(round(self.Experiment.trap_length/m, 3), 2)) + elif self.Experiment.trap_length_calc_flag: + self.Experiment.trap_length = self.trap_coil_2 - self.trap_coil_1 + logger.info("Calc'd trap length: {} m".format(round(self.Experiment.trap_length/m, 3), 2)) self.Efficiency = NameSpace({opt: eval(self.cfg.get('Efficiency', opt)) for opt in self.cfg.options('Efficiency')}) self.CavityVolume() self.CavityPower() + # C128 - Calculate Cavity L/D Ratio + if ((hasattr(self.Experiment, 'cavity_L_over_D')) and self.Experiment.cavity_L_over_D_calc_flag): + self.Experiment.cavity_L_over_D = self.cavity_length / (self.cavity_radius * 2) + #Calculate position dependent trapping efficiency self.pos_dependent_trapping_efficiency = trapping_efficiency( z_range = self.Experiment.trap_length /2, bg_magnetic_field = self.MagneticField.nominal_field, @@ -479,9 +484,9 @@ def CalcDefaults(self, overwrite=False): #Ioffe bite used in radial efficiency and effective volume calculation self.unusable_dist_from_wall = self.Efficiency.unusable_dist_from_wall - if self.Efficiency.calculate_ioffe_bite and all(hasattr(self.MagneticField, attr) for attr in ("nominal_field", "magnetic_inhomogenity", "ioffe_field", "ioffe_multipolarity")): - self.unusable_dist_from_wall = ioffe_bite(self.MagneticField.nominal_field, self.MagneticField.magnetic_inhomogenity, self.MagneticField.ioffe_field, self.MagneticField.ioffe_multipolarity, self.cavity_radius) - elif self.Efficiency.calculate_ioffe_bite: + if self.Efficiency.calculate_ioffe_bite_flag and all(hasattr(self.MagneticField, attr) for attr in ("nominal_field", "magnetic_inhomogenity", "ioffe_field", "ioffe_multipolarity")): + self.unusable_dist_from_wall = calculate_ioffe_bite(self.MagneticField.nominal_field, self.MagneticField.magnetic_inhomogenity, self.MagneticField.ioffe_field, self.MagneticField.ioffe_multipolarity, self.cavity_radius) + elif self.Efficiency.calculate_ioffe_bite_flag: logger.info("Error: Did not specify all attributes for Ioffe bite calculation in config file") #Assigning the background constant if it's not in the config file @@ -579,12 +584,28 @@ def CavityRadius(self): axial_mode_index = 1 self.cavity_radius = c0/(2*np.pi*self.cavity_freq)*np.sqrt(self.Jprime_0**2+axial_mode_index**2*np.pi**2/(4*self.Experiment.cavity_L_over_D**2)) return self.cavity_radius + + # Geometry of trap + def trap_geometry(self): + # C089 - [m] origin is the virtual point of the cone height + self.top_cone = 0.60*m + # C090 - [m] height of first trap coil + self.trap_coil_1 = 0.75*m + # C091 - [m] height of second trap coil + self.trap_coil_2 = 4.80*m + # C092 - [m] height of top plate of the cavity + self.top_plate_cavity = 5.30*m + # C093 - [m] height of the top of the vacuum system + self.top_vacuum_system = 7.50*m + return self.top_cone, self.trap_coil_1, self.trap_coil_2, self.top_plate_cavity, self.top_vacuum_system + # C126 - [m] Cavity Length + def CavityLength(self): + self.cavity_length = (self.top_plate_cavity - self.top_cone/2) + return self.cavity_length + def CavityVolume(self): #Calculate vacuum volume including cone and top service volume if self.Experiment.cavity_cone_flag: - #self.top_vacuum_system = 7.50*m - self.top_cone = 0.60*m - self.top_plate_cavity = 5.30*m self.total_cavity_volume = np.pi * self.cavity_radius**2 * (self.top_plate_cavity - (2/3) * self.top_cone) * self.Experiment.n_cavities else: #radius = 0.5*wavelength(self.T_endpoint, self.MagneticField.nominal_field) @@ -592,12 +613,12 @@ def CavityVolume(self): logger.info("Frequency: {} MHz".format(round(self.cavity_freq/MHz, 3))) logger.info("Wavelength: {} cm".format(round(wavelength(self.T_endpoint, self.MagneticField.nominal_field)/cm, 3))) logger.info("Cavity radius: {} cm".format(round(self.cavity_radius/cm, 3))) - logger.info("Cavity length: {} cm".format(round(2*self.cavity_radius*self.Experiment.cavity_L_over_D/cm, 3))) + logger.info("Cavity length: {} cm".format(round(self.cavity_length/cm, 3))) + #logger.info("Cavity length: {} cm".format(round(2*self.cavity_radius*self.Experiment.cavity_L_over_D/cm, 3))) logger.info("Total cavity volume: {} m^3".format(round(self.total_cavity_volume/m**3, 3)))\ return self.total_cavity_volume - # ELECTRON TRAP def TrapVolume(self): # Total volume of the electron traps in all cavities @@ -654,7 +675,8 @@ def BoxTrappingEfficiency(self): return self.box_trapping_efficiency def TrapLength(self): - self.Experiment.trap_length = 0.8 * 2 * self.cavity_radius * self.Experiment.cavity_L_over_D + self.Experiment.trap_length = 0.8 * self.cavity_length + #self.Experiment.trap_length = 0.8 * 2 * self.cavity_radius * self.Experiment.cavity_L_over_D logger.info("Calc'd trap length: {} m".format(round(self.Experiment.trap_length/m, 3), 2)) def CavityPower(self): @@ -1096,10 +1118,9 @@ def print_Efficiencies(self): def print_T2_background_atomic_trap(self): #logger.info("T2 background: {}".format(self.Efficiency.T2_background_atomic_trap)) if self.Efficiency.T2_background_atomic_trap: - T2_total_density, T2_T_ratio = calculate_T2_background_atomic_trap(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D, self.Efficiency.max_ratio_T2_T, self.Experiment.number_density, self.Efficiency.wall_activity) - logger.info("T2_total_density: {} m^-3".format(T2_total_density*m**3)) + T2_total_density, T2_T_ratio = calculate_T2_background_atomic_trap(self.cavity_radius, self.cavity_length, self.FrequencyExtraction.cavity_temperature, self.Efficiency.max_ratio_T2_T, self.Experiment.number_density, self.Efficiency.wall_activity) + logger.info("T2_total_density: {:.4e} m^-3".format(round(T2_total_density*m**3), 4)) logger.info("Ratio T2/T: {}".format(T2_T_ratio)) - def print_pumping_requirements(self): #logger.info("Pumping Calculation: {}".format(self.Efficiency.pumping_calculation)) if self.Efficiency.pumping_calculation: @@ -1108,36 +1129,36 @@ def print_pumping_requirements(self): self.turbopumping_speed_limit = turbopumping_speed_limit(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D, self.Experiment.atomic) self.cavity_termination_speed = cavity_termination_speed(self.Efficiency.turbopumping_speed_cavity_termination_air, self.Efficiency.cavity_top_plate_temperature, self.Experiment.atomic) if self.Experiment.atomic: - logger.info("Pumping Speed Limit(Atomic): {} m^3/s".format(self.turbopumping_speed_limit * s / m**3)) - logger.info("Cavity Termination Speed (Atomic): {} m^3/s".format(self.cavity_termination_speed * s / m**3)) + logger.info("Pumping Speed Limit(Atomic): {:.4} m^3/s".format(self.turbopumping_speed_limit * s / m**3)) + logger.info("Cavity Termination Speed (Atomic): {:.4} m^3/s".format(self.cavity_termination_speed * s / m**3)) else: - logger.info("Pumping Speed Limit(Molecular): {} m^3/s".format(self.turbopumping_speed_limit * s / m**3)) - logger.info("Cavity Termination Speed (Molecular): {} m^3/s".format(self.cavity_termination_speed * s / m**3)) + logger.info("Pumping Speed Limit(Molecular): {:.4} m^3/s".format(self.turbopumping_speed_limit * s / m**3)) + logger.info("Cavity Termination Speed (Molecular): {:.4} m^3/s".format(self.cavity_termination_speed * s / m**3)) self.ratio_required_theoretical_turbopumping_speed = ratio_required_theoretical_turbopumping_speed(self.Efficiency.pumping_speed_required, self.turbopumping_speed_limit, self.cavity_termination_speed, self.turbopump_speed) - logger.info("Ratio of Turbopump Speed Limit: {}".format(self.ratio_required_theoretical_turbopumping_speed)) + logger.info("Ratio of Turbopump Speed Limit: {:.4e}".format(self.ratio_required_theoretical_turbopumping_speed)) if self.Efficiency.T2_heat_leak: self.time_constant_He, self.current_He = calculate_He_heat_leak(self.turbopumping_speed_limit, self.cavity_termination_speed, self.turbopump_speed, self.FrequencyExtraction.cavity_temperature, self.Experiment.design_density, self.total_cavity_volume, self.Efficiency.wall_activity) - logger.info("He Time Constant: {} s".format(self.time_constant_He/s)) - logger.info("Atom current required for He-3 leak: {} atoms/s".format(self.current_He*s)) + logger.info("He Time Constant: {} s".format(round(self.time_constant_He/s), 4)) + logger.info("Atom current required for He-3 leak: {:.4e} atoms/s".format(round(self.current_He*s), 4)) self.time_constant_aperture, self.current_aperture = calculate_aperture_heat_leak(self.DopplerBroadening.gas_temperature, self.Experiment.design_density, self.total_cavity_volume) - logger.info("Aperture Time Constant: {} s".format(self.time_constant_aperture/s)) - logger.info("Atom current required for aperture leak: {} atoms/s".format(self.current_aperture*s)) - self.time_constant_rad, self.current_rad = calculate_rad_heat_leak(self.cavity_radius, self.Experiment.number_density, self.Experiment.cavity_L_over_D, self.Experiment.design_density, self.total_cavity_volume, self.Efficiency.net_rad_efficiency) - logger.info("Radiation Time Constant: {} s".format(self.time_constant_rad/s)) - logger.info("Atom current required for radiation leak: {} atoms/s".format(self.current_rad*s)) - self.time_constant_desorp, self.current_desorp = calculate_T2_desorption_from_wall(self.cavity_radius, self.Experiment.cavity_L_over_D, self.Efficiency.wall_activity, self.Experiment.design_density, self.total_cavity_volume) - logger.info("Desorption Time Constant: {} s".format(self.time_constant_desorp/s)) - logger.info("Atom current required for desorption losses: {} atoms/s".format(self.current_desorp*s)) + logger.info("Aperture Time Constant: {} s".format(round(self.time_constant_aperture/s), 4)) + logger.info("Atom current required for aperture leak: {:.4e} atoms/s".format(round(self.current_aperture*s), 4)) + self.time_constant_rad, self.current_rad = calculate_rad_heat_leak(self.cavity_radius, self.cavity_length, self.Experiment.number_density, self.Experiment.design_density, self.total_cavity_volume, self.Efficiency.net_rad_efficiency) + logger.info("Radiation Time Constant: {} s".format(round(self.time_constant_rad/s), 4)) + logger.info("Atom current required for radiation leak: {:.4e} atoms/s".format(round(self.current_rad*s), 4)) + self.time_constant_desorp, self.current_desorp = calculate_T2_desorption_from_wall(self.cavity_radius, self.cavity_length, self.Efficiency.wall_activity, self.Experiment.design_density, self.total_cavity_volume) + logger.info("Desorption Time Constant: {} s".format(round(self.time_constant_desorp/s), 4)) + logger.info("Atom current required for desorption losses: {:.4e} atoms/s".format(round(self.current_desorp*s), 4)) if not self.Experiment.pure_magnetic_flag: logger.info("***Magnetogravitional Trap***") else: logger.info("***Pure Magnetic Trap***") - self.time_constant_dipolar, self.current_dipolar = calculate_dipolar_loss(self.MagneticField.nominal_field/T, self.cavity_radius, self.Experiment.design_density, self.total_cavity_volume, self.DopplerBroadening.gas_temperature, self.Experiment.cavity_L_over_D, self.Experiment.pure_magnetic_flag) - logger.info("Dipolar Time Constant: {} s".format(self.time_constant_dipolar/s)) - logger.info("Atom current required for dipolar losses: {} atoms/s".format(self.current_dipolar*s)) - self.time_constant_evaporation, self.current_evaporation = calculate_evaporation_loss(self.Experiment.pure_magnetic_flag, self.DopplerBroadening.gas_temperature, self.Experiment.cavity_L_over_D, self.cavity_radius, self.MagneticField.ioffe_field, self.MagneticField.nominal_field, self.Experiment.design_density, self.Efficiency.relative_loss_rate_cone_wall, self.total_cavity_volume) - logger.info("Evaporation Time Constant: {} s".format(self.time_constant_evaporation/s)) - logger.info("Atom current required for Evaporation losses: {} atoms/s".format(self.current_evaporation*s)) + self.time_constant_dipolar, self.current_dipolar = calculate_dipolar_loss(self.MagneticField.nominal_field/T, self.cavity_radius, self.Experiment.design_density, self.total_cavity_volume, self.DopplerBroadening.gas_temperature, self.Experiment.cavity_L_over_D, self.top_cone, self.top_plate_cavity, self.Experiment.pure_magnetic_flag) + logger.info("Dipolar Time Constant: {} s".format(round(self.time_constant_dipolar/s), 4)) + logger.info("Atom current required for dipolar losses: {:.4e} atoms/s".format(round(self.current_dipolar*s), 4)) + self.time_constant_evaporation, self.current_evaporation = calculate_evaporation_loss(self.Experiment.pure_magnetic_flag, self.DopplerBroadening.gas_temperature, self.cavity_radius, self.MagneticField.ioffe_field, self.MagneticField.nominal_field, self.cavity_length, self.top_cone, self.top_plate_cavity, self.Experiment.design_density, self.Efficiency.relative_loss_rate_cone_wall, self.total_cavity_volume) + logger.info("Evaporation Time Constant: {} s".format(round(self.time_constant_evaporation/s), 4)) + logger.info("Atom current required for Evaporation losses: {:.4e} atoms/s".format(round(self.current_evaporation*s), 4)) """ # Cramer-Rao lower bound / how much worse are we than the lower bound From 6be5c8552a0a0512b18258ed409ecbd49a3231e1 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Wed, 18 Feb 2026 17:59:43 -0500 Subject: [PATCH 39/57] Finished Calculations for atom supply to trap and turbopump requirements --- mermithid/misc/Constants_numericalunits.py | 8 +- .../sensitivity/SensitivityCavityFormulas.py | 221 ++++++++++-------- 2 files changed, 133 insertions(+), 96 deletions(-) diff --git a/mermithid/misc/Constants_numericalunits.py b/mermithid/misc/Constants_numericalunits.py index c3f63969..0aec57d6 100644 --- a/mermithid/misc/Constants_numericalunits.py +++ b/mermithid/misc/Constants_numericalunits.py @@ -46,15 +46,15 @@ # C020 - [kg/amu] Conversion kg_amu = 1.66e-27*kg/amu # C021 - Boltzmann Constant [eV/K] = kB * (1 eV / 1.61e-19 J) -kB_eV = 8.6e-5*eV/K +kB_eV = 8.617e-5*eV/K # C027 - [eV/T] Bohr Magneton -bohr_magneton = 5.78e-5*eV/T +bohr_magneton = 5.776e-5*eV/T # C028 - 1 Ci = 3.7e10 Bq Ci_Bq = 3.7e10 # C031 - Ground state branch (atomic) ground_state_branch_atomic = 0.702 # C032 - Ground state branch (molecular) -ground_state_branch_atomic = 0.570 +ground_state_branch_molecular = 0.570 # C036 - [eV] Recoil energy of tritium atom (Bodine) atomic_tritium_recoil_energy = 3.409*eV # C038 - [eV] Binding energy of tritium molecule (Bodine) @@ -73,6 +73,8 @@ LGd_rates = 50 # C049 - per beta decay: see: https://www.overleaf.com/2817746228snnghrnzfthk molecules_desorbed_wall_beta = 1000 +# C050 - eV to J conversion +eV_J = 1.605e-19 *J/eV # units that do not show up in numericalunits # missing pre-factors diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index 02959158..f8eebbcd 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -36,6 +36,7 @@ def calculate_ioffe_bite(nominal_field, magnetic_inhomogenity, ioffe_field, ioff # C271 - [m/s] Atomic thermal speed of 3T or 3He (C270) def calculate_average_velocity(temp): average_velocity = c0 * np.sqrt((8 * 0.025*eV * temp / np.absolute(T0)) / (np.pi * tritium_mass_atomic)) + #average_velocity = c0 * np.sqrt((8 * kB * temp) / (np.pi * tritium_mass_atomic * eV_J)) return average_velocity # C334 - [m^2] Cylinder wall; no endcaps included (2*pi*R*L) @@ -44,7 +45,7 @@ def calculate_trap_wall_area(cavity_radius, cavity_length): trap_wall_area = 2 * np.pi * cavity_radius * cavity_length return trap_wall_area -# C204 - [atoms] Inventory in physical volume +# C204 - [atom] Inventory in physical volume def calculate_inventory(design_density, volume): return design_density * volume @@ -54,19 +55,18 @@ def calculate_activity_in_trap(number_density, cavity_radius): coil_1_height = 0.75*m # C091 - [m] coil_2_height = 4.80*m - # C206 - [atoms] Use exponential density vertical gradient, calculate mean density between trap coils. Calculate atoms and activity between the trap coils. + # C206 - [atom] Use exponential density vertical gradient, calculate mean density between trap coils. Calculate atoms and activity between the trap coils. atoms_between_trap_coils = number_density * np.pi * cavity_radius**2 * np.absolute(coil_2_height - coil_1_height) return atoms_between_trap_coils / tritium_livetime # Background T2 in atomic trap: def calculate_T2_background_atomic_trap(cavity_radius, cavity_length, cavity_wall_temp, max_ratio_T2_T, number_density, wall_activity): - trap_wall_area = calculate_trap_wall_area(cavity_radius, cavity_length) # C306 - [mbar] Vapor Pressure of T2 w/ constants for saturated T2 vapor from Souers et al. T2_vapor_pressure = mbar * np.exp(T2_vapor_A + (T2_vapor_B*K/cavity_wall_temp) + T2_vapor_B_prime * np.log(cavity_wall_temp/K)) / 0.76 # C307 - [m^-3] Density of saturated vapor sat_vapor_density = NA * (T2_vapor_pressure/mbar) * np.absolute(T0) / (1000 * molar_volume * cavity_wall_temp) # C308 - [m^-3] = s^-1 * sqrt(kg/eV) / m^2 T2 density form desorption at end of a cycle - T2_density_desorp = 4 * molecules_desorbed_wall_beta * Ci_Bq * wall_activity * np.sqrt(molecules_desorbed_wall_beta * (tritium_mass_atomic / c0**2) / (2 * atomic_tritium_recoil_energy * 1.6*10**-19 * (J/eV) )) / trap_wall_area + T2_density_desorp = 4 * molecules_desorbed_wall_beta * Ci_Bq * wall_activity * np.sqrt(molecules_desorbed_wall_beta * (tritium_mass_atomic / c0**2) / (2 * atomic_tritium_recoil_energy * eV_J )) / calculate_trap_wall_area(cavity_radius, cavity_length) # C309 [m^-3] Total T2 density T2_total_density = sat_vapor_density + T2_density_desorp # C310 - T2/T number ratio. Activity ratio is 1.64 times bigger @@ -74,19 +74,6 @@ def calculate_T2_background_atomic_trap(cavity_radius, cavity_length, cavity_wal T2_T_ratio = T2_total_density / number_density return T2_total_density, T2_T_ratio -# He Heat Leak: Notable difference comes from pumping speed, physical volume, and design density -def calculate_He_heat_leak(pumping_speed_theoretical, pumping_cavity_termination, turbopump_speed, cavity_temperature, design_density, volume, wall_activity): - He_velocity = calculate_average_velocity(cavity_temperature) - # C266 - [atoms/s] Not including flow for He heat leak itself - He_production_rate = wall_activity * Ci_Bq - # C269 - [m^-3] Number density of Helium - He_density = He_production_rate * ((1/pumping_speed_theoretical) + (1/pumping_cavity_termination) + (1/turbopump_speed)) - # C272 - [s] Time constant for loss due to He-3 heat leak - time_constant_He = 1 / (He_density * He_velocity * H_He_crosssection) - # C273 - [s^-1] Atom current required to keep up with He-3 heat leak - current_He_leak = calculate_inventory(design_density, volume) / time_constant_He - return time_constant_He, current_He_leak - # Aperture Heat Leak: def calculate_aperture_heat_leak(trapped_gas_temp, design_density, volume): global area_loading_aperture @@ -95,18 +82,17 @@ def calculate_aperture_heat_leak(trapped_gas_temp, design_density, volume): # C250 - [m/s] Average velocity of trapped gas trapped_gas_velocity = calculate_average_velocity(trapped_gas_temp) # C251 - [atom/s] Inward atom current required through aperture to balance losses (j_aperture) - current_aperture = area_loading_aperture * design_density * trapped_gas_velocity / 4 + current_aperture_leak = area_loading_aperture * design_density * trapped_gas_velocity / 4 # C252 - [s] Time constant for loss through aperture - time_constant_aperture = calculate_inventory(design_density, volume) / current_aperture - return time_constant_aperture, current_aperture + time_constant_aperture = calculate_inventory(design_density, volume) / current_aperture_leak + return time_constant_aperture, current_aperture_leak # Radioactivity Heat Leak: -def calculate_rad_heat_leak(cavity_radius, cavity_length, number_density, design_density, volume, net_efficiency): +def calculate_rad_heat_leak(cavity_radius, number_density, design_density, volume, net_efficiency): # C256 - [s] Input from Ben Clark's thesis with a mirror ratio of 0.5 half_life = 11 * 24 * 3600 * s - trap_wall_area = calculate_trap_wall_area(cavity_radius, cavity_length) trap_activity = calculate_activity_in_trap(number_density, cavity_radius) - # C257 - [atoms/s] Atom current required to balance loss due to radioactivity + # C257 - [atom/s] Atom current required to balance loss due to radioactivity current_rad_leak = trap_activity * np.log(2) * tritium_livetime / (net_efficiency * half_life) # C258 - [s] Time constant for loss due to radioactivty (tau_rad) time_constant_rad = calculate_inventory(design_density, volume) / current_rad_leak @@ -114,13 +100,25 @@ def calculate_rad_heat_leak(cavity_radius, cavity_length, number_density, design # T2 Desorption from the wall: def calculate_T2_desorption_from_wall(cavity_radius, cavity_length, wall_activity, design_density, volume): - trap_wall_area = calculate_trap_wall_area(cavity_radius, cavity_length) # C261 - [s] Mean lifetime of atom in trap from desorption - time_constant_desorp = trap_wall_area / (2 * molecules_desorbed_wall_beta * wall_activity * Ci_Bq * H_H2_crosssection) - # C262 - [atoms/s] Atom current required to keep up with desorption losses + time_constant_desorp = calculate_trap_wall_area(cavity_radius, cavity_length) / (2 * molecules_desorbed_wall_beta * wall_activity * Ci_Bq * H_H2_crosssection) + # C262 - [atom/s] Atom current required to keep up with desorption losses current_desorp = calculate_inventory(design_density, volume) / time_constant_desorp return time_constant_desorp, current_desorp +# He Heat Leak: Notable difference comes from pumping speed, physical volume, and design density +def calculate_He_heat_leak(pumping_speed_theoretical, pumping_cavity_termination, turbopump_speed, cavity_temperature, design_density, volume, wall_activity): + He_velocity = calculate_average_velocity(cavity_temperature) + # C266 - [atom/s] Not including flow for He heat leak itself + He_production_rate = wall_activity * Ci_Bq + # C269 - [m^-3] Number density of Helium + He_density = He_production_rate * ((1/pumping_speed_theoretical) + (1/pumping_cavity_termination) + (1/turbopump_speed)) + # C272 - [s] Time constant for loss due to He-3 heat leak + time_constant_He = 1 / (He_density * He_velocity * H_He_crosssection) + # C273 - [s^-1] Atom current required to keep up with He-3 heat leak + current_He_leak = calculate_inventory(design_density, volume) / time_constant_He + return time_constant_He, current_He_leak + # Dipolar loss rate: Calculation with z-dependent density, cylinder & cone. def calculate_dipolar_loss(nominal_field, cavity_radius, design_density, volume, trapped_gas_temp, cavity_L_over_D, top_cone, top_plate_cavity, pure_magnetic_flag): global gravity_temperature_scale, surface_density_scale @@ -131,7 +129,7 @@ def calculate_dipolar_loss(nominal_field, cavity_radius, design_density, volume, # C298 - [m^3/s] Polynomial fit for Dipolar spin-flip rate (G_dd). Depends on field (Lagendijk et al). Polynomial-log fit used now. dipolar_spin_flip_rate = (60.106 + 13.812 * np.log(nominal_field) - 4.7867 * np.log(nominal_field)**2 - 2.3192 * np.log(nominal_field)**3 \ - 0.32663 * np.log(nominal_field)**4 - 0.015775 * np.log(nominal_field)**5) * 1e-22 * LGd_rates * m**3/s - # C299 - [atoms/s] Flow to keep up with dipolar losses + # C299 - [atom/s] Flow to keep up with dipolar losses current_dipolar = dipolar_spin_flip_rate * np.pi * cavity_radius**2 * surface_density_scale**2 * (gravity_temperature_scale * (np.exp(-gravity_temperature_scale * top_cone) \ - np.exp(-gravity_temperature_scale * top_plate_cavity)) / 2 + (1 - np.exp(-2 * gravity_temperature_scale * top_cone) * (2 * gravity_temperature_scale**2 \ * top_cone**2 + 2 * gravity_temperature_scale * top_cone + 1)) / (4 * top_cone**2 * gravity_temperature_scale)) @@ -174,45 +172,71 @@ def calculate_evaporation_loss(pure_magnetic_flag, trapped_gas_temp, cavity_radi return time_constant_total_evaporation, current_evaporation # T2 Heat Leak: -#def calculate_T2_heat_leak(trapped_gas_temp, design_density, volume): - # C277 - [atoms/s] T2 production rate (all atoms entering, except for T2 heat leak itself) - #T2_production_rate = -''' # C328 - made into config file instead -#def turbopumping_speed_required(atom_current, molecular_density): -# return atom_current / 2 / molecular_density -''' +def calculate_T2_heat_leak(aperture_current, rad_current, He_current, evap_current, dipolar_current, T2_total_density, cavity_wall_temp, design_density, volume): + # C277 - [atom/s] T2 production rate (all atoms entering, except for T2 heat leak itself) + T2_production_rate = aperture_current + rad_current + He_current + evap_current + dipolar_current + # C279 - [s] Time constant for loss due to T2 heat leak + time_constant_T2 = 1 / (T2_total_density * H_H2_crosssection * calculate_average_velocity(cavity_wall_temp) / np.sqrt(2)) + # C280 - [atom/s] + current_T2_leak = calculate_inventory(design_density, volume) / time_constant_T2 + return time_constant_T2, current_T2_leak + +# C303 - [s] Total lifetime of trap: +def calculate_trap_lifetime(time_rad, time_desorp, time_He, time_T2, time_evap, time_dipolar): + return 1 / ((1/time_rad) + (1/time_desorp) + (1/time_He) + (1/time_T2) + (1/time_evap) + (1/time_dipolar)) +# Atom Supply into Trap: +def calculate_trap_atom_supply(rad_current, evap_current, dipolar_current, He_current, T2_current, design_density, volume): + # The ‘atomic current required’ includes the c-state atoms as well as d-state, although those are lost almost immediately to spin exchange. + # C107 - C state flag for hyperfine states for total gas into trap. Trap is always d-state only. No c-states (1), include c-states (2) + c_states_flag = 1 + # C318 - [atom/s] Total current is atom current (d state only) + He heat leak + T2 heat leak + total_atom_current = rad_current + evap_current + dipolar_current + He_current + T2_current + # C319 - [s] Total time constant for atoms to remain in the trap + total_time_constant_trap = calculate_inventory(design_density, volume) / total_atom_current + # C320 - [atom/s] Atom current with c-states and d-states in calculation + total_atom_current_states = total_atom_current * c_states_flag + return total_time_constant_trap, total_atom_current_states ''' There are two possible methods for pumping away the tritium, all of which is eventually in molecular form. One is to keep the tritium in circulation by using turbopumps and avoiding temperatures below 10K in the trap region, and the second is to cool parts of the trap region outside the magnetic wall to < 3K in order to cryopump the tritium. The mechanical pumping method is impractical because of the pumping speed required, a fraction of a billion L/s, where an achievable upper limit is 4 orders of magnitude smaller. The speed requirement is driven by the combination of the input atomic current and the need to maintain the molecular fraction below 10−4. The cryopumping method ties up very large amounts of tritium, tens to hundreds of kCi, by the end of a day, and the need to warm up and recycle that tritium on such a short time scale would lead to low statistical precision and instabilities. +Still need turbos to handle He-3 and to do pumpout during recycling, but initial hope that we could run turbos only and keep the T2 pressure low enough was not realized. Must recycle. ''' # Turbopump Calculations: Need to convert tritium mass amu to eV : 1 amu ~ 931 MeV/c^2 and kB = 8.6E-05 eV/K. Multiply by sqrt(2) if atomic; # Useful for Atomic and Helium-3 # C233/C236 - [m^3/s] (molecular/atomic) theoretical pumping speed. The mean speed of the cylinder connected to a perfect pump (Dushman). Uniform source, outputs mean density in cavity. Cannot exceed (obstruction not included). -def turbopumping_speed_limit(cavity_radius, cavity_temperature, cavity_L_over_D, atomic_flag): - return (np.pi * cavity_radius**2 * c0 * np.sqrt(kB_eV * cavity_temperature / (4 * np.pi * tritium_mass_atomic)) / (0.5 + cavity_L_over_D / 8)) * (np.sqrt(2) * atomic_flag) +def turbopump_speed_limit(cavity_radius, cavity_temperature, cavity_L_over_D, atomic_flag): + return (np.pi * cavity_radius**2 * c0 * np.sqrt(kB_eV * cavity_temperature / (4 * np.pi * tritium_mass_atomic)) / (0.5 + cavity_L_over_D / 8)) * (1 + atomic_flag * (np.sqrt(2) - 1)) # C235/C238 - [m^3/s] (molecular/atomic) Assumed ambient room air (28-29 amu) temperature (293 K, and a pumping speed of 0.5 L/s def cavity_termination_speed(turbopumping_speed_air, cavity_top_plate_temp, atomic_flag): - return (turbopumping_speed_air * np.sqrt(cavity_top_plate_temp * 28 * amu * c0**2 / (293 * K * 2 * tritium_mass_atomic))) * (np.sqrt(2) * atomic_flag) + return (turbopumping_speed_air * np.sqrt(cavity_top_plate_temp * 28 * amu * c0**2 / (293 * K * 2 * tritium_mass_atomic))) * (1 + atomic_flag * (np.sqrt(2) - 1)) # C239 - [m^3/s] Turbopump in series speed (2.5 m^3/s * 2) def turbopump_speed(number_turbos, turbopumping_speed_gas): return number_turbos * turbopumping_speed_gas +# C327 - [m^-3] Molecular density allowed by molecular/atomic assuming total atom density in all of physical volume +def molecular_density_allowed(design_density, max_ratio_T2_T): + return design_density * max_ratio_T2_T * ground_state_branch_atomic / 2 +# The Pumping speed required (molecular) in C328 is kind of a dimensional calculation, not really a speed. As a result, the cryopumping speed limit is probably not real. Ideal cryopumping is the impingement rate: it all sticks. The actual molecular density is determined by the vapor pressure (Souers formula). +# C328 - [m^3/s] - Pumping speed required to keep molecular pressure from exceeding reference limit +def turbopump_speed_required(atom_current, molecular_density_limit): + return atom_current / 2 / molecular_density_limit # C331 - If above 1000, critical -def ratio_required_theoretical_turbopumping_speed(pumping_speed_required, pumping_speed_limit, cavity_termination_speed, turbopump_speed): +def ratio_turbopump_speed(pumping_speed_required, pumping_speed_limit, cavity_termination_speed, turbopump_speed): return pumping_speed_required * ((1 / pumping_speed_limit) + (1 / cavity_termination_speed) + (1 / turbopump_speed)) -''' -# Atom Supply into Trap: -# Total current is atom current (d state only) + He heat leak + T2 heat leak -#def total_current(): -# return -# (No c state = 1, Include c state = 2). For total gas into trap; trap is always d-state only. The ‘atomic current required’ includes the c-state atoms as well as d-state, although those are lost almost immediately to spin exchange. -#def atom_current (c_state_flag = 1, total_current): -# return c_state_flag * total_current -# Molecular density allowed by molecular/atomic assuming total atom density in all of physical volume -def molecular_density_allowed(atom_density=1.5*10**17/m**3, max_ratio_nM_nA = 10**-4, GS_atomic_branch = 7.02*10**-1): - return atom_density * max_ratio_nM_nA * GS_atomic_branch / 2 -''' + +# Cryopumping Calculations: +def calculate_cryopump_speed(cavity_wall_temp, cavity_radius, cavity_length, pumping_speed_required): + # C335 - [m/s] Cryopumping speed per area for mass-3 atoms at wall temperature stated above. sqrt(k T/(2 pi M)); see https://www.synsysco.com/wp-content/uploads/2016/02/Basics-of-Cryopumping-Booklet.pdf. Using M=3 because most gas is spin-flipped atoms. + cryopump_speed_per_area = np.sqrt(kB_eV * cavity_wall_temp / (2 * np.pi * tritium_mass_atomic)) * c0 + # C336 - [m^3/s] With given cylindrical dimensions at wall temperature stated above for mass-3 atoms + trap_interior_surface_speed = cryopump_speed_per_area * calculate_trap_wall_area(cavity_radius, cavity_length) + ratio_cryopump_speed = pumping_speed_required / trap_interior_surface_speed + return ratio_cryopump_speed + + + +# Injection Line Calculations: @@ -451,18 +475,19 @@ def CalcDefaults(self, overwrite=False): self.CavityRadius() self.trap_geometry() self.CavityLength() - + + self.Efficiency = NameSpace({opt: eval(self.cfg.get('Efficiency', opt)) for opt in self.cfg.options('Efficiency')}) + self.CavityVolume() + self.CavityPower() + #Get trap length from cavity length if not specified if ((not hasattr(self.Experiment, 'trap_length')) or overwrite): self.Experiment.trap_length = 0.8 * 2 * self.cavity_radius * self.Experiment.cavity_L_over_D logger.info("Calc'd trap length: {} m".format(round(self.Experiment.trap_length/m, 3), 2)) elif self.Experiment.trap_length_calc_flag: + # C129 - Trap L/D = Trap L / 2 / cavity radius self.Experiment.trap_length = self.trap_coil_2 - self.trap_coil_1 - logger.info("Calc'd trap length: {} m".format(round(self.Experiment.trap_length/m, 3), 2)) - - self.Efficiency = NameSpace({opt: eval(self.cfg.get('Efficiency', opt)) for opt in self.cfg.options('Efficiency')}) - self.CavityVolume() - self.CavityPower() + logger.info("Calc'd trap length from coils: {} m".format(round(self.Experiment.trap_length/m, 3), 2)) # C128 - Calculate Cavity L/D Ratio if ((hasattr(self.Experiment, 'cavity_L_over_D')) and self.Experiment.cavity_L_over_D_calc_flag): @@ -674,11 +699,6 @@ def BoxTrappingEfficiency(self): self.box_trapping_efficiency = np.cos(self.FrequencyExtraction.minimum_angle_in_bandwidth) return self.box_trapping_efficiency - def TrapLength(self): - self.Experiment.trap_length = 0.8 * self.cavity_length - #self.Experiment.trap_length = 0.8 * 2 * self.cavity_radius * self.Experiment.cavity_L_over_D - logger.info("Calc'd trap length: {} m".format(round(self.Experiment.trap_length/m, 3), 2)) - def CavityPower(self): #Jprime_0 = 3.8317 max_ax_freq, mean_field, z_t = axial_motion(self.MagneticField.nominal_field, @@ -1102,10 +1122,8 @@ def print_SNRs(self, rho=None): def print_Efficiencies(self): - logger.info("Effective volume: {} mm^3".format(round(self.effective_volume/mm**3, 3))) logger.info("Total efficiency: {}".format(self.effective_volume/self.total_trap_volume)) - if not self.Efficiency.usefixedvalue: # radial and detection efficiency are configured in the config file logger.info("Radial efficiency: {}".format(self.radial_efficiency)) @@ -1118,47 +1136,64 @@ def print_Efficiencies(self): def print_T2_background_atomic_trap(self): #logger.info("T2 background: {}".format(self.Efficiency.T2_background_atomic_trap)) if self.Efficiency.T2_background_atomic_trap: - T2_total_density, T2_T_ratio = calculate_T2_background_atomic_trap(self.cavity_radius, self.cavity_length, self.FrequencyExtraction.cavity_temperature, self.Efficiency.max_ratio_T2_T, self.Experiment.number_density, self.Efficiency.wall_activity) - logger.info("T2_total_density: {:.4e} m^-3".format(round(T2_total_density*m**3), 4)) - logger.info("Ratio T2/T: {}".format(T2_T_ratio)) + self.T2_total_density, self.T2_T_ratio = calculate_T2_background_atomic_trap(self.cavity_radius, self.cavity_length, self.FrequencyExtraction.cavity_temperature, self.Efficiency.max_ratio_T2_T, self.Experiment.number_density, self.Efficiency.wall_activity) + logger.info("T2_total_density: {:.4e} m^-3".format(round(self.T2_total_density*m**3), 4)) + logger.info("Ratio T2/T: {:.4e}".format(self.T2_T_ratio)) + def print_pumping_requirements(self): #logger.info("Pumping Calculation: {}".format(self.Efficiency.pumping_calculation)) if self.Efficiency.pumping_calculation: - self.turbopump_speed = turbopump_speed(self.Efficiency.number_turbopumps, self.Efficiency.turbopumping_speed_gas_T2) + self.turbopump_speed = turbopump_speed(self.Efficiency.number_turbopumps, self.Efficiency.turbopump_speed_gas_T2) logger.info("Turbopump Speed: {} m^3/s".format(self.turbopump_speed * s / m**3)) - self.turbopumping_speed_limit = turbopumping_speed_limit(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D, self.Experiment.atomic) - self.cavity_termination_speed = cavity_termination_speed(self.Efficiency.turbopumping_speed_cavity_termination_air, self.Efficiency.cavity_top_plate_temperature, self.Experiment.atomic) - if self.Experiment.atomic: - logger.info("Pumping Speed Limit(Atomic): {:.4} m^3/s".format(self.turbopumping_speed_limit * s / m**3)) - logger.info("Cavity Termination Speed (Atomic): {:.4} m^3/s".format(self.cavity_termination_speed * s / m**3)) - else: - logger.info("Pumping Speed Limit(Molecular): {:.4} m^3/s".format(self.turbopumping_speed_limit * s / m**3)) - logger.info("Cavity Termination Speed (Molecular): {:.4} m^3/s".format(self.cavity_termination_speed * s / m**3)) - self.ratio_required_theoretical_turbopumping_speed = ratio_required_theoretical_turbopumping_speed(self.Efficiency.pumping_speed_required, self.turbopumping_speed_limit, self.cavity_termination_speed, self.turbopump_speed) - logger.info("Ratio of Turbopump Speed Limit: {:.4e}".format(self.ratio_required_theoretical_turbopumping_speed)) - if self.Efficiency.T2_heat_leak: - self.time_constant_He, self.current_He = calculate_He_heat_leak(self.turbopumping_speed_limit, self.cavity_termination_speed, self.turbopump_speed, self.FrequencyExtraction.cavity_temperature, self.Experiment.design_density, self.total_cavity_volume, self.Efficiency.wall_activity) - logger.info("He Time Constant: {} s".format(round(self.time_constant_He/s), 4)) - logger.info("Atom current required for He-3 leak: {:.4e} atoms/s".format(round(self.current_He*s), 4)) - self.time_constant_aperture, self.current_aperture = calculate_aperture_heat_leak(self.DopplerBroadening.gas_temperature, self.Experiment.design_density, self.total_cavity_volume) - logger.info("Aperture Time Constant: {} s".format(round(self.time_constant_aperture/s), 4)) - logger.info("Atom current required for aperture leak: {:.4e} atoms/s".format(round(self.current_aperture*s), 4)) - self.time_constant_rad, self.current_rad = calculate_rad_heat_leak(self.cavity_radius, self.cavity_length, self.Experiment.number_density, self.Experiment.design_density, self.total_cavity_volume, self.Efficiency.net_rad_efficiency) - logger.info("Radiation Time Constant: {} s".format(round(self.time_constant_rad/s), 4)) - logger.info("Atom current required for radiation leak: {:.4e} atoms/s".format(round(self.current_rad*s), 4)) + self.turbopump_speed_limit_atomic = turbopump_speed_limit(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D, self.Experiment.atomic) + self.cavity_termination_speed_atomic = cavity_termination_speed(self.Efficiency.turbopump_speed_cavity_termination_air, self.Efficiency.cavity_top_plate_temperature, self.Experiment.atomic) + self.turbopump_speed_limit_molecular = turbopump_speed_limit(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D, not self.Experiment.atomic) + self.cavity_termination_speed_molecular = cavity_termination_speed(self.Efficiency.turbopump_speed_cavity_termination_air, self.Efficiency.cavity_top_plate_temperature, not self.Experiment.atomic) + logger.info("Pumping Speed Limit (Atomic): {:.4} m^3/s".format(self.turbopump_speed_limit_atomic * s / m**3)) + logger.info("Cavity Termination Speed (Atomic): {:.4} m^3/s".format(self.cavity_termination_speed_atomic * s / m**3)) + logger.info("Pumping Speed Limit (Molecular): {:.4} m^3/s".format(self.turbopump_speed_limit_molecular * s / m**3)) + logger.info("Cavity Termination Speed (Molecular): {:.4} m^3/s".format(self.cavity_termination_speed_molecular * s / m**3)) + if self.Efficiency.T_atom_supply_trap: + self.time_constant_He, self.current_He_leak = calculate_He_heat_leak(self.turbopump_speed_limit_atomic, self.cavity_termination_speed_atomic, self.turbopump_speed, self.FrequencyExtraction.cavity_temperature, self.Experiment.design_density, self.total_cavity_volume, self.Efficiency.wall_activity) + logger.info("He Time Constant: {:.4} s".format(self.time_constant_He/s)) + logger.info("Atom current required for He-3 leak: {:.4e} atoms/s".format(self.current_He_leak*s)) + self.time_constant_aperture, self.current_aperture_leak = calculate_aperture_heat_leak(self.DopplerBroadening.gas_temperature, self.Experiment.design_density, self.total_cavity_volume) + logger.info("Aperture Time Constant: {:.4} s".format(self.time_constant_aperture/s)) + logger.info("Atom current required for aperture leak: {:.4e} atoms/s".format(self.current_aperture_leak*s)) + self.time_constant_rad, self.current_rad_leak = calculate_rad_heat_leak(self.cavity_radius, self.Experiment.number_density, self.Experiment.design_density, self.total_cavity_volume, self.Efficiency.net_rad_efficiency) + logger.info("Radiation Time Constant: {:.4} s".format(self.time_constant_rad/s)) + logger.info("Atom current required for radiation leak: {:.4e} atoms/s".format(self.current_rad_leak*s)) self.time_constant_desorp, self.current_desorp = calculate_T2_desorption_from_wall(self.cavity_radius, self.cavity_length, self.Efficiency.wall_activity, self.Experiment.design_density, self.total_cavity_volume) - logger.info("Desorption Time Constant: {} s".format(round(self.time_constant_desorp/s), 4)) - logger.info("Atom current required for desorption losses: {:.4e} atoms/s".format(round(self.current_desorp*s), 4)) + logger.info("Desorption Time Constant: {:.4} s".format(self.time_constant_desorp/s)) + logger.info("Atom current required for desorption losses: {:.4e} atoms/s".format(self.current_desorp*s)) if not self.Experiment.pure_magnetic_flag: logger.info("***Magnetogravitional Trap***") else: logger.info("***Pure Magnetic Trap***") self.time_constant_dipolar, self.current_dipolar = calculate_dipolar_loss(self.MagneticField.nominal_field/T, self.cavity_radius, self.Experiment.design_density, self.total_cavity_volume, self.DopplerBroadening.gas_temperature, self.Experiment.cavity_L_over_D, self.top_cone, self.top_plate_cavity, self.Experiment.pure_magnetic_flag) - logger.info("Dipolar Time Constant: {} s".format(round(self.time_constant_dipolar/s), 4)) - logger.info("Atom current required for dipolar losses: {:.4e} atoms/s".format(round(self.current_dipolar*s), 4)) + logger.info("Dipolar Time Constant: {:.4} s".format(self.time_constant_dipolar/s)) + logger.info("Atom current required for dipolar losses: {:.4e} atoms/s".format(self.current_dipolar*s)) self.time_constant_evaporation, self.current_evaporation = calculate_evaporation_loss(self.Experiment.pure_magnetic_flag, self.DopplerBroadening.gas_temperature, self.cavity_radius, self.MagneticField.ioffe_field, self.MagneticField.nominal_field, self.cavity_length, self.top_cone, self.top_plate_cavity, self.Experiment.design_density, self.Efficiency.relative_loss_rate_cone_wall, self.total_cavity_volume) - logger.info("Evaporation Time Constant: {} s".format(round(self.time_constant_evaporation/s), 4)) - logger.info("Atom current required for Evaporation losses: {:.4e} atoms/s".format(round(self.current_evaporation*s), 4)) + logger.info("Evaporation Time Constant: {:.4} s".format(self.time_constant_evaporation/s)) + logger.info("Atom current required for Evaporation losses: {:.4e} atoms/s".format(self.current_evaporation*s)) + self.time_constant_T2, self.current_T2_leak = calculate_T2_heat_leak(self.current_aperture_leak, self.current_rad_leak, self.current_He_leak, self.current_evaporation, self.current_dipolar, self.T2_total_density, self.FrequencyExtraction.cavity_temperature, self.Experiment.design_density, self.total_cavity_volume) + logger.info("T2 Time Constant: {:.4} s".format(self.time_constant_T2/s)) + logger.info("Atom current required for T2 leak: {:.4e} atoms/s".format(self.current_T2_leak*s)) + self.trap_lifetime = calculate_trap_lifetime(self.time_constant_rad, self.time_constant_desorp, self.time_constant_He, self.time_constant_T2, self.time_constant_evaporation, self.time_constant_dipolar) + self.total_time_constant_trap, self.total_atom_current = calculate_trap_atom_supply(self.current_rad_leak, self.current_evaporation, self.current_dipolar, self.current_He_leak, self.current_T2_leak, self.Experiment.design_density, self.total_cavity_volume) + logger.info("Total trap time constant: {:.4} s".format(self.total_time_constant_trap/s)) + logger.info("Total atom current: {:.4e} atoms/s".format(self.total_atom_current*s)) + logger.info("Trap lifetime: {:.4} s".format(self.trap_lifetime/s)) + + self.molecular_density_allowed = molecular_density_allowed(self.Experiment.design_density, self.Efficiency.max_ratio_T2_T) + self.turbopump_speed_required = turbopump_speed_required(self.total_atom_current, self.molecular_density_allowed) + logger.info("Turbopump Speed required: {} m^3/s".format(self.turbopump_speed_required * s / m**3)) + self.ratio_turbopump_speed = ratio_turbopump_speed(self.turbopump_speed_required, self.turbopump_speed_limit_molecular, self.cavity_termination_speed_molecular, self.turbopump_speed) + logger.info("Ratio of Turbopump Speed Limit: {:.4e}".format(self.ratio_turbopump_speed)) + if (self.ratio_turbopump_speed >= 1.0): logger.info("CRITICAL PUMPING REACHED! Experiment cannot procede with turbopumps.") + self.ratio_cryopump_speed = calculate_cryopump_speed(self.FrequencyExtraction.cavity_temperature, self.cavity_radius, self.cavity_length, self.turbopump_speed_required) + logger.info("Ratio of Cryopump Speed: {:.4}".format(self.ratio_cryopump_speed)) + if (self.ratio_cryopump_speed >= 1.0): logger.info("CRITICAL PUMPING REACHED! The molecular pumping speed required is a dimensional calculation, not really a speed. As a result, the cryopumping speed limit is not real. Ideal cryopumping is the impingement rate: it all sticks. The actual molecular density is determined by the vapor pressure.") """ # Cramer-Rao lower bound / how much worse are we than the lower bound From 8f66ac54eee0c41476cb87ca326c18e343fb5fca Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Thu, 19 Feb 2026 11:15:10 -0500 Subject: [PATCH 40/57] Added documentation to source loss terms --- .../sensitivity/SensitivityCavityFormulas.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index f8eebbcd..3c5313b5 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -30,6 +30,7 @@ area_loading_aperture, gravity_temperature_scale, surface_density_scale = (None, None, None) # C157 - [m] Fiducial distance from the cavity wall between max(ioffe_bite, larmor_radius) +# Ioffe field at wall is now the quadrature of the central field and Ioffe field by itself, everywhere it is used. def calculate_ioffe_bite(nominal_field, magnetic_inhomogenity, ioffe_field, ioffe_multipolarity, cavity_radius): return ((2 * nominal_field**2 * magnetic_inhomogenity / (2 * nominal_field**2 * magnetic_inhomogenity + ioffe_field**2))**(1/(ioffe_multipolarity-2)) * cavity_radius * -1) + cavity_radius @@ -88,6 +89,7 @@ def calculate_aperture_heat_leak(trapped_gas_temp, design_density, volume): return time_constant_aperture, current_aperture_leak # Radioactivity Heat Leak: +# Radioactivity loss is just a single number from Ben Clark’s thesis, needs to be done better. def calculate_rad_heat_leak(cavity_radius, number_density, design_density, volume, net_efficiency): # C256 - [s] Input from Ben Clark's thesis with a mirror ratio of 0.5 half_life = 11 * 24 * 3600 * s @@ -98,7 +100,8 @@ def calculate_rad_heat_leak(cavity_radius, number_density, design_density, volum time_constant_rad = calculate_inventory(design_density, volume) / current_rad_leak return time_constant_rad, current_rad_leak -# T2 Desorption from the wall: +# T2 Desorption from the wall: T2 desorption from walls as a background and a trap-heating loss. +# Burst of molecules is emitted from the wall with each decay and can knock out atoms from the trap. Description added to end of CDR 4.5.5. New parameter to enter is the number of molecules (choose 1000 for now). def calculate_T2_desorption_from_wall(cavity_radius, cavity_length, wall_activity, design_density, volume): # C261 - [s] Mean lifetime of atom in trap from desorption time_constant_desorp = calculate_trap_wall_area(cavity_radius, cavity_length) / (2 * molecules_desorbed_wall_beta * wall_activity * Ci_Bq * H_H2_crosssection) @@ -122,9 +125,9 @@ def calculate_He_heat_leak(pumping_speed_theoretical, pumping_cavity_termination # Dipolar loss rate: Calculation with z-dependent density, cylinder & cone. def calculate_dipolar_loss(nominal_field, cavity_radius, design_density, volume, trapped_gas_temp, cavity_L_over_D, top_cone, top_plate_cavity, pure_magnetic_flag): global gravity_temperature_scale, surface_density_scale - # C201 - [m^-1] + # C201 - [m^-1] Add the gravity-temperature scale parameter “b” for magnetogravitational trap gravity_temperature_scale = ((tritium_mass_atomic/c0**2 * gravity / (kB * trapped_gas_temp)) * np.absolute(pure_magnetic_flag * (1 + 1 / cavity_L_over_D) - 1)) - # C202 - [m^-2] + # C202 - [m^-2] Add the surface density scale parameter “a/b” for magnetogravitational trap surface_density_scale = design_density * top_plate_cavity / (1 - np.exp(-gravity_temperature_scale * top_plate_cavity)) # C298 - [m^3/s] Polynomial fit for Dipolar spin-flip rate (G_dd). Depends on field (Lagendijk et al). Polynomial-log fit used now. dipolar_spin_flip_rate = (60.106 + 13.812 * np.log(nominal_field) - 4.7867 * np.log(nominal_field)**2 - 2.3192 * np.log(nominal_field)**3 \ @@ -137,7 +140,8 @@ def calculate_dipolar_loss(nominal_field, cavity_radius, design_density, volume, time_constant_dipolar = calculate_inventory(design_density, volume) / current_dipolar return time_constant_dipolar, current_dipolar -# Evaporation loss rate: does not take into account density of states with height. Magnetic potential limits evaporation. +# Evaporation loss rate: Does not take into account density of states with height. Magnetic potential limits evaporation. +# Cone and cylinder now separate because cone is weaker owing to azimuthal modulation of Ioffe field. Each now has its own density multiplier. The weaker cone field is handled in a separate Igor calculation Coneangle.pxp outside this SS and entered as a loss rate multiplier in C111. def calculate_evaporation_loss(pure_magnetic_flag, trapped_gas_temp, cavity_radius, ioffe_field, nominal_field, cavity_length, top_cone, top_plate_cavity, design_density, relative_loss_rate_cone_wall, volume): # C283 - [m] Mean free path at the base of cavity mfp_cavity_base = 1 / (gravity_temperature_scale * surface_density_scale * tritium_tritium_crosssection_atomic) @@ -237,7 +241,7 @@ def calculate_cryopump_speed(cavity_wall_temp, cavity_radius, cavity_length, pum # Injection Line Calculations: - +#Added details of injection (beginning cell 327): vertical flow speed in trap and in the beamline, energy in trapped gas, in beam gas (should be the same), density in the injection line, mfp in injection line, dipolar loss in injection line. There are 2 new entries for this in the choices – beam temperature and beamline field. # Wouters functinos @@ -489,7 +493,7 @@ def CalcDefaults(self, overwrite=False): self.Experiment.trap_length = self.trap_coil_2 - self.trap_coil_1 logger.info("Calc'd trap length from coils: {} m".format(round(self.Experiment.trap_length/m, 3), 2)) - # C128 - Calculate Cavity L/D Ratio + # C128 - Calculate Cavity L/D Ratio with actual dimensions: top of cone, first trap coil, second trap coil, top plate of cavity, top of vacuum containment. if ((hasattr(self.Experiment, 'cavity_L_over_D')) and self.Experiment.cavity_L_over_D_calc_flag): self.Experiment.cavity_L_over_D = self.cavity_length / (self.cavity_radius * 2) @@ -623,7 +627,7 @@ def trap_geometry(self): # C093 - [m] height of the top of the vacuum system self.top_vacuum_system = 7.50*m return self.top_cone, self.trap_coil_1, self.trap_coil_2, self.top_plate_cavity, self.top_vacuum_system - # C126 - [m] Cavity Length + # C126 - [m] Cavity Length from true L and f; estimated as L-z1/2 def CavityLength(self): self.cavity_length = (self.top_plate_cavity - self.top_cone/2) return self.cavity_length From f0dc49cc49102fb368f5f74e0bf3277ce821f410 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Mon, 23 Feb 2026 18:55:44 -0500 Subject: [PATCH 41/57] Added Cryopump and Injection line calculations and parameters --- mermithid/misc/Constants_numericalunits.py | 13 +++- .../sensitivity/SensitivityCavityFormulas.py | 74 ++++++++++++++----- 2 files changed, 66 insertions(+), 21 deletions(-) diff --git a/mermithid/misc/Constants_numericalunits.py b/mermithid/misc/Constants_numericalunits.py index 0aec57d6..bc2c308d 100644 --- a/mermithid/misc/Constants_numericalunits.py +++ b/mermithid/misc/Constants_numericalunits.py @@ -47,10 +47,14 @@ kg_amu = 1.66e-27*kg/amu # C021 - Boltzmann Constant [eV/K] = kB * (1 eV / 1.61e-19 J) kB_eV = 8.617e-5*eV/K +# C026 - [J/T] Bohr Magneton +bohr_magneton = 9.27e-24*J/T # C027 - [eV/T] Bohr Magneton -bohr_magneton = 5.776e-5*eV/T -# C028 - 1 Ci = 3.7e10 Bq -Ci_Bq = 3.7e10 +#bohr_magneton = 5.776e-5*eV/T +# C028 - Curie Becquere Conversion: 1 Ci = 3.7e10 Bq. NOTE CURIE IS NOT A UNIT IN NUMERICAL UNITS SO DO THIS EXPLICTLY +Ci_Bq = 3.7e10/s +# C030 - [s^-1] Atomic tritium decay constant +lambda_tritium = 1 / tritium_livetime # C031 - Ground state branch (atomic) ground_state_branch_atomic = 0.702 # C032 - Ground state branch (molecular) @@ -75,6 +79,9 @@ molecules_desorbed_wall_beta = 1000 # C050 - eV to J conversion eV_J = 1.605e-19 *J/eV +# C118 - [Ci] Total activity on wall at recyling. Ci (Bq = 1 decay per second) +wall_activity = 100 * Ci_Bq + # units that do not show up in numericalunits # missing pre-factors diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index 3c5313b5..f2c709a8 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -61,13 +61,13 @@ def calculate_activity_in_trap(number_density, cavity_radius): return atoms_between_trap_coils / tritium_livetime # Background T2 in atomic trap: -def calculate_T2_background_atomic_trap(cavity_radius, cavity_length, cavity_wall_temp, max_ratio_T2_T, number_density, wall_activity): +def calculate_T2_background_atomic_trap(cavity_radius, cavity_length, cavity_wall_temp, max_ratio_T2_T, number_density): # C306 - [mbar] Vapor Pressure of T2 w/ constants for saturated T2 vapor from Souers et al. T2_vapor_pressure = mbar * np.exp(T2_vapor_A + (T2_vapor_B*K/cavity_wall_temp) + T2_vapor_B_prime * np.log(cavity_wall_temp/K)) / 0.76 # C307 - [m^-3] Density of saturated vapor sat_vapor_density = NA * (T2_vapor_pressure/mbar) * np.absolute(T0) / (1000 * molar_volume * cavity_wall_temp) # C308 - [m^-3] = s^-1 * sqrt(kg/eV) / m^2 T2 density form desorption at end of a cycle - T2_density_desorp = 4 * molecules_desorbed_wall_beta * Ci_Bq * wall_activity * np.sqrt(molecules_desorbed_wall_beta * (tritium_mass_atomic / c0**2) / (2 * atomic_tritium_recoil_energy * eV_J )) / calculate_trap_wall_area(cavity_radius, cavity_length) + T2_density_desorp = 4 * molecules_desorbed_wall_beta * wall_activity * np.sqrt(molecules_desorbed_wall_beta * (tritium_mass_atomic / c0**2) / (2 * atomic_tritium_recoil_energy * eV_J )) / calculate_trap_wall_area(cavity_radius, cavity_length) # C309 [m^-3] Total T2 density T2_total_density = sat_vapor_density + T2_density_desorp # C310 - T2/T number ratio. Activity ratio is 1.64 times bigger @@ -102,23 +102,25 @@ def calculate_rad_heat_leak(cavity_radius, number_density, design_density, volum # T2 Desorption from the wall: T2 desorption from walls as a background and a trap-heating loss. # Burst of molecules is emitted from the wall with each decay and can knock out atoms from the trap. Description added to end of CDR 4.5.5. New parameter to enter is the number of molecules (choose 1000 for now). -def calculate_T2_desorption_from_wall(cavity_radius, cavity_length, wall_activity, design_density, volume): +def calculate_T2_desorption_from_wall(cavity_radius, cavity_length, design_density, volume): # C261 - [s] Mean lifetime of atom in trap from desorption - time_constant_desorp = calculate_trap_wall_area(cavity_radius, cavity_length) / (2 * molecules_desorbed_wall_beta * wall_activity * Ci_Bq * H_H2_crosssection) + time_constant_desorp = calculate_trap_wall_area(cavity_radius, cavity_length) / (2 * molecules_desorbed_wall_beta * wall_activity * H_H2_crosssection) # C262 - [atom/s] Atom current required to keep up with desorption losses current_desorp = calculate_inventory(design_density, volume) / time_constant_desorp return time_constant_desorp, current_desorp # He Heat Leak: Notable difference comes from pumping speed, physical volume, and design density -def calculate_He_heat_leak(pumping_speed_theoretical, pumping_cavity_termination, turbopump_speed, cavity_temperature, design_density, volume, wall_activity): +def calculate_He_heat_leak(pumping_speed_theoretical, pumping_cavity_termination, turbopump_speed, cavity_temperature, design_density, volume): He_velocity = calculate_average_velocity(cavity_temperature) # C266 - [atom/s] Not including flow for He heat leak itself - He_production_rate = wall_activity * Ci_Bq + He_production_rate = wall_activity # C269 - [m^-3] Number density of Helium He_density = He_production_rate * ((1/pumping_speed_theoretical) + (1/pumping_cavity_termination) + (1/turbopump_speed)) # C272 - [s] Time constant for loss due to He-3 heat leak time_constant_He = 1 / (He_density * He_velocity * H_He_crosssection) - # C273 - [s^-1] Atom current required to keep up with He-3 heat leak + # C273 - Fraction of total gas that is He + He_fraction = He_density / design_density + # C274 - [s^-1] Atom current required to keep up with He-3 heat leak current_He_leak = calculate_inventory(design_density, volume) / time_constant_He return time_constant_He, current_He_leak @@ -146,7 +148,7 @@ def calculate_evaporation_loss(pure_magnetic_flag, trapped_gas_temp, cavity_radi # C283 - [m] Mean free path at the base of cavity mfp_cavity_base = 1 / (gravity_temperature_scale * surface_density_scale * tritium_tritium_crosssection_atomic) # C284 - Pure Magnetic eta - eta = bohr_magneton * (np.sqrt(ioffe_field**2 + nominal_field**2) - nominal_field) / (trapped_gas_temp * 0.025*eV / np.absolute(T0)) + eta = (bohr_magneton / eV_J) * (np.sqrt(ioffe_field**2 + nominal_field**2) - nominal_field) / (trapped_gas_temp * 0.025*eV / np.absolute(T0)) # C285 - Magnetogravitational eta eta_grav = (tritium_mass_atomic/c0**2) * kg_amu * gravity * cavity_length / (kB * trapped_gas_temp) # C286 - [s^-1] Evaporation out the top; set to 1/1000 for pure magnetic trap @@ -230,18 +232,46 @@ def ratio_turbopump_speed(pumping_speed_required, pumping_speed_limit, cavity_te return pumping_speed_required * ((1 / pumping_speed_limit) + (1 / cavity_termination_speed) + (1 / turbopump_speed)) # Cryopumping Calculations: -def calculate_cryopump_speed(cavity_wall_temp, cavity_radius, cavity_length, pumping_speed_required): +def calculate_cryopump_speed(cavity_wall_temp, cavity_radius, cavity_length, pumping_speed_required, atom_current, design_density, max_ratio_T2_T): # C335 - [m/s] Cryopumping speed per area for mass-3 atoms at wall temperature stated above. sqrt(k T/(2 pi M)); see https://www.synsysco.com/wp-content/uploads/2016/02/Basics-of-Cryopumping-Booklet.pdf. Using M=3 because most gas is spin-flipped atoms. cryopump_speed_per_area = np.sqrt(kB_eV * cavity_wall_temp / (2 * np.pi * tritium_mass_atomic)) * c0 # C336 - [m^3/s] With given cylindrical dimensions at wall temperature stated above for mass-3 atoms trap_interior_surface_speed = cryopump_speed_per_area * calculate_trap_wall_area(cavity_radius, cavity_length) + # C338 - Ratio of required molecular speed to cryopumping speed. Ideal cryopumping is the impingement rate (all sticks). ratio_cryopump_speed = pumping_speed_required / trap_interior_surface_speed - return ratio_cryopump_speed - - + # C339 - [W] Heat delivered to the cold surface + recombination_heat_load = atom_current * molecular_tritium_binding_energy * eV_J / 2 + # C340 - [Ci/day] Accumulated activity per day + accumulated_activity_day = atom_current * lambda_tritium * 3600 * 24 / Ci_Bq + # C342 - [day] Time between recycling cryosurface. For cryopumping atomic experiment; determines inventory on walls + time_recycling_crysurface = wall_activity / accumulated_activity_day + # C344 - [m^3/s] Cryopumping speed for molecules + cryopump_speed = np.sqrt(kB_eV * cavity_wall_temp / (4 * np.pi * tritium_mass_atomic)) * c0 * calculate_trap_wall_area(cavity_radius, cavity_length) + # C345 - [molecules/s] max allowed injection rate of molecules + max_molecules_injection = cryopump_speed * molecular_density_allowed(design_density, max_ratio_T2_T) + return ratio_cryopump_speed, cryopump_speed # Injection Line Calculations: -#Added details of injection (beginning cell 327): vertical flow speed in trap and in the beamline, energy in trapped gas, in beam gas (should be the same), density in the injection line, mfp in injection line, dipolar loss in injection line. There are 2 new entries for this in the choices – beam temperature and beamline field. +# density in the injection line, mfp in injection line, dipolar loss in injection line. There are 2 new entries for this in the choices – beam temperature and beamline field. +def calculate_injection_line(atom_current, cavity_radius, design_density, trapped_gas_temp, nominal_field, injection_gas_temp, injection_field): + # C349 - [m/s] Vertical flow speed in trap at bottom and in the beamline + vertical_injection_speed = atom_current / (np.pi * cavity_radius**2 * design_density) + # C350 - [J/atom] Energy in trapped gas and in beam gas + trapped_gas_energy = (1.5 * kB * trapped_gas_temp) + (0.5 * (tritium_mass_atomic/c0**2) * vertical_injection_speed**2) + (bohr_magneton * nominal_field) + # C351 - [m^-3] Density in the injection line + injection_density = atom_current * np.sqrt((tritium_mass_atomic/c0**2) / (2 * trapped_gas_energy - 3 * kB * injection_gas_temp - 2 * bohr_magneton * injection_field)) / area_loading_aperture + # C352 - [m] Mean free path in beamline + mfp_beamline = 1 / (injection_density * tritium_tritium_crosssection_atomic) + # C353 - [m/s] Vertical flow speed through aperture + vertical_aperture_speed = design_density * np.pi * cavity_radius**2 * vertical_injection_speed / (area_loading_aperture * injection_density) + # C354 - [J/atom] Energy in injected gas + injection_gas_energy = (1.5 * kB * injection_gas_temp) + (0.5 * (tritium_mass_atomic/c0**2) * vertical_aperture_speed**2) + (bohr_magneton * injection_field) + # C355 - [m^3/s] 5th degree polynomial fit for injection line dipolar loss rate + injection_dipolar_loss_rate = (60.106 + 13.812 * np.log(injection_field/T) - 4.7867 * np.log(injection_field/T)**2 - 2.3192 * np.log(injection_field/T)**3 \ + - 0.32663 * np.log(injection_field/T)**4 - 0.015775 * np.log(injection_field/T)**5) * 1e-22 * LGd_rates * m**3/s + # C356 - [m^-1] Beamline dipolar loss + beamline_dipolar = injection_dipolar_loss_rate * injection_density / vertical_aperture_speed + return vertical_injection_speed, trapped_gas_energy, injection_density, vertical_aperture_speed, injection_gas_energy # Wouters functinos @@ -1140,7 +1170,7 @@ def print_Efficiencies(self): def print_T2_background_atomic_trap(self): #logger.info("T2 background: {}".format(self.Efficiency.T2_background_atomic_trap)) if self.Efficiency.T2_background_atomic_trap: - self.T2_total_density, self.T2_T_ratio = calculate_T2_background_atomic_trap(self.cavity_radius, self.cavity_length, self.FrequencyExtraction.cavity_temperature, self.Efficiency.max_ratio_T2_T, self.Experiment.number_density, self.Efficiency.wall_activity) + self.T2_total_density, self.T2_T_ratio = calculate_T2_background_atomic_trap(self.cavity_radius, self.cavity_length, self.FrequencyExtraction.cavity_temperature, self.Efficiency.max_ratio_T2_T, self.Experiment.number_density) logger.info("T2_total_density: {:.4e} m^-3".format(round(self.T2_total_density*m**3), 4)) logger.info("Ratio T2/T: {:.4e}".format(self.T2_T_ratio)) @@ -1158,7 +1188,7 @@ def print_pumping_requirements(self): logger.info("Pumping Speed Limit (Molecular): {:.4} m^3/s".format(self.turbopump_speed_limit_molecular * s / m**3)) logger.info("Cavity Termination Speed (Molecular): {:.4} m^3/s".format(self.cavity_termination_speed_molecular * s / m**3)) if self.Efficiency.T_atom_supply_trap: - self.time_constant_He, self.current_He_leak = calculate_He_heat_leak(self.turbopump_speed_limit_atomic, self.cavity_termination_speed_atomic, self.turbopump_speed, self.FrequencyExtraction.cavity_temperature, self.Experiment.design_density, self.total_cavity_volume, self.Efficiency.wall_activity) + self.time_constant_He, self.current_He_leak = calculate_He_heat_leak(self.turbopump_speed_limit_atomic, self.cavity_termination_speed_atomic, self.turbopump_speed, self.FrequencyExtraction.cavity_temperature, self.Experiment.design_density, self.total_cavity_volume) logger.info("He Time Constant: {:.4} s".format(self.time_constant_He/s)) logger.info("Atom current required for He-3 leak: {:.4e} atoms/s".format(self.current_He_leak*s)) self.time_constant_aperture, self.current_aperture_leak = calculate_aperture_heat_leak(self.DopplerBroadening.gas_temperature, self.Experiment.design_density, self.total_cavity_volume) @@ -1167,7 +1197,7 @@ def print_pumping_requirements(self): self.time_constant_rad, self.current_rad_leak = calculate_rad_heat_leak(self.cavity_radius, self.Experiment.number_density, self.Experiment.design_density, self.total_cavity_volume, self.Efficiency.net_rad_efficiency) logger.info("Radiation Time Constant: {:.4} s".format(self.time_constant_rad/s)) logger.info("Atom current required for radiation leak: {:.4e} atoms/s".format(self.current_rad_leak*s)) - self.time_constant_desorp, self.current_desorp = calculate_T2_desorption_from_wall(self.cavity_radius, self.cavity_length, self.Efficiency.wall_activity, self.Experiment.design_density, self.total_cavity_volume) + self.time_constant_desorp, self.current_desorp = calculate_T2_desorption_from_wall(self.cavity_radius, self.cavity_length, self.Experiment.design_density, self.total_cavity_volume) logger.info("Desorption Time Constant: {:.4} s".format(self.time_constant_desorp/s)) logger.info("Atom current required for desorption losses: {:.4e} atoms/s".format(self.current_desorp*s)) if not self.Experiment.pure_magnetic_flag: @@ -1195,9 +1225,17 @@ def print_pumping_requirements(self): self.ratio_turbopump_speed = ratio_turbopump_speed(self.turbopump_speed_required, self.turbopump_speed_limit_molecular, self.cavity_termination_speed_molecular, self.turbopump_speed) logger.info("Ratio of Turbopump Speed Limit: {:.4e}".format(self.ratio_turbopump_speed)) if (self.ratio_turbopump_speed >= 1.0): logger.info("CRITICAL PUMPING REACHED! Experiment cannot procede with turbopumps.") - self.ratio_cryopump_speed = calculate_cryopump_speed(self.FrequencyExtraction.cavity_temperature, self.cavity_radius, self.cavity_length, self.turbopump_speed_required) + self.ratio_cryopump_speed, self.cryopump_speed = calculate_cryopump_speed(self.FrequencyExtraction.cavity_temperature, self.cavity_radius, self.cavity_length, self.turbopump_speed_required, self.total_atom_current, self.Experiment.design_density, self.Efficiency.max_ratio_T2_T) logger.info("Ratio of Cryopump Speed: {:.4}".format(self.ratio_cryopump_speed)) - if (self.ratio_cryopump_speed >= 1.0): logger.info("CRITICAL PUMPING REACHED! The molecular pumping speed required is a dimensional calculation, not really a speed. As a result, the cryopumping speed limit is not real. Ideal cryopumping is the impingement rate: it all sticks. The actual molecular density is determined by the vapor pressure.") + if (self.ratio_cryopump_speed >= 1.0): logger.info("CRITICAL PUMPING REACHED! The molecular pumping speed required is a dimensional calculation, not really a speed. As a result, the cryopumping speed limit is not real. Ideal cryopumping is the impingement rate: it all sticks. The actual molecular density is determined by the vapor pressure.") + logger.info("Cryopump speed of molecules: {:.4} m^3/s".format(self.cryopump_speed * s/m**3)) + self.vertical_injection_speed, self.trapped_gas_energy, self.injection_density, self.vertical_aperture_speed, self.injection_gas_energy = calculate_injection_line(self.total_atom_current, self.cavity_radius, self.Experiment.design_density, self.DopplerBroadening.gas_temperature, self.MagneticField.nominal_field, self.DopplerBroadening.injection_temperature, self.MagneticField.injection_field) + logger.info("Injection_density: {:.4e} m^-3".format(self.injection_density*m**3)) + if (self.injection_density*m**3 > 1e20): logger.info("Too high! Lower temperature and/or magnetic field in the injection beamline.") + logger.info("Injection speed: {:.4e} m/s".format(self.vertical_injection_speed * s/m)) + logger.info("Trapped gas energy: {:.4e} J".format(self.trapped_gas_energy/J)) + logger.info("Aperture speed: {:.4e} m/s".format(self.vertical_aperture_speed * s/m)) + logger.info("Injection gas energy: {:.4e} J".format(self.injection_gas_energy/J)) """ # Cramer-Rao lower bound / how much worse are we than the lower bound From 363eaf9460c4e4c3cdf94bf0fc8ae28cf505ef37 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Mon, 23 Feb 2026 22:57:26 -0500 Subject: [PATCH 42/57] Fixed molar volume bug --- mermithid/misc/Constants_numericalunits.py | 2 +- mermithid/sensitivity/SensitivityCavityFormulas.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mermithid/misc/Constants_numericalunits.py b/mermithid/misc/Constants_numericalunits.py index bc2c308d..f1ba29d0 100644 --- a/mermithid/misc/Constants_numericalunits.py +++ b/mermithid/misc/Constants_numericalunits.py @@ -34,7 +34,7 @@ # Atomic Calculator # C008 - [m^3] Volume of 1mol of ideal gas at 1atm. Higher pressure --> smaller volume; higher temp --> lower volume -molar_volume = 2.24*e-2*m**3 +molar_volume = 0.0224*m**3 # C009 - [m^2] H-He Cross-section at low temp but > 5K (Berlinsky) H_He_crosssection = 2e-19*m**2 # C010 - [m^2] H-He Cross-section at low temps (Berlinsky) diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index f2c709a8..95019a42 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -65,14 +65,14 @@ def calculate_T2_background_atomic_trap(cavity_radius, cavity_length, cavity_wal # C306 - [mbar] Vapor Pressure of T2 w/ constants for saturated T2 vapor from Souers et al. T2_vapor_pressure = mbar * np.exp(T2_vapor_A + (T2_vapor_B*K/cavity_wall_temp) + T2_vapor_B_prime * np.log(cavity_wall_temp/K)) / 0.76 # C307 - [m^-3] Density of saturated vapor - sat_vapor_density = NA * (T2_vapor_pressure/mbar) * np.absolute(T0) / (1000 * molar_volume * cavity_wall_temp) + sat_vapor_density = NA * (T2_vapor_pressure/mbar) * np.absolute(T0/cavity_wall_temp) / (1000 * molar_volume) # C308 - [m^-3] = s^-1 * sqrt(kg/eV) / m^2 T2 density form desorption at end of a cycle T2_density_desorp = 4 * molecules_desorbed_wall_beta * wall_activity * np.sqrt(molecules_desorbed_wall_beta * (tritium_mass_atomic / c0**2) / (2 * atomic_tritium_recoil_energy * eV_J )) / calculate_trap_wall_area(cavity_radius, cavity_length) # C309 [m^-3] Total T2 density T2_total_density = sat_vapor_density + T2_density_desorp # C310 - T2/T number ratio. Activity ratio is 1.64 times bigger - # Number density b/w trap coils varies between Mermithid and Atomic Calculator due to inelastic + elastic T2-e cross-section T2_T_ratio = T2_total_density / number_density + #return T2_vapor_pressure, sat_vapor_density, T2_density_desorp, T2_total_density, T2_T_ratio return T2_total_density, T2_T_ratio # Aperture Heat Leak: @@ -517,7 +517,7 @@ def CalcDefaults(self, overwrite=False): #Get trap length from cavity length if not specified if ((not hasattr(self.Experiment, 'trap_length')) or overwrite): self.Experiment.trap_length = 0.8 * 2 * self.cavity_radius * self.Experiment.cavity_L_over_D - logger.info("Calc'd trap length: {} m".format(round(self.Experiment.trap_length/m, 3), 2)) + logger.info("Calc'd trap length: {:.3} m".format(self.Experiment.trap_length/m, 3)) elif self.Experiment.trap_length_calc_flag: # C129 - Trap L/D = Trap L / 2 / cavity radius self.Experiment.trap_length = self.trap_coil_2 - self.trap_coil_1 @@ -1171,7 +1171,7 @@ def print_T2_background_atomic_trap(self): #logger.info("T2 background: {}".format(self.Efficiency.T2_background_atomic_trap)) if self.Efficiency.T2_background_atomic_trap: self.T2_total_density, self.T2_T_ratio = calculate_T2_background_atomic_trap(self.cavity_radius, self.cavity_length, self.FrequencyExtraction.cavity_temperature, self.Efficiency.max_ratio_T2_T, self.Experiment.number_density) - logger.info("T2_total_density: {:.4e} m^-3".format(round(self.T2_total_density*m**3), 4)) + logger.info("T2_total_density: {:.4e} m^-3".format(self.T2_total_density*m**3)) logger.info("Ratio T2/T: {:.4e}".format(self.T2_T_ratio)) def print_pumping_requirements(self): From 2c6ea1b5181be7b3c2773d53007ad178a402c960 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Tue, 24 Feb 2026 18:02:01 -0500 Subject: [PATCH 43/57] Added more experimental parameters and activity calculations --- mermithid/misc/Constants_numericalunits.py | 6 +- .../sensitivity/SensitivityCavityFormulas.py | 70 +++++++++++-------- 2 files changed, 44 insertions(+), 32 deletions(-) diff --git a/mermithid/misc/Constants_numericalunits.py b/mermithid/misc/Constants_numericalunits.py index f1ba29d0..1ec5fd87 100644 --- a/mermithid/misc/Constants_numericalunits.py +++ b/mermithid/misc/Constants_numericalunits.py @@ -77,10 +77,12 @@ LGd_rates = 50 # C049 - per beta decay: see: https://www.overleaf.com/2817746228snnghrnzfthk molecules_desorbed_wall_beta = 1000 -# C050 - eV to J conversion +# C050 - [eV] to [J] conversion eV_J = 1.605e-19 *J/eV +# C108 - [m^2] Choose an area to determine injection speed and density +area_atom_loading_aperture = 0.001 * m**2 # C118 - [Ci] Total activity on wall at recyling. Ci (Bq = 1 decay per second) -wall_activity = 100 * Ci_Bq +wall_activity = 100 # units that do not show up in numericalunits diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index 95019a42..2b3c3332 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -27,7 +27,6 @@ # Jins functions - Atomic Calculator # Python decides whether a name is local or global at compile time (not runtime). The name must already exist in the module’s global namespace by the time the function runs, or Python has nowhere to bind it. -area_loading_aperture, gravity_temperature_scale, surface_density_scale = (None, None, None) # C157 - [m] Fiducial distance from the cavity wall between max(ioffe_bite, larmor_radius) # Ioffe field at wall is now the quadrature of the central field and Ioffe field by itself, everywhere it is used. @@ -46,6 +45,16 @@ def calculate_trap_wall_area(cavity_radius, cavity_length): trap_wall_area = 2 * np.pi * cavity_radius * cavity_length return trap_wall_area +# C201 - [m^-1] Add the gravity-temperature scale parameter “b” for magnetogravitational trap. For pure magnetic trap, make gravity weaker by ratio of L/D. +def calculate_gravity_temperature_scale(trapped_gas_temp, pure_magnetic_flag, cavity_L_over_D): + gravity_temperature_scale = ((tritium_mass_atomic/c0**2 * gravity / (kB * trapped_gas_temp)) * np.absolute(pure_magnetic_flag * (1 + 1 / cavity_L_over_D) - 1)) + return gravity_temperature_scale + +# C202 - [m^-2] Add the surface density scale parameter “a/b” for magnetogravitational trap +def calculate_surface_density_scale(design_density, gravity_temperature_scale, top_plate_cavity): + surface_density_scale = design_density * top_plate_cavity / (1 - np.exp(-gravity_temperature_scale * top_plate_cavity)) + return surface_density_scale + # C204 - [atom] Inventory in physical volume def calculate_inventory(design_density, volume): return design_density * volume @@ -67,7 +76,7 @@ def calculate_T2_background_atomic_trap(cavity_radius, cavity_length, cavity_wal # C307 - [m^-3] Density of saturated vapor sat_vapor_density = NA * (T2_vapor_pressure/mbar) * np.absolute(T0/cavity_wall_temp) / (1000 * molar_volume) # C308 - [m^-3] = s^-1 * sqrt(kg/eV) / m^2 T2 density form desorption at end of a cycle - T2_density_desorp = 4 * molecules_desorbed_wall_beta * wall_activity * np.sqrt(molecules_desorbed_wall_beta * (tritium_mass_atomic / c0**2) / (2 * atomic_tritium_recoil_energy * eV_J )) / calculate_trap_wall_area(cavity_radius, cavity_length) + T2_density_desorp = 4 * molecules_desorbed_wall_beta * (wall_activity * Ci_Bq) * np.sqrt(molecules_desorbed_wall_beta * (tritium_mass_atomic / c0**2) / (2 * atomic_tritium_recoil_energy * eV_J )) / calculate_trap_wall_area(cavity_radius, cavity_length) # C309 [m^-3] Total T2 density T2_total_density = sat_vapor_density + T2_density_desorp # C310 - T2/T number ratio. Activity ratio is 1.64 times bigger @@ -77,13 +86,10 @@ def calculate_T2_background_atomic_trap(cavity_radius, cavity_length, cavity_wal # Aperture Heat Leak: def calculate_aperture_heat_leak(trapped_gas_temp, design_density, volume): - global area_loading_aperture - # C108 - [m^2] Choose an area to determine injection speed and density. - area_loading_aperture = 0.001*m**2 # C250 - [m/s] Average velocity of trapped gas trapped_gas_velocity = calculate_average_velocity(trapped_gas_temp) # C251 - [atom/s] Inward atom current required through aperture to balance losses (j_aperture) - current_aperture_leak = area_loading_aperture * design_density * trapped_gas_velocity / 4 + current_aperture_leak = area_atom_loading_aperture * design_density * trapped_gas_velocity / 4 # C252 - [s] Time constant for loss through aperture time_constant_aperture = calculate_inventory(design_density, volume) / current_aperture_leak return time_constant_aperture, current_aperture_leak @@ -104,7 +110,7 @@ def calculate_rad_heat_leak(cavity_radius, number_density, design_density, volum # Burst of molecules is emitted from the wall with each decay and can knock out atoms from the trap. Description added to end of CDR 4.5.5. New parameter to enter is the number of molecules (choose 1000 for now). def calculate_T2_desorption_from_wall(cavity_radius, cavity_length, design_density, volume): # C261 - [s] Mean lifetime of atom in trap from desorption - time_constant_desorp = calculate_trap_wall_area(cavity_radius, cavity_length) / (2 * molecules_desorbed_wall_beta * wall_activity * H_H2_crosssection) + time_constant_desorp = calculate_trap_wall_area(cavity_radius, cavity_length) / (2 * molecules_desorbed_wall_beta * (wall_activity * Ci_Bq) * H_H2_crosssection) # C262 - [atom/s] Atom current required to keep up with desorption losses current_desorp = calculate_inventory(design_density, volume) / time_constant_desorp return time_constant_desorp, current_desorp @@ -113,7 +119,7 @@ def calculate_T2_desorption_from_wall(cavity_radius, cavity_length, design_densi def calculate_He_heat_leak(pumping_speed_theoretical, pumping_cavity_termination, turbopump_speed, cavity_temperature, design_density, volume): He_velocity = calculate_average_velocity(cavity_temperature) # C266 - [atom/s] Not including flow for He heat leak itself - He_production_rate = wall_activity + He_production_rate = wall_activity * Ci_Bq # C269 - [m^-3] Number density of Helium He_density = He_production_rate * ((1/pumping_speed_theoretical) + (1/pumping_cavity_termination) + (1/turbopump_speed)) # C272 - [s] Time constant for loss due to He-3 heat leak @@ -126,11 +132,8 @@ def calculate_He_heat_leak(pumping_speed_theoretical, pumping_cavity_termination # Dipolar loss rate: Calculation with z-dependent density, cylinder & cone. def calculate_dipolar_loss(nominal_field, cavity_radius, design_density, volume, trapped_gas_temp, cavity_L_over_D, top_cone, top_plate_cavity, pure_magnetic_flag): - global gravity_temperature_scale, surface_density_scale - # C201 - [m^-1] Add the gravity-temperature scale parameter “b” for magnetogravitational trap - gravity_temperature_scale = ((tritium_mass_atomic/c0**2 * gravity / (kB * trapped_gas_temp)) * np.absolute(pure_magnetic_flag * (1 + 1 / cavity_L_over_D) - 1)) - # C202 - [m^-2] Add the surface density scale parameter “a/b” for magnetogravitational trap - surface_density_scale = design_density * top_plate_cavity / (1 - np.exp(-gravity_temperature_scale * top_plate_cavity)) + gravity_temperature_scale = calculate_gravity_temperature_scale(trapped_gas_temp, pure_magnetic_flag, cavity_L_over_D) + surface_density_scale = calculate_surface_density_scale(design_density, gravity_temperature_scale, top_plate_cavity) # C298 - [m^3/s] Polynomial fit for Dipolar spin-flip rate (G_dd). Depends on field (Lagendijk et al). Polynomial-log fit used now. dipolar_spin_flip_rate = (60.106 + 13.812 * np.log(nominal_field) - 4.7867 * np.log(nominal_field)**2 - 2.3192 * np.log(nominal_field)**3 \ - 0.32663 * np.log(nominal_field)**4 - 0.015775 * np.log(nominal_field)**5) * 1e-22 * LGd_rates * m**3/s @@ -144,7 +147,10 @@ def calculate_dipolar_loss(nominal_field, cavity_radius, design_density, volume, # Evaporation loss rate: Does not take into account density of states with height. Magnetic potential limits evaporation. # Cone and cylinder now separate because cone is weaker owing to azimuthal modulation of Ioffe field. Each now has its own density multiplier. The weaker cone field is handled in a separate Igor calculation Coneangle.pxp outside this SS and entered as a loss rate multiplier in C111. -def calculate_evaporation_loss(pure_magnetic_flag, trapped_gas_temp, cavity_radius, ioffe_field, nominal_field, cavity_length, top_cone, top_plate_cavity, design_density, relative_loss_rate_cone_wall, volume): +def calculate_evaporation_loss(cavity_L_over_D, pure_magnetic_flag, trapped_gas_temp, cavity_radius, ioffe_field, nominal_field, cavity_length, top_cone, top_plate_cavity, design_density, relative_loss_rate_cone_wall, volume): + gravity_temperature_scale = calculate_gravity_temperature_scale(trapped_gas_temp, pure_magnetic_flag, cavity_L_over_D) + surface_density_scale = calculate_surface_density_scale(design_density, gravity_temperature_scale, top_plate_cavity) + # C283 - [m] Mean free path at the base of cavity mfp_cavity_base = 1 / (gravity_temperature_scale * surface_density_scale * tritium_tritium_crosssection_atomic) # C284 - Pure Magnetic eta @@ -161,21 +167,17 @@ def calculate_evaporation_loss(pure_magnetic_flag, trapped_gas_temp, cavity_radi evaporation_sides = (2 * np.pi * cavity_radius * cavity_length * design_density * np.sqrt(kB * trapped_gas_temp / (2 * np.pi * tritium_mass_atomic / c0**2)) \ * np.exp(-eta) * top_plate_cavity * (np.exp(-gravity_temperature_scale * top_cone) - np.exp(-gravity_temperature_scale * top_plate_cavity)) \ / ((1 - np.exp(-gravity_temperature_scale * top_plate_cavity)) * (top_plate_cavity - top_cone))) - # C289 [Bq - Decay/s] - Accumulated activity in the sides by end of cycle - #accumulated_activity_sides = evaporation_sides * # C290 - [s^-1] Evaporation in cone; for pure magnetic, double it to account for both ends evaporation_cone = (relative_loss_rate_cone_wall * np.pi * cavity_radius * np.sqrt(cavity_radius**2 + top_cone**2) * design_density \ * np.sqrt(kB * trapped_gas_temp / (2 * np.pi * tritium_mass_atomic/c0**2)) * np.exp(-eta) * top_plate_cavity * (1 - np.exp(-gravity_temperature_scale * top_cone)) \ / ((1 - np.exp(-gravity_temperature_scale * top_plate_cavity)) * top_cone)) * np.absolute(pure_magnetic_flag + 1) - # C291 [Bq - Decay/s] - Accumulated activity in the cone by end of cycle - #accumulated_activity_cone = evaporation_cone * # C292 - [s] Time constant for evaporation loss of cone and sides time_constant_evap_cone_sides = calculate_inventory(design_density, volume) / (evaporation_cone + evaporation_sides) # C295 - [atom/s] Flow to keep up with evaporation losses aka Total evaporation_sum current_evaporation = evaporation_top + evaporation_sides + evaporation_cone # C294 - [s] Time constant for total evaporation loss time_constant_total_evaporation = calculate_inventory(design_density, volume) / current_evaporation - return time_constant_total_evaporation, current_evaporation + return time_constant_total_evaporation, current_evaporation, evaporation_top, evaporation_sides, evaporation_cone # T2 Heat Leak: def calculate_T2_heat_leak(aperture_current, rad_current, He_current, evap_current, dipolar_current, T2_total_density, cavity_wall_temp, design_density, volume): @@ -242,14 +244,14 @@ def calculate_cryopump_speed(cavity_wall_temp, cavity_radius, cavity_length, pum # C339 - [W] Heat delivered to the cold surface recombination_heat_load = atom_current * molecular_tritium_binding_energy * eV_J / 2 # C340 - [Ci/day] Accumulated activity per day - accumulated_activity_day = atom_current * lambda_tritium * 3600 * 24 / Ci_Bq + accumulated_activity_day = atom_current * lambda_tritium * (86400*s/day) / Ci_Bq # C342 - [day] Time between recycling cryosurface. For cryopumping atomic experiment; determines inventory on walls - time_recycling_crysurface = wall_activity / accumulated_activity_day + time_recycling_cryosurface = wall_activity / accumulated_activity_day # C344 - [m^3/s] Cryopumping speed for molecules cryopump_speed = np.sqrt(kB_eV * cavity_wall_temp / (4 * np.pi * tritium_mass_atomic)) * c0 * calculate_trap_wall_area(cavity_radius, cavity_length) # C345 - [molecules/s] max allowed injection rate of molecules max_molecules_injection = cryopump_speed * molecular_density_allowed(design_density, max_ratio_T2_T) - return ratio_cryopump_speed, cryopump_speed + return ratio_cryopump_speed, cryopump_speed, time_recycling_cryosurface # Injection Line Calculations: # density in the injection line, mfp in injection line, dipolar loss in injection line. There are 2 new entries for this in the choices – beam temperature and beamline field. @@ -259,11 +261,11 @@ def calculate_injection_line(atom_current, cavity_radius, design_density, trappe # C350 - [J/atom] Energy in trapped gas and in beam gas trapped_gas_energy = (1.5 * kB * trapped_gas_temp) + (0.5 * (tritium_mass_atomic/c0**2) * vertical_injection_speed**2) + (bohr_magneton * nominal_field) # C351 - [m^-3] Density in the injection line - injection_density = atom_current * np.sqrt((tritium_mass_atomic/c0**2) / (2 * trapped_gas_energy - 3 * kB * injection_gas_temp - 2 * bohr_magneton * injection_field)) / area_loading_aperture + injection_density = atom_current * np.sqrt((tritium_mass_atomic/c0**2) / (2 * trapped_gas_energy - 3 * kB * injection_gas_temp - 2 * bohr_magneton * injection_field)) / area_atom_loading_aperture # C352 - [m] Mean free path in beamline mfp_beamline = 1 / (injection_density * tritium_tritium_crosssection_atomic) # C353 - [m/s] Vertical flow speed through aperture - vertical_aperture_speed = design_density * np.pi * cavity_radius**2 * vertical_injection_speed / (area_loading_aperture * injection_density) + vertical_aperture_speed = design_density * np.pi * cavity_radius**2 * vertical_injection_speed / (area_atom_loading_aperture * injection_density) # C354 - [J/atom] Energy in injected gas injection_gas_energy = (1.5 * kB * injection_gas_temp) + (0.5 * (tritium_mass_atomic/c0**2) * vertical_aperture_speed**2) + (bohr_magneton * injection_field) # C355 - [m^3/s] 5th degree polynomial fit for injection line dipolar loss rate @@ -1178,7 +1180,7 @@ def print_pumping_requirements(self): #logger.info("Pumping Calculation: {}".format(self.Efficiency.pumping_calculation)) if self.Efficiency.pumping_calculation: self.turbopump_speed = turbopump_speed(self.Efficiency.number_turbopumps, self.Efficiency.turbopump_speed_gas_T2) - logger.info("Turbopump Speed: {} m^3/s".format(self.turbopump_speed * s / m**3)) + logger.info("Turbopump Speed: {:.4} m^3/s".format(self.turbopump_speed * s / m**3)) self.turbopump_speed_limit_atomic = turbopump_speed_limit(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D, self.Experiment.atomic) self.cavity_termination_speed_atomic = cavity_termination_speed(self.Efficiency.turbopump_speed_cavity_termination_air, self.Efficiency.cavity_top_plate_temperature, self.Experiment.atomic) self.turbopump_speed_limit_molecular = turbopump_speed_limit(self.cavity_radius, self.FrequencyExtraction.cavity_temperature, self.Experiment.cavity_L_over_D, not self.Experiment.atomic) @@ -1207,7 +1209,7 @@ def print_pumping_requirements(self): self.time_constant_dipolar, self.current_dipolar = calculate_dipolar_loss(self.MagneticField.nominal_field/T, self.cavity_radius, self.Experiment.design_density, self.total_cavity_volume, self.DopplerBroadening.gas_temperature, self.Experiment.cavity_L_over_D, self.top_cone, self.top_plate_cavity, self.Experiment.pure_magnetic_flag) logger.info("Dipolar Time Constant: {:.4} s".format(self.time_constant_dipolar/s)) logger.info("Atom current required for dipolar losses: {:.4e} atoms/s".format(self.current_dipolar*s)) - self.time_constant_evaporation, self.current_evaporation = calculate_evaporation_loss(self.Experiment.pure_magnetic_flag, self.DopplerBroadening.gas_temperature, self.cavity_radius, self.MagneticField.ioffe_field, self.MagneticField.nominal_field, self.cavity_length, self.top_cone, self.top_plate_cavity, self.Experiment.design_density, self.Efficiency.relative_loss_rate_cone_wall, self.total_cavity_volume) + self.time_constant_evaporation, self.current_evaporation, self.evaporation_top, self.evaporation_sides, self.evaporation_cone = calculate_evaporation_loss(self.Experiment.cavity_L_over_D, self.Experiment.pure_magnetic_flag, self.DopplerBroadening.gas_temperature, self.cavity_radius, self.MagneticField.ioffe_field, self.MagneticField.nominal_field, self.cavity_length, self.top_cone, self.top_plate_cavity, self.Experiment.design_density, self.Efficiency.relative_loss_rate_cone_wall, self.total_cavity_volume) logger.info("Evaporation Time Constant: {:.4} s".format(self.time_constant_evaporation/s)) logger.info("Atom current required for Evaporation losses: {:.4e} atoms/s".format(self.current_evaporation*s)) self.time_constant_T2, self.current_T2_leak = calculate_T2_heat_leak(self.current_aperture_leak, self.current_rad_leak, self.current_He_leak, self.current_evaporation, self.current_dipolar, self.T2_total_density, self.FrequencyExtraction.cavity_temperature, self.Experiment.design_density, self.total_cavity_volume) @@ -1224,17 +1226,25 @@ def print_pumping_requirements(self): logger.info("Turbopump Speed required: {} m^3/s".format(self.turbopump_speed_required * s / m**3)) self.ratio_turbopump_speed = ratio_turbopump_speed(self.turbopump_speed_required, self.turbopump_speed_limit_molecular, self.cavity_termination_speed_molecular, self.turbopump_speed) logger.info("Ratio of Turbopump Speed Limit: {:.4e}".format(self.ratio_turbopump_speed)) - if (self.ratio_turbopump_speed >= 1.0): logger.info("CRITICAL PUMPING REACHED! Experiment cannot procede with turbopumps.") - self.ratio_cryopump_speed, self.cryopump_speed = calculate_cryopump_speed(self.FrequencyExtraction.cavity_temperature, self.cavity_radius, self.cavity_length, self.turbopump_speed_required, self.total_atom_current, self.Experiment.design_density, self.Efficiency.max_ratio_T2_T) + if (self.ratio_turbopump_speed >= 1.0): logger.info("CRITICAL PUMPING REACHED! Experiment cannot proceed with turbopumps.") + self.ratio_cryopump_speed, self.cryopump_speed, self.time_recycling_cryosurface = calculate_cryopump_speed(self.FrequencyExtraction.cavity_temperature, self.cavity_radius, self.cavity_length, self.turbopump_speed_required, self.total_atom_current, self.Experiment.design_density, self.Efficiency.max_ratio_T2_T) logger.info("Ratio of Cryopump Speed: {:.4}".format(self.ratio_cryopump_speed)) if (self.ratio_cryopump_speed >= 1.0): logger.info("CRITICAL PUMPING REACHED! The molecular pumping speed required is a dimensional calculation, not really a speed. As a result, the cryopumping speed limit is not real. Ideal cryopumping is the impingement rate: it all sticks. The actual molecular density is determined by the vapor pressure.") logger.info("Cryopump speed of molecules: {:.4} m^3/s".format(self.cryopump_speed * s/m**3)) + logger.info("Time Recycling Cryosurface: {:.4} days".format(self.time_recycling_cryosurface / day)) + # C289 [Bq - Decay/s] - Accumulated activity in the sides by end of cycle + self.accumulated_activity_sides = self.evaporation_sides * self.time_recycling_cryosurface * (86400*s/day) * lambda_tritium / Ci_Bq + # C291 [Bq - Decay/s] - Accumulated activity in the cone by end of cycle + self.accumulated_activity_cone = self.evaporation_cone * self.time_recycling_cryosurface * (86400*s/day) * lambda_tritium / Ci_Bq + logger.info("Accumulated activity on the sides by end of cycle: {:.4} Ci".format(self.accumulated_activity_sides)) + logger.info("Accumulated activity on the cone by end of cycle: {:.4} Ci".format(self.accumulated_activity_cone)) + self.vertical_injection_speed, self.trapped_gas_energy, self.injection_density, self.vertical_aperture_speed, self.injection_gas_energy = calculate_injection_line(self.total_atom_current, self.cavity_radius, self.Experiment.design_density, self.DopplerBroadening.gas_temperature, self.MagneticField.nominal_field, self.DopplerBroadening.injection_temperature, self.MagneticField.injection_field) logger.info("Injection_density: {:.4e} m^-3".format(self.injection_density*m**3)) if (self.injection_density*m**3 > 1e20): logger.info("Too high! Lower temperature and/or magnetic field in the injection beamline.") - logger.info("Injection speed: {:.4e} m/s".format(self.vertical_injection_speed * s/m)) + logger.info("Vertical injection speed in trap at bottom: {:.4e} m/s".format(self.vertical_injection_speed * s/m)) logger.info("Trapped gas energy: {:.4e} J".format(self.trapped_gas_energy/J)) - logger.info("Aperture speed: {:.4e} m/s".format(self.vertical_aperture_speed * s/m)) + logger.info("Vertical injection speed in aperture: {:.4e} m/s".format(self.vertical_aperture_speed * s/m)) logger.info("Injection gas energy: {:.4e} J".format(self.injection_gas_energy/J)) From 539a221f6b64055085e8ee574b1b480e20f2ed79 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Wed, 25 Feb 2026 12:27:25 -0500 Subject: [PATCH 44/57] Editing Broadening effects --- mermithid/sensitivity/SensitivityCavityFormulas.py | 4 +++- mermithid/sensitivity/SensitivityFormulas.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index 2b3c3332..20ec38d1 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -659,7 +659,7 @@ def trap_geometry(self): # C093 - [m] height of the top of the vacuum system self.top_vacuum_system = 7.50*m return self.top_cone, self.trap_coil_1, self.trap_coil_2, self.top_plate_cavity, self.top_vacuum_system - # C126 - [m] Cavity Length from true L and f; estimated as L-z1/2 + # C126 - [m] Cavity Length from true L and f (estimated as L-z1/2); effective cavity length enters first third of ioffe cone def CavityLength(self): self.cavity_length = (self.top_plate_cavity - self.top_cone/2) return self.cavity_length @@ -667,6 +667,8 @@ def CavityLength(self): def CavityVolume(self): #Calculate vacuum volume including cone and top service volume if self.Experiment.cavity_cone_flag: + # V = V_cyl + V_cone = pi * r^2 * (L + h/3), total height is height of trap coils + height of cone; + # See Robertson_H&V_2025-10-29 for explanation; nothing changes about physical volume for second cone in horizontal configuration. self.total_cavity_volume = np.pi * self.cavity_radius**2 * (self.top_plate_cavity - (2/3) * self.top_cone) * self.Experiment.n_cavities else: #radius = 0.5*wavelength(self.T_endpoint, self.MagneticField.nominal_field) diff --git a/mermithid/sensitivity/SensitivityFormulas.py b/mermithid/sensitivity/SensitivityFormulas.py index 4ae8207b..23489aff 100755 --- a/mermithid/sensitivity/SensitivityFormulas.py +++ b/mermithid/sensitivity/SensitivityFormulas.py @@ -316,7 +316,7 @@ def syst_doppler_broadening(self): delta = self.DopplerBroadening.Default_Systematic_Uncertainty return sigma, delta - # termal doppler broardening + # thermal doppler broadening gasTemp = self.DopplerBroadening.gas_temperature mass_T = self.T_mass endpoint = self.T_endpoint @@ -335,7 +335,7 @@ def syst_doppler_broadening(self): Ee = endpoint + me*c0**2 p_rec = np.sqrt( Emax**2-me**2*c0**4 + (Emax - Ee - E_rec)**2 - mbeta**2 + 2*Ee*(Emax - Ee - E_rec)*betae*betanu*cosThetaenu ) sigma_trans = np.sqrt(p_rec**2/(2*mass_T)*2*kB*gasTemp) - + # sigma_trans = 2 * Ke * np.sqrt(kB_eV * trapped_gas_temp / (mass (eV) * beta^2)) if self.Experiment.atomic == True: delta_trans = np.sqrt(p_rec**2/(2*mass_T)*kB/gasTemp*self.DopplerBroadening.gas_temperature_uncertainty**2) else: From bf17249f2533067aa0bb921b1db75f4f26671661 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Mon, 2 Mar 2026 15:42:38 -0500 Subject: [PATCH 45/57] Added Atomic Calculator stand alone functions python file --- mermithid/cavity/AtomicCalculator.py | 264 ++++++++++++++++++ .../sensitivity/SensitivityCavityFormulas.py | 261 +---------------- 2 files changed, 271 insertions(+), 254 deletions(-) create mode 100644 mermithid/cavity/AtomicCalculator.py diff --git a/mermithid/cavity/AtomicCalculator.py b/mermithid/cavity/AtomicCalculator.py new file mode 100644 index 00000000..cc37dd11 --- /dev/null +++ b/mermithid/cavity/AtomicCalculator.py @@ -0,0 +1,264 @@ +import numpy as np +from scipy.special import roots_laguerre +from mermithid.misc.Constants_numericalunits import * +''' +Ben Jones calculation disagrees with evaporation losses in atomic calc and must be integrated: https://3.basecamp.com/3700981/buckets/3107037/uploads/8902667883 + +''' + +# Jins functions - Atomic Calculator +# Python decides whether a name is local or global at compile time (not runtime). The name must already exist in the module’s global namespace by the time the function runs, or Python has nowhere to bind it. + +# C157 - [m] Fiducial distance from the cavity wall between max(ioffe_bite, larmor_radius) +# Ioffe field at wall is now the quadrature of the central field and Ioffe field by itself, everywhere it is used. +def calculate_ioffe_bite(nominal_field, magnetic_inhomogenity, ioffe_field, ioffe_multipolarity, cavity_radius): + return ((2 * nominal_field**2 * magnetic_inhomogenity / (2 * nominal_field**2 * magnetic_inhomogenity + ioffe_field**2))**(1/(ioffe_multipolarity-2)) * cavity_radius * -1) + cavity_radius + +# C271 - [m/s] Atomic thermal speed of 3T or 3He (C270) +def calculate_average_velocity(temp): + average_velocity = c0 * np.sqrt((8 * 0.025*eV * temp / np.absolute(T0)) / (np.pi * tritium_mass_atomic)) + #average_velocity = c0 * np.sqrt((8 * kB * temp) / (np.pi * tritium_mass_atomic * eV_J)) + return average_velocity + +# C334 - [m^2] Cylinder wall; no endcaps included (2*pi*R*L) +def calculate_trap_wall_area(cavity_radius, cavity_length): + # (Cavity length is cavity top plate - top of cone / 2) + trap_wall_area = 2 * np.pi * cavity_radius * cavity_length + return trap_wall_area + +# C201 - [m^-1] Add the gravity-temperature scale parameter “b” for magnetogravitational trap. For pure magnetic trap, make gravity weaker by ratio of L/D. +def calculate_gravity_temperature_scale(trapped_gas_temp, pure_magnetic_flag, cavity_L_over_D): + gravity_temperature_scale = ((tritium_mass_atomic/c0**2 * gravity / (kB * trapped_gas_temp)) * np.absolute(pure_magnetic_flag * (1 + 1 / cavity_L_over_D) - 1)) + return gravity_temperature_scale + +# C202 - [m^-2] Add the surface density scale parameter “a/b” for magnetogravitational trap +def calculate_surface_density_scale(design_density, gravity_temperature_scale, top_plate_cavity): + surface_density_scale = design_density * top_plate_cavity / (1 - np.exp(-gravity_temperature_scale * top_plate_cavity)) + return surface_density_scale + +# C204 - [atom] Inventory in physical volume +def calculate_inventory(design_density, volume): + return design_density * volume + +# C203 - [m^-3] Mean Trap Density within coils. Density in the electron-trapping region as a function of the density in the full volume. Differ depending on whether the experiment is horizontal or vertical. (mean density) +# Full volume (everywhere where atoms are trapped) includes some length of the cavity past the ends of the trap, as well as the volume inside the Ioffe cones. +# In a horizontal experiment, the average atom density in the electron trapping region would be about the same as in the full volume, because the electron trapping region and the non-electron trapping region have the same range/distribution of heights in the them. +# In a vertical experiment, the atom density will be different in the full volume compared with the electron-trapping volume, since atoms pool toward the bottom Ioffe cone. +def calculate_mean_trap_density(design_density, top_plate_cavity, trapped_gas_temp, pure_magnetic_flag, cavity_L_over_D, trap_coil_1, trap_coil_2): + gravity_temp_scale = calculate_gravity_temperature_scale(trapped_gas_temp, pure_magnetic_flag, cavity_L_over_D) + surface_density_scale = calculate_surface_density_scale(design_density, gravity_temp_scale, top_plate_cavity) + mean_trap_density = design_density * top_plate_cavity * (np.exp(-gravity_temp_scale * trap_coil_1) - np.exp(-gravity_temp_scale * trap_coil_2)) / ((1 - np.exp(-gravity_temp_scale * top_plate_cavity)) * (trap_coil_2 - trap_coil_1)) + return mean_trap_density + +# C208 - [decays/s] Radioactivity in trap per cavity +def calculate_activity_in_trap(number_density, cavity_radius, trap_coil_1, trap_coil_2): + # C206 - [atom] Use exponential density vertical gradient, calculate mean density between trap coils. Calculate atoms and activity between the trap coils. + atoms_between_trap_coils = number_density * np.pi * cavity_radius**2 * np.absolute(trap_coil_2 - trap_coil_1) + return atoms_between_trap_coils / tritium_livetime + +# Background T2 in atomic trap: +def calculate_T2_background_atomic_trap(cavity_radius, cavity_length, cavity_wall_temp, max_ratio_T2_T, number_density): + # C306 - [mbar] Vapor Pressure of T2 w/ constants for saturated T2 vapor from Souers et al. + T2_vapor_pressure = mbar * np.exp(T2_vapor_A + (T2_vapor_B*K/cavity_wall_temp) + T2_vapor_B_prime * np.log(cavity_wall_temp/K)) / 0.76 + # C307 - [m^-3] Density of saturated vapor + sat_vapor_density = NA * (T2_vapor_pressure/mbar) * np.absolute(T0/cavity_wall_temp) / (1000 * molar_volume) + # C308 - [m^-3] = s^-1 * sqrt(kg/eV) / m^2 T2 density form desorption at end of a cycle + T2_density_desorp = 4 * molecules_desorbed_wall_beta * (wall_activity * Ci_Bq) * np.sqrt(molecules_desorbed_wall_beta * (tritium_mass_atomic / c0**2) / (2 * atomic_tritium_recoil_energy * eV_J )) / calculate_trap_wall_area(cavity_radius, cavity_length) + # C309 [m^-3] Total T2 density + T2_total_density = sat_vapor_density + T2_density_desorp + # C310 - T2/T number ratio. Activity ratio is 1.64 times bigger + T2_T_ratio = T2_total_density / number_density + #return T2_vapor_pressure, sat_vapor_density, T2_density_desorp, T2_total_density, T2_T_ratio + return T2_total_density, T2_T_ratio + +# Aperture Heat Leak: +def calculate_aperture_heat_leak(trapped_gas_temp, design_density, volume): + # C250 - [m/s] Average velocity of trapped gas + trapped_gas_velocity = calculate_average_velocity(trapped_gas_temp) + # C251 - [atom/s] Inward atom current required through aperture to balance losses (j_aperture) + current_aperture_leak = area_atom_loading_aperture * design_density * trapped_gas_velocity / 4 + # C252 - [s] Time constant for loss through aperture + time_constant_aperture = calculate_inventory(design_density, volume) / current_aperture_leak + return time_constant_aperture, current_aperture_leak + +# Radioactivity Heat Leak: +# Radioactivity loss is just a single number from Ben Clark’s thesis, needs to be done better. Dependent on a cross-section that varies with temp. +def calculate_rad_heat_leak(cavity_radius, number_density, trap_coil_1, trap_coil_2, design_density, volume, net_efficiency): + # C256 - [s] Input from Ben Clark's thesis with a mirror ratio of 0.5 + half_life = 11 * 24 * 3600 * s + trap_activity = calculate_activity_in_trap(number_density, cavity_radius, trap_coil_1, trap_coil_2) + # C257 - [atom/s] Atom current required to balance loss due to radioactivity + current_rad_leak = trap_activity * np.log(2) * tritium_livetime / (net_efficiency * half_life) + # C258 - [s] Time constant for loss due to radioactivty (tau_rad) + time_constant_rad = calculate_inventory(design_density, volume) / current_rad_leak + return time_constant_rad, current_rad_leak + +# T2 Desorption from the wall: T2 desorption from walls as a background and a trap-heating loss. +# Burst of molecules is emitted from the wall with each decay and can knock out atoms from the trap. Description in CDR 4.5.5. New parameter to enter is the number of molecules (choose 1000 for now). +def calculate_T2_desorption_from_wall(cavity_radius, cavity_length, design_density, volume): + # C261 - [s] Mean lifetime of atom in trap from desorption + time_constant_desorp = calculate_trap_wall_area(cavity_radius, cavity_length) / (2 * molecules_desorbed_wall_beta * (wall_activity * Ci_Bq) * H_H2_crosssection) + # C262 - [atom/s] Atom current required to keep up with desorption losses + current_desorp = calculate_inventory(design_density, volume) / time_constant_desorp + return time_constant_desorp, current_desorp + +# He Heat Leak: Notable difference comes from pumping speed, physical volume, and design density +def calculate_He_heat_leak(pumping_speed_theoretical, pumping_cavity_termination, turbopump_speed, cavity_temperature, design_density, volume): + He_velocity = calculate_average_velocity(cavity_temperature) + # C266 - [atom/s] Not including flow for He heat leak itself + He_production_rate = wall_activity * Ci_Bq + # C269 - [m^-3] Number density of Helium + He_density = He_production_rate * ((1/pumping_speed_theoretical) + (1/pumping_cavity_termination) + (1/turbopump_speed)) + # C272 - [s] Time constant for loss due to He-3 heat leak + time_constant_He = 1 / (He_density * He_velocity * H_He_crosssection) + # C273 - Fraction of total gas that is He + He_fraction = He_density / design_density + # C274 - [s^-1] Atom current required to keep up with He-3 heat leak + current_He_leak = calculate_inventory(design_density, volume) / time_constant_He + return time_constant_He, current_He_leak + +# Dipolar loss rate: Calculation with z-dependent density, cylinder & cone. Polynomial fit of nominal field. +def calculate_dipolar_loss(nominal_field, cavity_radius, design_density, volume, trapped_gas_temp, cavity_L_over_D, top_cone, top_plate_cavity, pure_magnetic_flag): + gravity_temperature_scale = calculate_gravity_temperature_scale(trapped_gas_temp, pure_magnetic_flag, cavity_L_over_D) + surface_density_scale = calculate_surface_density_scale(design_density, gravity_temperature_scale, top_plate_cavity) + # C298 - [m^3/s] Polynomial fit for Dipolar spin-flip rate (G_dd). Depends on field (Lagendijk et al). Polynomial-log fit used now. + dipolar_spin_flip_rate = (60.106 + 13.812 * np.log(nominal_field) - 4.7867 * np.log(nominal_field)**2 - 2.3192 * np.log(nominal_field)**3 \ + - 0.32663 * np.log(nominal_field)**4 - 0.015775 * np.log(nominal_field)**5) * 1e-22 * LGd_rates * m**3/s + # C299 - [atom/s] Flow to keep up with dipolar losses + current_dipolar = dipolar_spin_flip_rate * np.pi * cavity_radius**2 * surface_density_scale**2 * (gravity_temperature_scale * (np.exp(-gravity_temperature_scale * top_cone) \ + - np.exp(-gravity_temperature_scale * top_plate_cavity)) / 2 + (1 - np.exp(-2 * gravity_temperature_scale * top_cone) * (2 * gravity_temperature_scale**2 \ + * top_cone**2 + 2 * gravity_temperature_scale * top_cone + 1)) / (4 * top_cone**2 * gravity_temperature_scale)) + # C300 - [s] Time constant for loss due to dipolar spin-flip loss + time_constant_dipolar = calculate_inventory(design_density, volume) / current_dipolar + return time_constant_dipolar, current_dipolar + +# Evaporation loss rate: Does not take into account density of states with height. Magnetic potential limits evaporation. +# Cone and cylinder now separate because cone is weaker owing to azimuthal modulation of Ioffe field. Each now has its own density multiplier. The weaker cone field is handled in a separate Igor calculation Coneangle.pxp outside this SS and entered as a loss rate multiplier in C111. +def calculate_evaporation_loss(cavity_L_over_D, pure_magnetic_flag, trapped_gas_temp, cavity_radius, ioffe_field, nominal_field, cavity_length, top_cone, top_plate_cavity, design_density, relative_loss_rate_cone_wall, volume): + gravity_temperature_scale = calculate_gravity_temperature_scale(trapped_gas_temp, pure_magnetic_flag, cavity_L_over_D) + surface_density_scale = calculate_surface_density_scale(design_density, gravity_temperature_scale, top_plate_cavity) + + # C283 - [m] Mean free path at the base of cavity + mfp_cavity_base = 1 / (gravity_temperature_scale * surface_density_scale * tritium_tritium_crosssection_atomic) + # C284 - Pure Magnetic eta + eta = (bohr_magneton / eV_J) * (np.sqrt(ioffe_field**2 + nominal_field**2) - nominal_field) / (trapped_gas_temp * 0.025*eV / np.absolute(T0)) + # C285 - Magnetogravitational eta + eta_grav = (tritium_mass_atomic/c0**2) * kg_amu * gravity * cavity_length / (kB * trapped_gas_temp) + # C286 - [s^-1] Evaporation out the top; set to 1/1000 for pure magnetic trap + evaporation_top = (np.log(1 + np.log(2) * np.exp(gravity_temperature_scale * top_plate_cavity) / (surface_density_scale * tritium_tritium_crosssection_atomic)) \ + * surface_density_scale * gravity_temperature_scale * np.exp(-gravity_temperature_scale * top_plate_cavity) * np.sqrt(np.pi * kB * trapped_gas_temp / (2 * tritium_mass_atomic / c0**2)) * cavity_radius**2) \ + * np.absolute(pure_magnetic_flag * (1 + 0.001) -1) + # C287 - [s] Time constant for evaporation out of the top + time_constant_evap_top = calculate_inventory(design_density, volume) / evaporation_top + # C288 - [s^-1] Evaporation to the sides + evaporation_sides = (2 * np.pi * cavity_radius * cavity_length * design_density * np.sqrt(kB * trapped_gas_temp / (2 * np.pi * tritium_mass_atomic / c0**2)) \ + * np.exp(-eta) * top_plate_cavity * (np.exp(-gravity_temperature_scale * top_cone) - np.exp(-gravity_temperature_scale * top_plate_cavity)) \ + / ((1 - np.exp(-gravity_temperature_scale * top_plate_cavity)) * (top_plate_cavity - top_cone))) + # C290 - [s^-1] Evaporation in cone; for pure magnetic, double it to account for both ends + evaporation_cone = (relative_loss_rate_cone_wall * np.pi * cavity_radius * np.sqrt(cavity_radius**2 + top_cone**2) * design_density \ + * np.sqrt(kB * trapped_gas_temp / (2 * np.pi * tritium_mass_atomic/c0**2)) * np.exp(-eta) * top_plate_cavity * (1 - np.exp(-gravity_temperature_scale * top_cone)) \ + / ((1 - np.exp(-gravity_temperature_scale * top_plate_cavity)) * top_cone)) * np.absolute(pure_magnetic_flag + 1) + # C292 - [s] Time constant for evaporation loss of cone and sides + time_constant_evap_cone_sides = calculate_inventory(design_density, volume) / (evaporation_cone + evaporation_sides) + # C295 - [atom/s] Flow to keep up with evaporation losses aka Total evaporation_sum + current_evaporation = evaporation_top + evaporation_sides + evaporation_cone + # C294 - [s] Time constant for total evaporation loss + time_constant_total_evaporation = calculate_inventory(design_density, volume) / current_evaporation + return time_constant_total_evaporation, current_evaporation, evaporation_top, evaporation_sides, evaporation_cone + +# T2 Heat Leak: +def calculate_T2_heat_leak(aperture_current, rad_current, He_current, evap_current, dipolar_current, T2_total_density, cavity_wall_temp, design_density, volume): + # C277 - [atom/s] T2 production rate (all atoms entering, except for T2 heat leak itself) + T2_production_rate = aperture_current + rad_current + He_current + evap_current + dipolar_current + # C279 - [s] Time constant for loss due to T2 heat leak + time_constant_T2 = 1 / (T2_total_density * H_H2_crosssection * calculate_average_velocity(cavity_wall_temp) / np.sqrt(2)) + # C280 - [atom/s] + current_T2_leak = calculate_inventory(design_density, volume) / time_constant_T2 + return time_constant_T2, current_T2_leak + +# C303 - [s] Total lifetime of trap: +def calculate_trap_lifetime(time_rad, time_desorp, time_He, time_T2, time_evap, time_dipolar): + return 1 / ((1/time_rad) + (1/time_desorp) + (1/time_He) + (1/time_T2) + (1/time_evap) + (1/time_dipolar)) + +# Atom Supply into Trap: +# The inputted atom current determines the (average) density in the full volume, but the density for mass sensitivity is average atom density within the electron trap. Conversion is needed between these two densities +def calculate_trap_atom_supply(rad_current, evap_current, dipolar_current, He_current, T2_current, design_density, volume): + # The ‘atomic current required’ includes the c-state atoms as well as d-state, although those are lost almost immediately to spin exchange. + # C107 - C state flag for hyperfine states for total gas into trap. Trap is always d-state only. No c-states (1), include c-states (2) + c_states_flag = 1 + # C318 - [atom/s] Total current is atom current (d state only) + He heat leak + T2 heat leak + total_atom_current = rad_current + evap_current + dipolar_current + He_current + T2_current + # C319 - [s] Total time constant for atoms to remain in the trap + total_time_constant_trap = calculate_inventory(design_density, volume) / total_atom_current + # C320 - [atom/s] Atom current with c-states and d-states in calculation + total_atom_current_states = total_atom_current * c_states_flag + return total_time_constant_trap, total_atom_current_states +''' +There are two possible methods for pumping away the tritium, all of which is eventually in molecular form. One is to keep the tritium in circulation by using turbopumps and avoiding temperatures below 10K in the trap region, and the second is to cool parts of the trap region outside the magnetic wall to < 3K in order to cryopump the tritium. +The mechanical pumping method is impractical because of the pumping speed required, a fraction of a billion L/s, where an achievable upper limit is 4 orders of magnitude smaller. +The speed requirement is driven by the combination of the input atomic current and the need to maintain the molecular fraction below 10−4. +The cryopumping method ties up very large amounts of tritium, tens to hundreds of kCi, by the end of a day, and the need to warm up and recycle that tritium on such a short time scale would lead to low statistical precision and instabilities. +Still need turbos to handle He-3 and to do pumpout during recycling, but initial hope that we could run turbos only and keep the T2 pressure low enough was not realized. Must recycle. +''' +# Turbopump Calculations: Need to convert tritium mass amu to eV : 1 amu ~ 931 MeV/c^2 and kB = 8.6E-05 eV/K. Multiply by sqrt(2) if atomic; # Useful for Atomic and Helium-3 +# C233/C236 - [m^3/s] (molecular/atomic) theoretical pumping speed. The mean speed of the cylinder connected to a perfect pump (Dushman). Uniform source, outputs mean density in cavity. Cannot exceed (obstruction not included). +def turbopump_speed_limit(cavity_radius, cavity_temperature, cavity_L_over_D, atomic_flag): + return (np.pi * cavity_radius**2 * c0 * np.sqrt(kB_eV * cavity_temperature / (4 * np.pi * tritium_mass_atomic)) / (0.5 + cavity_L_over_D / 8)) * (1 + atomic_flag * (np.sqrt(2) - 1)) +# C235/C238 - [m^3/s] (molecular/atomic) Assumed ambient room air (28-29 amu) temperature (293 K, and a pumping speed of 0.5 L/s +def cavity_termination_speed(turbopumping_speed_air, cavity_top_plate_temp, atomic_flag): + return (turbopumping_speed_air * np.sqrt(cavity_top_plate_temp * 28 * amu * c0**2 / (293 * K * 2 * tritium_mass_atomic))) * (1 + atomic_flag * (np.sqrt(2) - 1)) +# C239 - [m^3/s] Turbopump in series speed (2.5 m^3/s * 2) +def turbopump_speed(number_turbos, turbopumping_speed_gas): + return number_turbos * turbopumping_speed_gas +# C327 - [m^-3] Molecular density allowed by molecular/atomic assuming total atom density in all of physical volume +def molecular_density_allowed(design_density, max_ratio_T2_T): + return design_density * max_ratio_T2_T * ground_state_branch_atomic / 2 +# The Pumping speed required (molecular) in C328 is kind of a dimensional calculation, not really a speed. As a result, the cryopumping speed limit is probably not real. Ideal cryopumping is the impingement rate: it all sticks. The actual molecular density is determined by the vapor pressure (Souers formula). +# C328 - [m^3/s] - Pumping speed required to keep molecular pressure from exceeding reference limit +def turbopump_speed_required(atom_current, molecular_density_limit): + return atom_current / 2 / molecular_density_limit +# C331 - If above 1000, critical +def ratio_turbopump_speed(pumping_speed_required, pumping_speed_limit, cavity_termination_speed, turbopump_speed): + return pumping_speed_required * ((1 / pumping_speed_limit) + (1 / cavity_termination_speed) + (1 / turbopump_speed)) + +# Cryopumping Calculations: +def calculate_cryopump_speed(cavity_wall_temp, cavity_radius, cavity_length, pumping_speed_required, atom_current, design_density, max_ratio_T2_T): + # C335 - [m/s] Cryopumping speed per area for mass-3 atoms at wall temperature stated above. sqrt(k T/(2 pi M)); see https://www.synsysco.com/wp-content/uploads/2016/02/Basics-of-Cryopumping-Booklet.pdf. Using M=3 because most gas is spin-flipped atoms. + cryopump_speed_per_area = np.sqrt(kB_eV * cavity_wall_temp / (2 * np.pi * tritium_mass_atomic)) * c0 + # C336 - [m^3/s] With given cylindrical dimensions at wall temperature stated above for mass-3 atoms + trap_interior_surface_speed = cryopump_speed_per_area * calculate_trap_wall_area(cavity_radius, cavity_length) + # C338 - Ratio of required molecular speed to cryopumping speed. Ideal cryopumping is the impingement rate (all sticks). + ratio_cryopump_speed = pumping_speed_required / trap_interior_surface_speed + # C339 - [W] Heat delivered to the cold surface + recombination_heat_load = atom_current * molecular_tritium_binding_energy * eV_J / 2 + # C340 - [Ci/day] Accumulated activity per day + accumulated_activity_day = atom_current * lambda_tritium * (86400*s/day) / Ci_Bq + # C342 - [day] Time between recycling cryosurface. For cryopumping atomic experiment; determines inventory on walls + time_recycling_cryosurface = wall_activity / accumulated_activity_day + # C344 - [m^3/s] Cryopumping speed for molecules + cryopump_speed = np.sqrt(kB_eV * cavity_wall_temp / (4 * np.pi * tritium_mass_atomic)) * c0 * calculate_trap_wall_area(cavity_radius, cavity_length) + # C345 - [molecules/s] max allowed injection rate of molecules + max_molecules_injection = cryopump_speed * molecular_density_allowed(design_density, max_ratio_T2_T) + return ratio_cryopump_speed, cryopump_speed, time_recycling_cryosurface + +# Injection Line Calculations: Polynomial fit of injection field +# Density in the injection line, mfp in injection line, dipolar loss in injection line. There are 2 new entries for this in the choices – beam temperature and beamline field. +def calculate_injection_line(atom_current, cavity_radius, design_density, trapped_gas_temp, nominal_field, injection_gas_temp, injection_field): + # C349 - [m/s] Vertical flow speed in trap at bottom and in the beamline + vertical_injection_speed = atom_current / (np.pi * cavity_radius**2 * design_density) + # C350 - [J/atom] Energy in trapped gas and in beam gas + trapped_gas_energy = (1.5 * kB * trapped_gas_temp) + (0.5 * (tritium_mass_atomic/c0**2) * vertical_injection_speed**2) + (bohr_magneton * nominal_field) + # C351 - [m^-3] Density in the injection line + injection_density = atom_current * np.sqrt((tritium_mass_atomic/c0**2) / (2 * trapped_gas_energy - 3 * kB * injection_gas_temp - 2 * bohr_magneton * injection_field)) / area_atom_loading_aperture + # C352 - [m] Mean free path in beamline + mfp_beamline = 1 / (injection_density * tritium_tritium_crosssection_atomic) + # C353 - [m/s] Vertical flow speed through aperture + vertical_aperture_speed = design_density * np.pi * cavity_radius**2 * vertical_injection_speed / (area_atom_loading_aperture * injection_density) + # C354 - [J/atom] Energy in injected gas + injection_gas_energy = (1.5 * kB * injection_gas_temp) + (0.5 * (tritium_mass_atomic/c0**2) * vertical_aperture_speed**2) + (bohr_magneton * injection_field) + # C355 - [m^3/s] 5th degree polynomial fit for injection line dipolar loss rate + injection_dipolar_loss_rate = (60.106 + 13.812 * np.log(injection_field/T) - 4.7867 * np.log(injection_field/T)**2 - 2.3192 * np.log(injection_field/T)**3 \ + - 0.32663 * np.log(injection_field/T)**4 - 0.015775 * np.log(injection_field/T)**5) * 1e-22 * LGd_rates * m**3/s + # C356 - [m^-1] Beamline dipolar loss + beamline_dipolar = injection_dipolar_loss_rate * injection_density / vertical_aperture_speed + return vertical_injection_speed, trapped_gas_energy, injection_density, vertical_aperture_speed, injection_gas_energy diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index 20ec38d1..18a2c408 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -15,267 +15,16 @@ from mermithid.misc.Constants_numericalunits import * from mermithid.misc.CRESFunctions_numericalunits import * from mermithid.cavity.HannekeFunctions import * +from mermithid.cavity.AtomicCalculator import * from mermithid.sensitivity.SensitivityFormulas import * - try: from morpho.utilities import morphologging logger = morphologging.getLogger(__name__) except: print("Run without morpho!") -# Jins functions - Atomic Calculator -# Python decides whether a name is local or global at compile time (not runtime). The name must already exist in the module’s global namespace by the time the function runs, or Python has nowhere to bind it. - -# C157 - [m] Fiducial distance from the cavity wall between max(ioffe_bite, larmor_radius) -# Ioffe field at wall is now the quadrature of the central field and Ioffe field by itself, everywhere it is used. -def calculate_ioffe_bite(nominal_field, magnetic_inhomogenity, ioffe_field, ioffe_multipolarity, cavity_radius): - return ((2 * nominal_field**2 * magnetic_inhomogenity / (2 * nominal_field**2 * magnetic_inhomogenity + ioffe_field**2))**(1/(ioffe_multipolarity-2)) * cavity_radius * -1) + cavity_radius - -# C271 - [m/s] Atomic thermal speed of 3T or 3He (C270) -def calculate_average_velocity(temp): - average_velocity = c0 * np.sqrt((8 * 0.025*eV * temp / np.absolute(T0)) / (np.pi * tritium_mass_atomic)) - #average_velocity = c0 * np.sqrt((8 * kB * temp) / (np.pi * tritium_mass_atomic * eV_J)) - return average_velocity - -# C334 - [m^2] Cylinder wall; no endcaps included (2*pi*R*L) -def calculate_trap_wall_area(cavity_radius, cavity_length): - # (Cavity length is cavity top plate - top of cone / 2) - trap_wall_area = 2 * np.pi * cavity_radius * cavity_length - return trap_wall_area - -# C201 - [m^-1] Add the gravity-temperature scale parameter “b” for magnetogravitational trap. For pure magnetic trap, make gravity weaker by ratio of L/D. -def calculate_gravity_temperature_scale(trapped_gas_temp, pure_magnetic_flag, cavity_L_over_D): - gravity_temperature_scale = ((tritium_mass_atomic/c0**2 * gravity / (kB * trapped_gas_temp)) * np.absolute(pure_magnetic_flag * (1 + 1 / cavity_L_over_D) - 1)) - return gravity_temperature_scale - -# C202 - [m^-2] Add the surface density scale parameter “a/b” for magnetogravitational trap -def calculate_surface_density_scale(design_density, gravity_temperature_scale, top_plate_cavity): - surface_density_scale = design_density * top_plate_cavity / (1 - np.exp(-gravity_temperature_scale * top_plate_cavity)) - return surface_density_scale - -# C204 - [atom] Inventory in physical volume -def calculate_inventory(design_density, volume): - return design_density * volume - -# C208 - [decays/s] Radioactivity in trap per cavity -def calculate_activity_in_trap(number_density, cavity_radius): - # C090 - [m] - coil_1_height = 0.75*m - # C091 - [m] - coil_2_height = 4.80*m - # C206 - [atom] Use exponential density vertical gradient, calculate mean density between trap coils. Calculate atoms and activity between the trap coils. - atoms_between_trap_coils = number_density * np.pi * cavity_radius**2 * np.absolute(coil_2_height - coil_1_height) - return atoms_between_trap_coils / tritium_livetime - -# Background T2 in atomic trap: -def calculate_T2_background_atomic_trap(cavity_radius, cavity_length, cavity_wall_temp, max_ratio_T2_T, number_density): - # C306 - [mbar] Vapor Pressure of T2 w/ constants for saturated T2 vapor from Souers et al. - T2_vapor_pressure = mbar * np.exp(T2_vapor_A + (T2_vapor_B*K/cavity_wall_temp) + T2_vapor_B_prime * np.log(cavity_wall_temp/K)) / 0.76 - # C307 - [m^-3] Density of saturated vapor - sat_vapor_density = NA * (T2_vapor_pressure/mbar) * np.absolute(T0/cavity_wall_temp) / (1000 * molar_volume) - # C308 - [m^-3] = s^-1 * sqrt(kg/eV) / m^2 T2 density form desorption at end of a cycle - T2_density_desorp = 4 * molecules_desorbed_wall_beta * (wall_activity * Ci_Bq) * np.sqrt(molecules_desorbed_wall_beta * (tritium_mass_atomic / c0**2) / (2 * atomic_tritium_recoil_energy * eV_J )) / calculate_trap_wall_area(cavity_radius, cavity_length) - # C309 [m^-3] Total T2 density - T2_total_density = sat_vapor_density + T2_density_desorp - # C310 - T2/T number ratio. Activity ratio is 1.64 times bigger - T2_T_ratio = T2_total_density / number_density - #return T2_vapor_pressure, sat_vapor_density, T2_density_desorp, T2_total_density, T2_T_ratio - return T2_total_density, T2_T_ratio - -# Aperture Heat Leak: -def calculate_aperture_heat_leak(trapped_gas_temp, design_density, volume): - # C250 - [m/s] Average velocity of trapped gas - trapped_gas_velocity = calculate_average_velocity(trapped_gas_temp) - # C251 - [atom/s] Inward atom current required through aperture to balance losses (j_aperture) - current_aperture_leak = area_atom_loading_aperture * design_density * trapped_gas_velocity / 4 - # C252 - [s] Time constant for loss through aperture - time_constant_aperture = calculate_inventory(design_density, volume) / current_aperture_leak - return time_constant_aperture, current_aperture_leak - -# Radioactivity Heat Leak: -# Radioactivity loss is just a single number from Ben Clark’s thesis, needs to be done better. -def calculate_rad_heat_leak(cavity_radius, number_density, design_density, volume, net_efficiency): - # C256 - [s] Input from Ben Clark's thesis with a mirror ratio of 0.5 - half_life = 11 * 24 * 3600 * s - trap_activity = calculate_activity_in_trap(number_density, cavity_radius) - # C257 - [atom/s] Atom current required to balance loss due to radioactivity - current_rad_leak = trap_activity * np.log(2) * tritium_livetime / (net_efficiency * half_life) - # C258 - [s] Time constant for loss due to radioactivty (tau_rad) - time_constant_rad = calculate_inventory(design_density, volume) / current_rad_leak - return time_constant_rad, current_rad_leak - -# T2 Desorption from the wall: T2 desorption from walls as a background and a trap-heating loss. -# Burst of molecules is emitted from the wall with each decay and can knock out atoms from the trap. Description added to end of CDR 4.5.5. New parameter to enter is the number of molecules (choose 1000 for now). -def calculate_T2_desorption_from_wall(cavity_radius, cavity_length, design_density, volume): - # C261 - [s] Mean lifetime of atom in trap from desorption - time_constant_desorp = calculate_trap_wall_area(cavity_radius, cavity_length) / (2 * molecules_desorbed_wall_beta * (wall_activity * Ci_Bq) * H_H2_crosssection) - # C262 - [atom/s] Atom current required to keep up with desorption losses - current_desorp = calculate_inventory(design_density, volume) / time_constant_desorp - return time_constant_desorp, current_desorp - -# He Heat Leak: Notable difference comes from pumping speed, physical volume, and design density -def calculate_He_heat_leak(pumping_speed_theoretical, pumping_cavity_termination, turbopump_speed, cavity_temperature, design_density, volume): - He_velocity = calculate_average_velocity(cavity_temperature) - # C266 - [atom/s] Not including flow for He heat leak itself - He_production_rate = wall_activity * Ci_Bq - # C269 - [m^-3] Number density of Helium - He_density = He_production_rate * ((1/pumping_speed_theoretical) + (1/pumping_cavity_termination) + (1/turbopump_speed)) - # C272 - [s] Time constant for loss due to He-3 heat leak - time_constant_He = 1 / (He_density * He_velocity * H_He_crosssection) - # C273 - Fraction of total gas that is He - He_fraction = He_density / design_density - # C274 - [s^-1] Atom current required to keep up with He-3 heat leak - current_He_leak = calculate_inventory(design_density, volume) / time_constant_He - return time_constant_He, current_He_leak - -# Dipolar loss rate: Calculation with z-dependent density, cylinder & cone. -def calculate_dipolar_loss(nominal_field, cavity_radius, design_density, volume, trapped_gas_temp, cavity_L_over_D, top_cone, top_plate_cavity, pure_magnetic_flag): - gravity_temperature_scale = calculate_gravity_temperature_scale(trapped_gas_temp, pure_magnetic_flag, cavity_L_over_D) - surface_density_scale = calculate_surface_density_scale(design_density, gravity_temperature_scale, top_plate_cavity) - # C298 - [m^3/s] Polynomial fit for Dipolar spin-flip rate (G_dd). Depends on field (Lagendijk et al). Polynomial-log fit used now. - dipolar_spin_flip_rate = (60.106 + 13.812 * np.log(nominal_field) - 4.7867 * np.log(nominal_field)**2 - 2.3192 * np.log(nominal_field)**3 \ - - 0.32663 * np.log(nominal_field)**4 - 0.015775 * np.log(nominal_field)**5) * 1e-22 * LGd_rates * m**3/s - # C299 - [atom/s] Flow to keep up with dipolar losses - current_dipolar = dipolar_spin_flip_rate * np.pi * cavity_radius**2 * surface_density_scale**2 * (gravity_temperature_scale * (np.exp(-gravity_temperature_scale * top_cone) \ - - np.exp(-gravity_temperature_scale * top_plate_cavity)) / 2 + (1 - np.exp(-2 * gravity_temperature_scale * top_cone) * (2 * gravity_temperature_scale**2 \ - * top_cone**2 + 2 * gravity_temperature_scale * top_cone + 1)) / (4 * top_cone**2 * gravity_temperature_scale)) - # C300 - [s] Time constant for loss due to dipolar spin-flip loss - time_constant_dipolar = calculate_inventory(design_density, volume) / current_dipolar - return time_constant_dipolar, current_dipolar - -# Evaporation loss rate: Does not take into account density of states with height. Magnetic potential limits evaporation. -# Cone and cylinder now separate because cone is weaker owing to azimuthal modulation of Ioffe field. Each now has its own density multiplier. The weaker cone field is handled in a separate Igor calculation Coneangle.pxp outside this SS and entered as a loss rate multiplier in C111. -def calculate_evaporation_loss(cavity_L_over_D, pure_magnetic_flag, trapped_gas_temp, cavity_radius, ioffe_field, nominal_field, cavity_length, top_cone, top_plate_cavity, design_density, relative_loss_rate_cone_wall, volume): - gravity_temperature_scale = calculate_gravity_temperature_scale(trapped_gas_temp, pure_magnetic_flag, cavity_L_over_D) - surface_density_scale = calculate_surface_density_scale(design_density, gravity_temperature_scale, top_plate_cavity) - - # C283 - [m] Mean free path at the base of cavity - mfp_cavity_base = 1 / (gravity_temperature_scale * surface_density_scale * tritium_tritium_crosssection_atomic) - # C284 - Pure Magnetic eta - eta = (bohr_magneton / eV_J) * (np.sqrt(ioffe_field**2 + nominal_field**2) - nominal_field) / (trapped_gas_temp * 0.025*eV / np.absolute(T0)) - # C285 - Magnetogravitational eta - eta_grav = (tritium_mass_atomic/c0**2) * kg_amu * gravity * cavity_length / (kB * trapped_gas_temp) - # C286 - [s^-1] Evaporation out the top; set to 1/1000 for pure magnetic trap - evaporation_top = (np.log(1 + np.log(2) * np.exp(gravity_temperature_scale * top_plate_cavity) / (surface_density_scale * tritium_tritium_crosssection_atomic)) \ - * surface_density_scale * gravity_temperature_scale * np.exp(-gravity_temperature_scale * top_plate_cavity) * np.sqrt(np.pi * kB * trapped_gas_temp / (2 * tritium_mass_atomic / c0**2)) * cavity_radius**2) \ - * np.absolute(pure_magnetic_flag * (1 + 0.001) -1) - # C287 - [s] Time constant for evaporation out of the top - time_constant_evap_top = calculate_inventory(design_density, volume) / evaporation_top - # C288 - [s^-1] Evaporation to the sides - evaporation_sides = (2 * np.pi * cavity_radius * cavity_length * design_density * np.sqrt(kB * trapped_gas_temp / (2 * np.pi * tritium_mass_atomic / c0**2)) \ - * np.exp(-eta) * top_plate_cavity * (np.exp(-gravity_temperature_scale * top_cone) - np.exp(-gravity_temperature_scale * top_plate_cavity)) \ - / ((1 - np.exp(-gravity_temperature_scale * top_plate_cavity)) * (top_plate_cavity - top_cone))) - # C290 - [s^-1] Evaporation in cone; for pure magnetic, double it to account for both ends - evaporation_cone = (relative_loss_rate_cone_wall * np.pi * cavity_radius * np.sqrt(cavity_radius**2 + top_cone**2) * design_density \ - * np.sqrt(kB * trapped_gas_temp / (2 * np.pi * tritium_mass_atomic/c0**2)) * np.exp(-eta) * top_plate_cavity * (1 - np.exp(-gravity_temperature_scale * top_cone)) \ - / ((1 - np.exp(-gravity_temperature_scale * top_plate_cavity)) * top_cone)) * np.absolute(pure_magnetic_flag + 1) - # C292 - [s] Time constant for evaporation loss of cone and sides - time_constant_evap_cone_sides = calculate_inventory(design_density, volume) / (evaporation_cone + evaporation_sides) - # C295 - [atom/s] Flow to keep up with evaporation losses aka Total evaporation_sum - current_evaporation = evaporation_top + evaporation_sides + evaporation_cone - # C294 - [s] Time constant for total evaporation loss - time_constant_total_evaporation = calculate_inventory(design_density, volume) / current_evaporation - return time_constant_total_evaporation, current_evaporation, evaporation_top, evaporation_sides, evaporation_cone - -# T2 Heat Leak: -def calculate_T2_heat_leak(aperture_current, rad_current, He_current, evap_current, dipolar_current, T2_total_density, cavity_wall_temp, design_density, volume): - # C277 - [atom/s] T2 production rate (all atoms entering, except for T2 heat leak itself) - T2_production_rate = aperture_current + rad_current + He_current + evap_current + dipolar_current - # C279 - [s] Time constant for loss due to T2 heat leak - time_constant_T2 = 1 / (T2_total_density * H_H2_crosssection * calculate_average_velocity(cavity_wall_temp) / np.sqrt(2)) - # C280 - [atom/s] - current_T2_leak = calculate_inventory(design_density, volume) / time_constant_T2 - return time_constant_T2, current_T2_leak - -# C303 - [s] Total lifetime of trap: -def calculate_trap_lifetime(time_rad, time_desorp, time_He, time_T2, time_evap, time_dipolar): - return 1 / ((1/time_rad) + (1/time_desorp) + (1/time_He) + (1/time_T2) + (1/time_evap) + (1/time_dipolar)) - -# Atom Supply into Trap: -def calculate_trap_atom_supply(rad_current, evap_current, dipolar_current, He_current, T2_current, design_density, volume): - # The ‘atomic current required’ includes the c-state atoms as well as d-state, although those are lost almost immediately to spin exchange. - # C107 - C state flag for hyperfine states for total gas into trap. Trap is always d-state only. No c-states (1), include c-states (2) - c_states_flag = 1 - # C318 - [atom/s] Total current is atom current (d state only) + He heat leak + T2 heat leak - total_atom_current = rad_current + evap_current + dipolar_current + He_current + T2_current - # C319 - [s] Total time constant for atoms to remain in the trap - total_time_constant_trap = calculate_inventory(design_density, volume) / total_atom_current - # C320 - [atom/s] Atom current with c-states and d-states in calculation - total_atom_current_states = total_atom_current * c_states_flag - return total_time_constant_trap, total_atom_current_states -''' -There are two possible methods for pumping away the tritium, all of which is eventually in molecular form. One is to keep the tritium in circulation by using turbopumps and avoiding temperatures below 10K in the trap region, and the second is to cool parts of the trap region outside the magnetic wall to < 3K in order to cryopump the tritium. -The mechanical pumping method is impractical because of the pumping speed required, a fraction of a billion L/s, where an achievable upper limit is 4 orders of magnitude smaller. -The speed requirement is driven by the combination of the input atomic current and the need to maintain the molecular fraction below 10−4. -The cryopumping method ties up very large amounts of tritium, tens to hundreds of kCi, by the end of a day, and the need to warm up and recycle that tritium on such a short time scale would lead to low statistical precision and instabilities. -Still need turbos to handle He-3 and to do pumpout during recycling, but initial hope that we could run turbos only and keep the T2 pressure low enough was not realized. Must recycle. -''' -# Turbopump Calculations: Need to convert tritium mass amu to eV : 1 amu ~ 931 MeV/c^2 and kB = 8.6E-05 eV/K. Multiply by sqrt(2) if atomic; # Useful for Atomic and Helium-3 -# C233/C236 - [m^3/s] (molecular/atomic) theoretical pumping speed. The mean speed of the cylinder connected to a perfect pump (Dushman). Uniform source, outputs mean density in cavity. Cannot exceed (obstruction not included). -def turbopump_speed_limit(cavity_radius, cavity_temperature, cavity_L_over_D, atomic_flag): - return (np.pi * cavity_radius**2 * c0 * np.sqrt(kB_eV * cavity_temperature / (4 * np.pi * tritium_mass_atomic)) / (0.5 + cavity_L_over_D / 8)) * (1 + atomic_flag * (np.sqrt(2) - 1)) -# C235/C238 - [m^3/s] (molecular/atomic) Assumed ambient room air (28-29 amu) temperature (293 K, and a pumping speed of 0.5 L/s -def cavity_termination_speed(turbopumping_speed_air, cavity_top_plate_temp, atomic_flag): - return (turbopumping_speed_air * np.sqrt(cavity_top_plate_temp * 28 * amu * c0**2 / (293 * K * 2 * tritium_mass_atomic))) * (1 + atomic_flag * (np.sqrt(2) - 1)) -# C239 - [m^3/s] Turbopump in series speed (2.5 m^3/s * 2) -def turbopump_speed(number_turbos, turbopumping_speed_gas): - return number_turbos * turbopumping_speed_gas -# C327 - [m^-3] Molecular density allowed by molecular/atomic assuming total atom density in all of physical volume -def molecular_density_allowed(design_density, max_ratio_T2_T): - return design_density * max_ratio_T2_T * ground_state_branch_atomic / 2 -# The Pumping speed required (molecular) in C328 is kind of a dimensional calculation, not really a speed. As a result, the cryopumping speed limit is probably not real. Ideal cryopumping is the impingement rate: it all sticks. The actual molecular density is determined by the vapor pressure (Souers formula). -# C328 - [m^3/s] - Pumping speed required to keep molecular pressure from exceeding reference limit -def turbopump_speed_required(atom_current, molecular_density_limit): - return atom_current / 2 / molecular_density_limit -# C331 - If above 1000, critical -def ratio_turbopump_speed(pumping_speed_required, pumping_speed_limit, cavity_termination_speed, turbopump_speed): - return pumping_speed_required * ((1 / pumping_speed_limit) + (1 / cavity_termination_speed) + (1 / turbopump_speed)) - -# Cryopumping Calculations: -def calculate_cryopump_speed(cavity_wall_temp, cavity_radius, cavity_length, pumping_speed_required, atom_current, design_density, max_ratio_T2_T): - # C335 - [m/s] Cryopumping speed per area for mass-3 atoms at wall temperature stated above. sqrt(k T/(2 pi M)); see https://www.synsysco.com/wp-content/uploads/2016/02/Basics-of-Cryopumping-Booklet.pdf. Using M=3 because most gas is spin-flipped atoms. - cryopump_speed_per_area = np.sqrt(kB_eV * cavity_wall_temp / (2 * np.pi * tritium_mass_atomic)) * c0 - # C336 - [m^3/s] With given cylindrical dimensions at wall temperature stated above for mass-3 atoms - trap_interior_surface_speed = cryopump_speed_per_area * calculate_trap_wall_area(cavity_radius, cavity_length) - # C338 - Ratio of required molecular speed to cryopumping speed. Ideal cryopumping is the impingement rate (all sticks). - ratio_cryopump_speed = pumping_speed_required / trap_interior_surface_speed - # C339 - [W] Heat delivered to the cold surface - recombination_heat_load = atom_current * molecular_tritium_binding_energy * eV_J / 2 - # C340 - [Ci/day] Accumulated activity per day - accumulated_activity_day = atom_current * lambda_tritium * (86400*s/day) / Ci_Bq - # C342 - [day] Time between recycling cryosurface. For cryopumping atomic experiment; determines inventory on walls - time_recycling_cryosurface = wall_activity / accumulated_activity_day - # C344 - [m^3/s] Cryopumping speed for molecules - cryopump_speed = np.sqrt(kB_eV * cavity_wall_temp / (4 * np.pi * tritium_mass_atomic)) * c0 * calculate_trap_wall_area(cavity_radius, cavity_length) - # C345 - [molecules/s] max allowed injection rate of molecules - max_molecules_injection = cryopump_speed * molecular_density_allowed(design_density, max_ratio_T2_T) - return ratio_cryopump_speed, cryopump_speed, time_recycling_cryosurface - -# Injection Line Calculations: -# density in the injection line, mfp in injection line, dipolar loss in injection line. There are 2 new entries for this in the choices – beam temperature and beamline field. -def calculate_injection_line(atom_current, cavity_radius, design_density, trapped_gas_temp, nominal_field, injection_gas_temp, injection_field): - # C349 - [m/s] Vertical flow speed in trap at bottom and in the beamline - vertical_injection_speed = atom_current / (np.pi * cavity_radius**2 * design_density) - # C350 - [J/atom] Energy in trapped gas and in beam gas - trapped_gas_energy = (1.5 * kB * trapped_gas_temp) + (0.5 * (tritium_mass_atomic/c0**2) * vertical_injection_speed**2) + (bohr_magneton * nominal_field) - # C351 - [m^-3] Density in the injection line - injection_density = atom_current * np.sqrt((tritium_mass_atomic/c0**2) / (2 * trapped_gas_energy - 3 * kB * injection_gas_temp - 2 * bohr_magneton * injection_field)) / area_atom_loading_aperture - # C352 - [m] Mean free path in beamline - mfp_beamline = 1 / (injection_density * tritium_tritium_crosssection_atomic) - # C353 - [m/s] Vertical flow speed through aperture - vertical_aperture_speed = design_density * np.pi * cavity_radius**2 * vertical_injection_speed / (area_atom_loading_aperture * injection_density) - # C354 - [J/atom] Energy in injected gas - injection_gas_energy = (1.5 * kB * injection_gas_temp) + (0.5 * (tritium_mass_atomic/c0**2) * vertical_aperture_speed**2) + (bohr_magneton * injection_field) - # C355 - [m^3/s] 5th degree polynomial fit for injection line dipolar loss rate - injection_dipolar_loss_rate = (60.106 + 13.812 * np.log(injection_field/T) - 4.7867 * np.log(injection_field/T)**2 - 2.3192 * np.log(injection_field/T)**3 \ - - 0.32663 * np.log(injection_field/T)**4 - 0.015775 * np.log(injection_field/T)**5) * 1e-22 * LGd_rates * m**3/s - # C356 - [m^-1] Beamline dipolar loss - beamline_dipolar = injection_dipolar_loss_rate * injection_density / vertical_aperture_speed - return vertical_injection_speed, trapped_gas_energy, injection_density, vertical_aperture_speed, injection_gas_energy - - # Wouters functinos def db_to_pwr_ratio(q_db): return 10**(q_db/10) @@ -529,6 +278,10 @@ def CalcDefaults(self, overwrite=False): if ((hasattr(self.Experiment, 'cavity_L_over_D')) and self.Experiment.cavity_L_over_D_calc_flag): self.Experiment.cavity_L_over_D = self.cavity_length / (self.cavity_radius * 2) + # C203 - Calculate mean density between trap coils if design density is specified + if ((hasattr(self.Experiment, 'design_density')) and self.Experiment.design_density_flag): + self.Experiment.number_density = calculate_mean_trap_density(self.Experiment.design_density, self.top_plate_cavity, self.DopplerBroadening.gas_temperature, self.Experiment.pure_magnetic_flag, self.Experiment.cavity_L_over_D, self.trap_coil_1, self.trap_coil_2) + #Calculate position dependent trapping efficiency self.pos_dependent_trapping_efficiency = trapping_efficiency( z_range = self.Experiment.trap_length /2, bg_magnetic_field = self.MagneticField.nominal_field, @@ -665,7 +418,7 @@ def CavityLength(self): return self.cavity_length def CavityVolume(self): - #Calculate vacuum volume including cone and top service volume + # Calculate vacuum volume including cone and top service volume - important for vertical LFA due to higher density at the bottom if self.Experiment.cavity_cone_flag: # V = V_cyl + V_cone = pi * r^2 * (L + h/3), total height is height of trap coils + height of cone; # See Robertson_H&V_2025-10-29 for explanation; nothing changes about physical volume for second cone in horizontal configuration. @@ -1198,7 +951,7 @@ def print_pumping_requirements(self): self.time_constant_aperture, self.current_aperture_leak = calculate_aperture_heat_leak(self.DopplerBroadening.gas_temperature, self.Experiment.design_density, self.total_cavity_volume) logger.info("Aperture Time Constant: {:.4} s".format(self.time_constant_aperture/s)) logger.info("Atom current required for aperture leak: {:.4e} atoms/s".format(self.current_aperture_leak*s)) - self.time_constant_rad, self.current_rad_leak = calculate_rad_heat_leak(self.cavity_radius, self.Experiment.number_density, self.Experiment.design_density, self.total_cavity_volume, self.Efficiency.net_rad_efficiency) + self.time_constant_rad, self.current_rad_leak = calculate_rad_heat_leak(self.cavity_radius, self.Experiment.number_density, self.trap_coil_1, self.trap_coil_2, self.Experiment.design_density, self.total_cavity_volume, self.Efficiency.net_rad_efficiency) logger.info("Radiation Time Constant: {:.4} s".format(self.time_constant_rad/s)) logger.info("Atom current required for radiation leak: {:.4e} atoms/s".format(self.current_rad_leak*s)) self.time_constant_desorp, self.current_desorp = calculate_T2_desorption_from_wall(self.cavity_radius, self.cavity_length, self.Experiment.design_density, self.total_cavity_volume) From a00f8943c8f2d06fbc98cb2eefa582ee72dffd17 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Tue, 3 Mar 2026 14:26:57 -0500 Subject: [PATCH 46/57] Added Plasma Broadening Effects to Sensitivity --- mermithid/cavity/AtomicCalculator.py | 2 +- .../sensitivity/SensitivityCavityFormulas.py | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/mermithid/cavity/AtomicCalculator.py b/mermithid/cavity/AtomicCalculator.py index cc37dd11..75feb654 100644 --- a/mermithid/cavity/AtomicCalculator.py +++ b/mermithid/cavity/AtomicCalculator.py @@ -2,7 +2,7 @@ from scipy.special import roots_laguerre from mermithid.misc.Constants_numericalunits import * ''' -Ben Jones calculation disagrees with evaporation losses in atomic calc and must be integrated: https://3.basecamp.com/3700981/buckets/3107037/uploads/8902667883 +Ben Jones calculation disagrees with evaporation losses (specifically out the top > 5 mK, agrees otherwise) in atomic calc and must be integrated: https://3.basecamp.com/3700981/buckets/3107037/uploads/8902667883 ''' diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index 18a2c408..f6bf8df5 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -780,6 +780,27 @@ def syst_magnetic_field(self): else: return 0, 0 + def syst_plasma_effects(self): + if self.PlasmaEffects.UseFixedValue: + sigma = self.PlasmaEffects.Default_Systematic_Smearing + delta = self.PlasmaEffects.Default_Systematic_Uncertainty + return sigma, delta + elif not self.PlasmaEffects.UseFixedValue and self.Experiment.design_density_flag: + # C212 [s] - Differs from atomic calculator due to In + Es Crosssection for T-e at 18.6 keV and trap density + mean_track_duration = track_length(self.Experiment.number_density, self.T_endpoint, molecular=(not self.Experiment.atomic)) + #mean_track_duration = self.time_window + # C061 - Only used to estimate plasma broadening and mean track number present. Rough guess. + tracks_per_event = 10 + # C131 - Box trap approximation + min_pitch_angle_acceptance = 0.089 + #C217 - [eV] Plasma Broadening Calculation. Conservative upper limit based on dominance of 1 charge + sigma = 7.2e-10 * (calculate_inventory(self.Experiment.design_density, self.total_cavity_volume) * lambda_tritium / Ci_Bq) * mean_track_duration * tracks_per_event \ + * Ci_Bq * min_pitch_angle_acceptance * np.log(self.cavity_length / self.cavity_radius) / 2.35 * eV + delta = self.PlasmaEffects.Default_Systematic_Uncertainty + return sigma, delta + else: + return 0, 0 + def det_efficiency_track_duration(self): """ Detection efficiency implemented based on René's slides, with faster and stable implementation using Gauss-Laguerre quadrature (G-L method): From f67bbb3e43f9f9bd44f611376c13bb692111eb0a Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Tue, 3 Mar 2026 14:49:14 -0500 Subject: [PATCH 47/57] Updated documentation regarding magnetic field sigma --- documentation/sensitivity_configurable_parameters.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/documentation/sensitivity_configurable_parameters.rst b/documentation/sensitivity_configurable_parameters.rst index 5e9f3662..08608c44 100644 --- a/documentation/sensitivity_configurable_parameters.rst +++ b/documentation/sensitivity_configurable_parameters.rst @@ -97,6 +97,14 @@ We use the CRLB for calculating the frequency resolution. The CRLB is calculated * ``sigmae_theta``: Fixed input in eV. Energy broadening remaining after theta reconstruction, from electrons with lower pitch angles exploring high fields. Accounts for both the uncertainty on theta and uncertainties on the trap depth/boxiness. * ``sigmae_phi``: Fixed input in eV. Energy broadening from phi field inhomogeneity that remains after phi reconstruction. +Each variable corresponds to the energy resolution contribution from field variation along a particular spatial direction / time, after accounting for position reconstruction in that direction / time. Then fraction_uncertainty_on_field_broadening accounts for how well that field broadening is known/calculated. +Broken up between different position and time coordinates, for cases in which we want to study the effect of field variation along each coordinate individually, and then input the resulting resolution contributions into mermithid. Resolution contributions due to field variation along the different coordinates are added in quadrature. +Currently using the variable sigmae_r to account for the total resolution contribution from field variation + reconstruction/calculation in all coordinates. + +In the atomic calculator, the input is a field inhomogeneity value deltaB/B in ppm, instead of being a resolution contribution value in eV. Field inhomogeneity is converted to a resolution value using a simple B-->E formula, based on the Larmor formula. Used to have something similar in mermithid but scrapped that. +Don't love this approach because it implies that a certain physical field inhomogeneity corresponds to a certain resolution contribution via the Larmor formula and that's really not the case. Take, for example, radial variation. If there is no radial reconstruction, then to get the resolution contribution from the physical field variation, one needs to account for the fact that there are more electrons at higher radii, and that the detection efficiency depends on radius in some way. Those effects re-weight the impact of the field vs. radius profile. In addition, if there is radial reconstruction, that can further reduce the resolution relative to the physical radial field inhomogeneity, because we can use knowledge of electron radii to correct for the field vs. radius variation. So, there are several degrees of separation between physical field inhomogeneity and a resolution contribution. +Since we don't have models of these various effects in mermithid or the atomic calculator, prefer to just have us directly input the resolution contributions. Yes, one could take the 0.085eV number and convert it to a deltaB/B requirement via the Larmor formula, but that number doesn't really mean anything. + **FinalStates** * ``ground_state_width_uncertainty_fraction``: Uncertainty on the ground state width. Recommended to use 0.001. From 0df4c2b1f6693d4c20cf8472c2dcd8231b3a1266 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Wed, 24 Jun 2026 17:17:10 -0400 Subject: [PATCH 48/57] Initial edits to incldue T2 molecular background to atomic T sensitivity --- docker-compose.yaml | 8 +++++--- mermithid/misc/Constants_numericalunits.py | 5 ++++- mermithid/sensitivity/SensitivityFormulas.py | 14 +++++++++++--- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 72e69c20..306a3b8a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,11 +1,13 @@ #version: "3.9" -# Check running containers docker ps (detached) -d -# Attach to container docker attach [name] -# Mermithid is a bind mount, this guarantees: live code edits on host, Python imports always resolve to your branch & no accidental use of the installed site-packages version +# Check running containers docker ps (detached) -d #docker compose up -d + +# Attach to container docker attach [name] #docker attach mermithid_sensitivity + +# Mermithid is a bind mount, this guarantees: live code edits on host, Python imports always resolve to your branch & no accidental use of the installed site-packages version #export PYTHONPATH=/mermithid/:$PYTHONPATH && # /mermithid is on sys.path. Python should import from it but it does not, because mermithid is already imported earlier by ROOT’s import hook. Once a module is loaded, Python never re-resolves it. diff --git a/mermithid/misc/Constants_numericalunits.py b/mermithid/misc/Constants_numericalunits.py index 1ec5fd87..e73ef687 100644 --- a/mermithid/misc/Constants_numericalunits.py +++ b/mermithid/misc/Constants_numericalunits.py @@ -59,6 +59,8 @@ ground_state_branch_atomic = 0.702 # C032 - Ground state branch (molecular) ground_state_branch_molecular = 0.570 +# C035 - [eV] Difference between atomic and effective molecular endpoints, effective difference 10-2 eV +endpoint_diff = 8*eV # C036 - [eV] Recoil energy of tritium atom (Bodine) atomic_tritium_recoil_energy = 3.409*eV # C038 - [eV] Binding energy of tritium molecule (Bodine) @@ -83,7 +85,8 @@ area_atom_loading_aperture = 0.001 * m**2 # C118 - [Ci] Total activity on wall at recyling. Ci (Bq = 1 decay per second) wall_activity = 100 - +# C211 - CALCULATION [s^-1] Atomic Activity in last eV of spectrum +last_1eV_activity_atomic = 5.42e-7/s # units that do not show up in numericalunits # missing pre-factors diff --git a/mermithid/sensitivity/SensitivityFormulas.py b/mermithid/sensitivity/SensitivityFormulas.py index 23489aff..183e9960 100755 --- a/mermithid/sensitivity/SensitivityFormulas.py +++ b/mermithid/sensitivity/SensitivityFormulas.py @@ -66,6 +66,7 @@ ''' from mermithid.misc.Constants_numericalunits import * from mermithid.misc.CRESFunctions_numericalunits import * +from mermithid.cavity.AtomicCalculator import * try: from morpho.utilities import morphologging @@ -108,7 +109,7 @@ def __init__(self, config_path): self.Experiment = NameSpace({opt: eval(self.cfg.get('Experiment', opt)) for opt in self.cfg.options('Experiment')}) - # seetings fro molecular or atomic tritium + # settings for molecular or atomic tritium self.tau_tritium = tritium_livetime if self.Experiment.atomic: self.T_mass = tritium_mass_atomic @@ -173,7 +174,14 @@ def BackgroundEvents(self): def DeltaEWidth(self): """optimal energy bin width""" labels, sigmas, deltas = self.get_systematics() - return np.sqrt(self.BackgroundRate()/self.SignalRate() + if self.Efficiency.T2_background_atomic_trap: + self.T2_total_density, self.T2_T_ratio = calculate_T2_background_atomic_trap(self.cavity_radius, self.cavity_length, self.FrequencyExtraction.cavity_temperature, self.Efficiency.max_ratio_T2_T, self.Experiment.number_density) + # C240 - Activity ratio of T2/T + self.T2_T_activity = self.T2_T_ratio * 2 / ground_state_branch_atomic + return np.sqrt((self.BackgroundRate() + 3 * endpoint_diff**2 * last_1eV_activity_atomic * self.T2_T_activity) / (last_1eV_activity_atomic * (1 + self.T2_T_activity)) + + 8*np.log(2)*(np.sum(sigmas**2))) + else: + return np.sqrt(self.BackgroundRate()/self.SignalRate() + 8*np.log(2)*(np.sum(sigmas**2))) def StatSens(self): @@ -481,4 +489,4 @@ def syst_plasma_effects(self): delta = self.PlasmaEffects.Default_Systematic_Uncertainty return sigma, delta else: - raise NotImplementedError("Plasma effect sysstematic is not implemented.") + raise NotImplementedError("Plasma effect systematic is not implemented.") From 9cdc20adcbf43813e9f5164fe1661d8deb8142bd Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Fri, 26 Jun 2026 16:48:04 -0400 Subject: [PATCH 49/57] Edited Delta E window to include T2 background in signal rate --- mermithid/cavity/AtomicCalculator.py | 14 +++++++++++ mermithid/misc/Constants_numericalunits.py | 7 +++--- .../sensitivity/SensitivityCavityFormulas.py | 5 ++-- mermithid/sensitivity/SensitivityFormulas.py | 25 ++++++++++++------- 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/mermithid/cavity/AtomicCalculator.py b/mermithid/cavity/AtomicCalculator.py index 75feb654..dbb0a2b3 100644 --- a/mermithid/cavity/AtomicCalculator.py +++ b/mermithid/cavity/AtomicCalculator.py @@ -262,3 +262,17 @@ def calculate_injection_line(atom_current, cavity_radius, design_density, trappe # C356 - [m^-1] Beamline dipolar loss beamline_dipolar = injection_dipolar_loss_rate * injection_density / vertical_aperture_speed return vertical_injection_speed, trapped_gas_energy, injection_density, vertical_aperture_speed, injection_gas_energy + + +def calculate_activity_last_1eV_spectrum(self, atomic_flag, number_density, cavity_radius, trap_coil_1, trap_coil_2, total_efficiency): + if atomic_flag: + trap_activity = calculate_activity_in_trap(number_density, cavity_radius, trap_coil_1, trap_coil_2) + # C209 - [decay/s] Activity in last 100 eV of spectrum * net efficiency + # Note last_1eV_fraction_atomic is the fraction of events in the last 1eV + activity_last_100eV_efficiency = trap_activity * last_1ev_fraction_atomic * total_efficiency * 1000000 / ground_state_branch_atomic + # C211 - [decay/s] + last_1eV_atomic = activity_last_100eV_efficiency * ground_state_branch_atomic / 1000000 + return last_1eV_atomic + else: + print("Yeah I didn't do that yet - Jin") + return None diff --git a/mermithid/misc/Constants_numericalunits.py b/mermithid/misc/Constants_numericalunits.py index e73ef687..7e3d97c3 100644 --- a/mermithid/misc/Constants_numericalunits.py +++ b/mermithid/misc/Constants_numericalunits.py @@ -1,3 +1,4 @@ +# C033 - Bayesian Paper (unitless) ''' Some constants useful for various things... The constants here use the numericalunits package. For constants not using this package import form Constants.py @@ -20,11 +21,13 @@ #tritium_electron_crosssection_atomic = 1.32e-22*m**2 #Inelastic cross-section + Elastic cross-section for T-e tritium_tritium_crosssection_atomic = 4.40e-16*m**2 # T-T cross-section at T=0 tritium_endpoint_atomic = 18563.251*eV +# C033 - [eV^-3] Bayesian Paper last_1ev_fraction_atomic = 2.067914e-13/eV**3 tritium_mass_molecular = 6.032099 * amu *c0**2 tritium_electron_crosssection_molecular = 3.67*1e-22*m**2 #[Inelastic from Aseev (2000) for T2] + [Elastic from Liu (1987) for H2, extrapolated by Elise to 18.6keV] tritium_endpoint_molecular = 18574.01*eV +# C034 - [eV^-3] Bayesian Paper last_1ev_fraction_molecular = 1.67364e-13/eV**3 ground_state_width = 0.436 * eV @@ -60,7 +63,7 @@ # C032 - Ground state branch (molecular) ground_state_branch_molecular = 0.570 # C035 - [eV] Difference between atomic and effective molecular endpoints, effective difference 10-2 eV -endpoint_diff = 8*eV +endpoint_diff = tritium_endpoint_molecular - tritium_endpoint_atomic # C036 - [eV] Recoil energy of tritium atom (Bodine) atomic_tritium_recoil_energy = 3.409*eV # C038 - [eV] Binding energy of tritium molecule (Bodine) @@ -85,8 +88,6 @@ area_atom_loading_aperture = 0.001 * m**2 # C118 - [Ci] Total activity on wall at recyling. Ci (Bq = 1 decay per second) wall_activity = 100 -# C211 - CALCULATION [s^-1] Atomic Activity in last eV of spectrum -last_1eV_activity_atomic = 5.42e-7/s # units that do not show up in numericalunits # missing pre-factors diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index f6bf8df5..0e9f2464 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -793,7 +793,8 @@ def syst_plasma_effects(self): tracks_per_event = 10 # C131 - Box trap approximation min_pitch_angle_acceptance = 0.089 - #C217 - [eV] Plasma Broadening Calculation. Conservative upper limit based on dominance of 1 charge + # C217 - [eV] Plasma Broadening Calculation. Conservative upper limit based on dominance of 1 charge + # 2.35 converts stddev to FWHM sigma = 7.2e-10 * (calculate_inventory(self.Experiment.design_density, self.total_cavity_volume) * lambda_tritium / Ci_Bq) * mean_track_duration * tracks_per_event \ * Ci_Bq * min_pitch_angle_acceptance * np.log(self.cavity_length / self.cavity_radius) / 2.35 * eV delta = self.PlasmaEffects.Default_Systematic_Uncertainty @@ -972,7 +973,7 @@ def print_pumping_requirements(self): self.time_constant_aperture, self.current_aperture_leak = calculate_aperture_heat_leak(self.DopplerBroadening.gas_temperature, self.Experiment.design_density, self.total_cavity_volume) logger.info("Aperture Time Constant: {:.4} s".format(self.time_constant_aperture/s)) logger.info("Atom current required for aperture leak: {:.4e} atoms/s".format(self.current_aperture_leak*s)) - self.time_constant_rad, self.current_rad_leak = calculate_rad_heat_leak(self.cavity_radius, self.Experiment.number_density, self.trap_coil_1, self.trap_coil_2, self.Experiment.design_density, self.total_cavity_volume, self.Efficiency.net_rad_efficiency) + self.time_constant_rad, self.current_rad_leak = calculate_rad_heat_leak(self.cavity_radius, self.Experiment.number_density, self.trap_coil_1, self.trap_coil_2, self.Experiment.design_density, self.total_cavity_volume, self.Efficiency.total_efficiency) logger.info("Radiation Time Constant: {:.4} s".format(self.time_constant_rad/s)) logger.info("Atom current required for radiation leak: {:.4e} atoms/s".format(self.current_rad_leak*s)) self.time_constant_desorp, self.current_desorp = calculate_T2_desorption_from_wall(self.cavity_radius, self.cavity_length, self.Experiment.design_density, self.total_cavity_volume) diff --git a/mermithid/sensitivity/SensitivityFormulas.py b/mermithid/sensitivity/SensitivityFormulas.py index 183e9960..0ee110db 100755 --- a/mermithid/sensitivity/SensitivityFormulas.py +++ b/mermithid/sensitivity/SensitivityFormulas.py @@ -144,16 +144,16 @@ def __init__(self, config_path): def SignalRate(self): """signal events in the energy interval before the endpoint, scale with DeltaE**3""" self.EffectiveVolume() - signal_rate = self.Experiment.number_density*self.effective_volume*self.last_1ev_fraction/self.tau_tritium + self.signal_rate = self.Experiment.number_density*self.effective_volume*self.last_1ev_fraction/self.tau_tritium if not self.Experiment.atomic: if hasattr(self.Experiment, 'gas_fractions'): avg_n_T_atoms = self.AvgNumTAtomsPerParticle_MolecularExperiment(self.Experiment.gas_fractions, self.Experiment.H2_type_gas_fractions) - signal_rate *= avg_n_T_atoms + self.signal_rate *= avg_n_T_atoms else: - signal_rate *= 2 + self.signal_rate *= 2 if hasattr(self.Experiment, 'active_gas_fraction'): - signal_rate *= self.Experiment.active_gas_fraction - return signal_rate + self.signal_rate *= self.Experiment.active_gas_fraction + return self.signal_rate def BackgroundRate(self): """background rate, can be calculated from multiple components. @@ -171,14 +171,21 @@ def BackgroundEvents(self): """Number of background events.""" return self.BackgroundRate()*self.Experiment.LiveTime*self.DeltaEWidth() + def SignalRatio(self): + self.T2_total_density, self.T2_T_ratio = calculate_T2_background_atomic_trap(self.cavity_radius, self.cavity_length, self.FrequencyExtraction.cavity_temperature, self.Efficiency.max_ratio_T2_T, self.Experiment.number_density) + # C240 - Activity ratio of T2/T + self.signal_ratio = self.T2_T_ratio * 2 / ground_state_branch_atomic + """ + # Atomic Calculator activity in last eV of spectrum + sig_rate = calculate_activity_last_1eV_spectrum(self.Experiment.atomic, self.Experiment.number_density, self.cavity_radius, self.trap_coil_1, self.trap_coil_2, self.Efficiency.total_efficiency): + """ + return self.signal_ratio + def DeltaEWidth(self): """optimal energy bin width""" labels, sigmas, deltas = self.get_systematics() if self.Efficiency.T2_background_atomic_trap: - self.T2_total_density, self.T2_T_ratio = calculate_T2_background_atomic_trap(self.cavity_radius, self.cavity_length, self.FrequencyExtraction.cavity_temperature, self.Efficiency.max_ratio_T2_T, self.Experiment.number_density) - # C240 - Activity ratio of T2/T - self.T2_T_activity = self.T2_T_ratio * 2 / ground_state_branch_atomic - return np.sqrt((self.BackgroundRate() + 3 * endpoint_diff**2 * last_1eV_activity_atomic * self.T2_T_activity) / (last_1eV_activity_atomic * (1 + self.T2_T_activity)) + return np.sqrt((self.BackgroundRate() + 3 * endpoint_diff**2 * self.SignalRate() * self.SignalRatio()) / (self.SignalRate() * (1 + self.SignalRatio())) + 8*np.log(2)*(np.sum(sigmas**2))) else: return np.sqrt(self.BackgroundRate()/self.SignalRate() From 9cc8bd7e8e283408b1c5a8e2dd1f7c5d3391f182 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Mon, 29 Jun 2026 15:44:55 -0400 Subject: [PATCH 50/57] Sensitivity Calculation edit of stats to include molecular bkg --- .../CavitySensitivityCurveProcessor.py | 4 +-- mermithid/sensitivity/SensitivityFormulas.py | 29 ++++++++++++++----- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/mermithid/processors/Sensitivity/CavitySensitivityCurveProcessor.py b/mermithid/processors/Sensitivity/CavitySensitivityCurveProcessor.py index 18d8c7f9..21d342d6 100644 --- a/mermithid/processors/Sensitivity/CavitySensitivityCurveProcessor.py +++ b/mermithid/processors/Sensitivity/CavitySensitivityCurveProcessor.py @@ -71,7 +71,7 @@ def InternalConfigure(self, params): # options self.optimize_main_density = reader.read_param(params, 'optimize_main_density', True) self.optimize_comparison_density = reader.read_param(params, 'optimize_comparison_density', True) - self.verbose = reader.read_param(params, 'verbose', True) + self.verbose = reader.read_param(params, 'verbose', False) self.comparison_curve = reader.read_param(params, 'comparison_curve', False) self.B_error = reader.read_param(params, 'B_inhomogeneity', 7e-6) self.B_error_uncertainty = reader.read_param(params, 'B_inhom_uncertainty', 0.05) @@ -592,7 +592,7 @@ def print_disclaimers(self): scenarios, if the minimum allowed density is 1e-20 atoms/m^3, the optimization \ over density still works.") logger.info("Once you have read these disclaimers and are familiar with them, you can set \ - verbose==False in your config dictionary to stop seeing them.") + verbose==False in your config dictionary (CavitySensitivtyCurveProcessor or test_anaylsis/) to stop seeing them.") def create_plot(self): diff --git a/mermithid/sensitivity/SensitivityFormulas.py b/mermithid/sensitivity/SensitivityFormulas.py index 0ee110db..0011044a 100755 --- a/mermithid/sensitivity/SensitivityFormulas.py +++ b/mermithid/sensitivity/SensitivityFormulas.py @@ -184,20 +184,35 @@ def SignalRatio(self): def DeltaEWidth(self): """optimal energy bin width""" labels, sigmas, deltas = self.get_systematics() + sig_rate = self.SignalRate() + bkg_rate = self.BackgroundRate() + zeta = self.SignalRatio() if self.Efficiency.T2_background_atomic_trap: - return np.sqrt((self.BackgroundRate() + 3 * endpoint_diff**2 * self.SignalRate() * self.SignalRatio()) / (self.SignalRate() * (1 + self.SignalRatio())) - + 8*np.log(2)*(np.sum(sigmas**2))) - else: - return np.sqrt(self.BackgroundRate()/self.SignalRate() - + 8*np.log(2)*(np.sum(sigmas**2))) + # zeta = molecular background / atomic signal (r_m / r_a); if r_m = 0 then zeta = 0 and equation reduces to else statement + # delta_E = sqrt((b + 3 * delta_endpoint * zeta * r_a) / ((1 + zeta) * r_a) + 8*ln(2) * Systematics) + return np.sqrt((bkg_rate + 3 * endpoint_diff**2 * sig_rate * zeta) / (sig_rate * (1 + zeta)) + + 8*np.log(2)*(np.sum(sigmas**2))) + + elif np.isclose(zeta, 0.0, atol=1e-12): + return np.sqrt(bkg_rate/sig_rate + 8*np.log(2)*(np.sum(sigmas**2))) def StatSens(self): """Pure statistic sensitivity assuming Poisson count experiment in a single bin As defined, it needs to be squared before being added to the systematic component""" + bkg_rate = self.BackgroundRate() sig_rate = self.SignalRate() DeltaE = self.DeltaEWidth() - sens = 2/(3*sig_rate*self.Experiment.LiveTime)*np.sqrt(sig_rate*self.Experiment.LiveTime*DeltaE - +self.BackgroundRate()*self.Experiment.LiveTime/DeltaE) + zeta = self.SignalRatio() + n_cavities = self.Experiment.n_cavities + # Stat^2 = 4/(9*r_a*t*#_cav) * ((1 + zeta)*delta_E + b*#_cav/r_a/delta_E + 3*#_cav*zeta*delta_endpoint*(1 + delta_endpoint/delta_E)) + if self.Efficiency.T2_background_atomic_trap: + sens = 2/(3*sig_rate*self.Experiment.LiveTime*n_cavities)*np.sqrt(sig_rate*self.Experiment.LiveTime*n_cavities*(1+zeta)*DeltaE + + bkg_rate*(n_cavities**2*self.Experiment.LiveTime/DeltaE) + 3*n_cavities**2*zeta*endpoint_diff*sig_rate* + self.Experiment.LiveTime*(1 + endpoint_diff/DeltaE) + # Stat^2 = 4/(9*r_a*t) * (delta_E + b/r_a/delta_E) if zeta=0 + elif np.isclose(zeta, 0.0, atol=1e-12): + sens = 2/(3*sig_rate*self.Experiment.LiveTime)*np.sqrt(sig_rate*self.Experiment.LiveTime*DeltaE + + bkg_rate*self.Experiment.LiveTime/DeltaE) return sens def SystSens(self): From 275e8fef9c9d63dec89336603fc191d31b475a34 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Mon, 29 Jun 2026 16:29:26 -0400 Subject: [PATCH 51/57] Finished T2 bkg integration into Stat Sensitivity Formula --- mermithid/sensitivity/SensitivityFormulas.py | 42 +++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/mermithid/sensitivity/SensitivityFormulas.py b/mermithid/sensitivity/SensitivityFormulas.py index 0011044a..1cc9b283 100755 --- a/mermithid/sensitivity/SensitivityFormulas.py +++ b/mermithid/sensitivity/SensitivityFormulas.py @@ -172,14 +172,16 @@ def BackgroundEvents(self): return self.BackgroundRate()*self.Experiment.LiveTime*self.DeltaEWidth() def SignalRatio(self): - self.T2_total_density, self.T2_T_ratio = calculate_T2_background_atomic_trap(self.cavity_radius, self.cavity_length, self.FrequencyExtraction.cavity_temperature, self.Efficiency.max_ratio_T2_T, self.Experiment.number_density) - # C240 - Activity ratio of T2/T - self.signal_ratio = self.T2_T_ratio * 2 / ground_state_branch_atomic - """ - # Atomic Calculator activity in last eV of spectrum - sig_rate = calculate_activity_last_1eV_spectrum(self.Experiment.atomic, self.Experiment.number_density, self.cavity_radius, self.trap_coil_1, self.trap_coil_2, self.Efficiency.total_efficiency): - """ - return self.signal_ratio + if self.Efficiency.T2_background_atomic_trap: + self.T2_total_density, self.T2_T_ratio = calculate_T2_background_atomic_trap(self.cavity_radius, self.cavity_length, self.FrequencyExtraction.cavity_temperature, self.Efficiency.max_ratio_T2_T, self.Experiment.number_density) + # C240 - Activity ratio of T2/T + signal_ratio = self.T2_T_ratio * 2 / ground_state_branch_atomic + """ + # Atomic Calculator activity in last eV of spectrum + sig_rate = calculate_activity_last_1eV_spectrum(self.Experiment.atomic, self.Experiment.number_density, self.cavity_radius, self.trap_coil_1, self.trap_coil_2, self.Efficiency.total_efficiency): + """ + return signal_ratio + return None def DeltaEWidth(self): """optimal energy bin width""" @@ -187,14 +189,16 @@ def DeltaEWidth(self): sig_rate = self.SignalRate() bkg_rate = self.BackgroundRate() zeta = self.SignalRatio() + #logger.info(f"zeta value: {repr(zeta)}") + #logger.info(f"zeta type: {type(zeta)}") + #logger.info(f"zeta dtype: {getattr(zeta, 'dtype', None)}") + #logger.info(f"zeta finite check: {np.isfinite(zeta)}") if self.Efficiency.T2_background_atomic_trap: # zeta = molecular background / atomic signal (r_m / r_a); if r_m = 0 then zeta = 0 and equation reduces to else statement # delta_E = sqrt((b + 3 * delta_endpoint * zeta * r_a) / ((1 + zeta) * r_a) + 8*ln(2) * Systematics) - return np.sqrt((bkg_rate + 3 * endpoint_diff**2 * sig_rate * zeta) / (sig_rate * (1 + zeta)) + return np.sqrt((bkg_rate + 3 * endpoint_diff**2 * sig_rate * zeta) / (sig_rate * (1 + zeta)) \ + 8*np.log(2)*(np.sum(sigmas**2))) - - elif np.isclose(zeta, 0.0, atol=1e-12): - return np.sqrt(bkg_rate/sig_rate + 8*np.log(2)*(np.sum(sigmas**2))) + return np.sqrt(bkg_rate/sig_rate + 8*np.log(2)*(np.sum(sigmas**2))) def StatSens(self): """Pure statistic sensitivity assuming Poisson count experiment in a single bin @@ -204,15 +208,15 @@ def StatSens(self): DeltaE = self.DeltaEWidth() zeta = self.SignalRatio() n_cavities = self.Experiment.n_cavities - # Stat^2 = 4/(9*r_a*t*#_cav) * ((1 + zeta)*delta_E + b*#_cav/r_a/delta_E + 3*#_cav*zeta*delta_endpoint*(1 + delta_endpoint/delta_E)) if self.Efficiency.T2_background_atomic_trap: - sens = 2/(3*sig_rate*self.Experiment.LiveTime*n_cavities)*np.sqrt(sig_rate*self.Experiment.LiveTime*n_cavities*(1+zeta)*DeltaE - + bkg_rate*(n_cavities**2*self.Experiment.LiveTime/DeltaE) + 3*n_cavities**2*zeta*endpoint_diff*sig_rate* - self.Experiment.LiveTime*(1 + endpoint_diff/DeltaE) + # Stat^2 = 4/(9*r_a*t*#_cav) * ((1 + zeta)*delta_E + b*#_cav/r_a/delta_E + 3*#_cav*zeta*delta_endpoint*(1 + delta_endpoint/delta_E)) + sens = 2/(3*sig_rate*self.Experiment.LiveTime*n_cavities)*np.sqrt(sig_rate*self.Experiment.LiveTime*n_cavities*(1+zeta)*DeltaE \ + + bkg_rate*(n_cavities**2*self.Experiment.LiveTime/DeltaE) + 3*n_cavities**2*zeta*endpoint_diff*sig_rate \ + * self.Experiment.LiveTime*(1 + endpoint_diff/DeltaE)) + return sens # Stat^2 = 4/(9*r_a*t) * (delta_E + b/r_a/delta_E) if zeta=0 - elif np.isclose(zeta, 0.0, atol=1e-12): - sens = 2/(3*sig_rate*self.Experiment.LiveTime)*np.sqrt(sig_rate*self.Experiment.LiveTime*DeltaE - + bkg_rate*self.Experiment.LiveTime/DeltaE) + sens = 2/(3*sig_rate*self.Experiment.LiveTime)*np.sqrt(sig_rate*self.Experiment.LiveTime*DeltaE \ + + bkg_rate*self.Experiment.LiveTime/DeltaE) return sens def SystSens(self): From 2df7bb26a00a183873759d9c3fa9f05791195b9d Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Mon, 29 Jun 2026 16:31:17 -0400 Subject: [PATCH 52/57] Small minor change of T2 bkg implementation --- mermithid/sensitivity/SensitivityFormulas.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mermithid/sensitivity/SensitivityFormulas.py b/mermithid/sensitivity/SensitivityFormulas.py index 1cc9b283..9b1730b8 100755 --- a/mermithid/sensitivity/SensitivityFormulas.py +++ b/mermithid/sensitivity/SensitivityFormulas.py @@ -213,9 +213,9 @@ def StatSens(self): sens = 2/(3*sig_rate*self.Experiment.LiveTime*n_cavities)*np.sqrt(sig_rate*self.Experiment.LiveTime*n_cavities*(1+zeta)*DeltaE \ + bkg_rate*(n_cavities**2*self.Experiment.LiveTime/DeltaE) + 3*n_cavities**2*zeta*endpoint_diff*sig_rate \ * self.Experiment.LiveTime*(1 + endpoint_diff/DeltaE)) - return sens - # Stat^2 = 4/(9*r_a*t) * (delta_E + b/r_a/delta_E) if zeta=0 - sens = 2/(3*sig_rate*self.Experiment.LiveTime)*np.sqrt(sig_rate*self.Experiment.LiveTime*DeltaE \ + else: + # Stat^2 = 4/(9*r_a*t) * (delta_E + b/r_a/delta_E) if zeta=0 + sens = 2/(3*sig_rate*self.Experiment.LiveTime)*np.sqrt(sig_rate*self.Experiment.LiveTime*DeltaE \ + bkg_rate*self.Experiment.LiveTime/DeltaE) return sens From 23132e762c2840862d52baec212969fd2f7e7c13 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Tue, 30 Jun 2026 16:06:45 -0400 Subject: [PATCH 53/57] Added comments and moved AtomicCalculator.py --- mermithid/{cavity => sensitivity}/AtomicCalculator.py | 0 mermithid/sensitivity/SensitivityCavityFormulas.py | 2 +- mermithid/sensitivity/SensitivityFormulas.py | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename mermithid/{cavity => sensitivity}/AtomicCalculator.py (100%) diff --git a/mermithid/cavity/AtomicCalculator.py b/mermithid/sensitivity/AtomicCalculator.py similarity index 100% rename from mermithid/cavity/AtomicCalculator.py rename to mermithid/sensitivity/AtomicCalculator.py diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index 0e9f2464..b8250368 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -15,7 +15,7 @@ from mermithid.misc.Constants_numericalunits import * from mermithid.misc.CRESFunctions_numericalunits import * from mermithid.cavity.HannekeFunctions import * -from mermithid.cavity.AtomicCalculator import * +from mermithid.sensitivity.AtomicCalculator import * from mermithid.sensitivity.SensitivityFormulas import * diff --git a/mermithid/sensitivity/SensitivityFormulas.py b/mermithid/sensitivity/SensitivityFormulas.py index 9b1730b8..b59cbbe1 100755 --- a/mermithid/sensitivity/SensitivityFormulas.py +++ b/mermithid/sensitivity/SensitivityFormulas.py @@ -66,7 +66,7 @@ ''' from mermithid.misc.Constants_numericalunits import * from mermithid.misc.CRESFunctions_numericalunits import * -from mermithid.cavity.AtomicCalculator import * +from mermithid.sensitivity.AtomicCalculator import * try: from morpho.utilities import morphologging @@ -174,7 +174,7 @@ def BackgroundEvents(self): def SignalRatio(self): if self.Efficiency.T2_background_atomic_trap: self.T2_total_density, self.T2_T_ratio = calculate_T2_background_atomic_trap(self.cavity_radius, self.cavity_length, self.FrequencyExtraction.cavity_temperature, self.Efficiency.max_ratio_T2_T, self.Experiment.number_density) - # C240 - Activity ratio of T2/T + # C240 - Activity ratio of the rate of T2/T events in the last eV of the T2/T spectrum. (Difference between the two spectra) signal_ratio = self.T2_T_ratio * 2 / ground_state_branch_atomic """ # Atomic Calculator activity in last eV of spectrum From 0036e86dc43f60b190f36ae73d7106e98886457a Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Wed, 1 Jul 2026 12:12:42 -0400 Subject: [PATCH 54/57] Added flag for fixed T2/T ratio and change usage of fixed total efficiency --- mermithid/sensitivity/SensitivityCavityFormulas.py | 8 +++++++- mermithid/sensitivity/SensitivityFormulas.py | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/mermithid/sensitivity/SensitivityCavityFormulas.py b/mermithid/sensitivity/SensitivityCavityFormulas.py index b8250368..5ee07732 100755 --- a/mermithid/sensitivity/SensitivityCavityFormulas.py +++ b/mermithid/sensitivity/SensitivityCavityFormulas.py @@ -950,6 +950,8 @@ def print_T2_background_atomic_trap(self): #logger.info("T2 background: {}".format(self.Efficiency.T2_background_atomic_trap)) if self.Efficiency.T2_background_atomic_trap: self.T2_total_density, self.T2_T_ratio = calculate_T2_background_atomic_trap(self.cavity_radius, self.cavity_length, self.FrequencyExtraction.cavity_temperature, self.Efficiency.max_ratio_T2_T, self.Experiment.number_density) + if self.Efficiency.usefixedratio: + self.T2_T_ratio = self.Efficiency.T2_T_ratio logger.info("T2_total_density: {:.4e} m^-3".format(self.T2_total_density*m**3)) logger.info("Ratio T2/T: {:.4e}".format(self.T2_T_ratio)) @@ -967,13 +969,17 @@ def print_pumping_requirements(self): logger.info("Pumping Speed Limit (Molecular): {:.4} m^3/s".format(self.turbopump_speed_limit_molecular * s / m**3)) logger.info("Cavity Termination Speed (Molecular): {:.4} m^3/s".format(self.cavity_termination_speed_molecular * s / m**3)) if self.Efficiency.T_atom_supply_trap: + if self.Efficiency.usefixedvalue: + self.total_efficiency = self.Efficiency.fixed_efficiency + else: + self.total_efficiency = self.effective_volume/self.total_trap_volume self.time_constant_He, self.current_He_leak = calculate_He_heat_leak(self.turbopump_speed_limit_atomic, self.cavity_termination_speed_atomic, self.turbopump_speed, self.FrequencyExtraction.cavity_temperature, self.Experiment.design_density, self.total_cavity_volume) logger.info("He Time Constant: {:.4} s".format(self.time_constant_He/s)) logger.info("Atom current required for He-3 leak: {:.4e} atoms/s".format(self.current_He_leak*s)) self.time_constant_aperture, self.current_aperture_leak = calculate_aperture_heat_leak(self.DopplerBroadening.gas_temperature, self.Experiment.design_density, self.total_cavity_volume) logger.info("Aperture Time Constant: {:.4} s".format(self.time_constant_aperture/s)) logger.info("Atom current required for aperture leak: {:.4e} atoms/s".format(self.current_aperture_leak*s)) - self.time_constant_rad, self.current_rad_leak = calculate_rad_heat_leak(self.cavity_radius, self.Experiment.number_density, self.trap_coil_1, self.trap_coil_2, self.Experiment.design_density, self.total_cavity_volume, self.Efficiency.total_efficiency) + self.time_constant_rad, self.current_rad_leak = calculate_rad_heat_leak(self.cavity_radius, self.Experiment.number_density, self.trap_coil_1, self.trap_coil_2, self.Experiment.design_density, self.total_cavity_volume, self.total_efficiency) logger.info("Radiation Time Constant: {:.4} s".format(self.time_constant_rad/s)) logger.info("Atom current required for radiation leak: {:.4e} atoms/s".format(self.current_rad_leak*s)) self.time_constant_desorp, self.current_desorp = calculate_T2_desorption_from_wall(self.cavity_radius, self.cavity_length, self.Experiment.design_density, self.total_cavity_volume) diff --git a/mermithid/sensitivity/SensitivityFormulas.py b/mermithid/sensitivity/SensitivityFormulas.py index b59cbbe1..e72ac3e1 100755 --- a/mermithid/sensitivity/SensitivityFormulas.py +++ b/mermithid/sensitivity/SensitivityFormulas.py @@ -174,11 +174,17 @@ def BackgroundEvents(self): def SignalRatio(self): if self.Efficiency.T2_background_atomic_trap: self.T2_total_density, self.T2_T_ratio = calculate_T2_background_atomic_trap(self.cavity_radius, self.cavity_length, self.FrequencyExtraction.cavity_temperature, self.Efficiency.max_ratio_T2_T, self.Experiment.number_density) + if self.Efficiency.usefixedratio: + self.T2_T_ratio = self.Efficiency.T2_T_ratio # C240 - Activity ratio of the rate of T2/T events in the last eV of the T2/T spectrum. (Difference between the two spectra) signal_ratio = self.T2_T_ratio * 2 / ground_state_branch_atomic """ # Atomic Calculator activity in last eV of spectrum - sig_rate = calculate_activity_last_1eV_spectrum(self.Experiment.atomic, self.Experiment.number_density, self.cavity_radius, self.trap_coil_1, self.trap_coil_2, self.Efficiency.total_efficiency): + if self.Efficiency.usefixedvalue: + self.total_efficiency = self.Efficiency.fixed_efficiency + else: + self.total_efficiency = self.effective_volume/self.total_trap_volume + sig_rate = calculate_activity_last_1eV_spectrum(self.Experiment.atomic, self.Experiment.number_density, self.cavity_radius, self.trap_coil_1, self.trap_coil_2, self.total_efficiency): """ return signal_ratio return None From 2f78c3a8ca8e5f4d6c36e2ad2adf33ca11c244a8 Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Wed, 15 Jul 2026 11:22:10 -0400 Subject: [PATCH 55/57] Added molecular activity in last eV of spectrum option --- .../sensitivity/.SensitivityFormulas.py.swp | Bin 0 -> 1024 bytes mermithid/sensitivity/AtomicCalculator.py | 8 +++++--- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 mermithid/sensitivity/.SensitivityFormulas.py.swp diff --git a/mermithid/sensitivity/.SensitivityFormulas.py.swp b/mermithid/sensitivity/.SensitivityFormulas.py.swp new file mode 100644 index 0000000000000000000000000000000000000000..5915625b42d62022ecdeaf0e29792d507c7707d3 GIT binary patch literal 1024 zcmYc?$V<%2S1{KzVn6|%tPBjf`K77FIf*FZ*f^e{x-KE%VaDzz*klo6!Kr!0nI)NJ enI)BO`9-; Date: Wed, 15 Jul 2026 11:28:00 -0400 Subject: [PATCH 56/57] Added molecular activity case in last eV of spectrum --- mermithid/sensitivity/AtomicCalculator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mermithid/sensitivity/AtomicCalculator.py b/mermithid/sensitivity/AtomicCalculator.py index c97148fd..e1f71e46 100644 --- a/mermithid/sensitivity/AtomicCalculator.py +++ b/mermithid/sensitivity/AtomicCalculator.py @@ -277,4 +277,3 @@ def calculate_activity_last_1eV_spectrum(self, atomic_flag, number_density, cavi activity_last_100eV_efficiency = trap_activity * last_1ev_fraction_molecular * total_efficiency * 1000000 / ground_state_branch_molecular last_1eV_molecular = activity_last_100eV_efficiency * ground_state_branch_molecular / 1000000 return last_1eV_molecular - From cfc689c86b951bd3c523dc26126ff545fde506da Mon Sep 17 00:00:00 2001 From: ManjinderOueslati Date: Wed, 15 Jul 2026 14:31:48 -0400 Subject: [PATCH 57/57] Added comment for information of last eV fraction --- mermithid/misc/FakeTritiumDataFunctions.py | 2 +- .../sensitivity/.SensitivityFormulas.py.swp | Bin 1024 -> 0 bytes mermithid/sensitivity/SensitivityFormulas.py | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 mermithid/sensitivity/.SensitivityFormulas.py.swp diff --git a/mermithid/misc/FakeTritiumDataFunctions.py b/mermithid/misc/FakeTritiumDataFunctions.py index 4bd26fe2..cbaa5cdc 100644 --- a/mermithid/misc/FakeTritiumDataFunctions.py +++ b/mermithid/misc/FakeTritiumDataFunctions.py @@ -341,7 +341,7 @@ def convolved_bkgd_rate_arrays(K, Kmin, Kmax, lineshape, ls_params, min_energy, return convolved - +# https://3.basecamp.com/3700981/buckets/3107037/documents/10099116348 ##Fraction of events near the endpoint ##Currently, this only holds for the last 13.6 eV of the spectrum #def frac_near_endpt(Kmin, Q, mass, atom_or_mol='atom'): diff --git a/mermithid/sensitivity/.SensitivityFormulas.py.swp b/mermithid/sensitivity/.SensitivityFormulas.py.swp deleted file mode 100644 index 5915625b42d62022ecdeaf0e29792d507c7707d3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1024 zcmYc?$V<%2S1{KzVn6|%tPBjf`K77FIf*FZ*f^e{x-KE%VaDzz*klo6!Kr!0nI)NJ enI)BO`9-;