|
| 1 | +--- |
| 2 | +jupyter: |
| 3 | + jupytext: |
| 4 | + text_representation: |
| 5 | + extension: .md |
| 6 | + format_name: markdown |
| 7 | + format_version: '1.3' |
| 8 | + jupytext_version: 1.17.1 |
| 9 | + kernelspec: |
| 10 | + display_name: Python 3 |
| 11 | + name: python3 |
| 12 | +--- |
| 13 | + |
| 14 | +<a href="https://colab.research.google.com/github/project-ida/arpa-e-experiments/blob/neutrons-background-2/tutorials/He3-Background-Characterization.ipynb" target="_parent"><img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"/></a> <a href="https://nbviewer.org/github/project-ida/arpa-e-experiments/blob/neutrons-background-2/tutorials/He3-Background-Characterization.ipynb" target="_parent"><img src="https://nbviewer.org/static/img/nav_logo.svg" alt="Open In nbviewer" width="100"/></a> |
| 15 | + |
| 16 | +```python id="Rjxll-Sdchar" |
| 17 | + |
| 18 | +``` |
| 19 | + |
| 20 | +<!-- #region id="_WZ7vK7sctla" --> |
| 21 | +# Helium-3 Detector Background Characterization |
| 22 | + |
| 23 | +### Prequel - What's a Helium-3 detector ? |
| 24 | + |
| 25 | + |
| 26 | +Helium-3 (³He) detectors are gas-filled proportional counters widely used for detecting thermal neutrons. When a thermal neutron enters the detector, it interacts with a ³He nucleus via the reaction: |
| 27 | + |
| 28 | +$$ |
| 29 | +n + {}^3\text{He} \rightarrow p + {}^3\text{H} + 764\,\text{keV} |
| 30 | +$$ |
| 31 | + |
| 32 | +This reaction produces a proton and a triton (³H), which ionize the surrounding gas. The resulting charge is collected by an anode wire, and the induced current is amplified to produce a measurable signal. The signal amplitude is proportional to the energy deposited and provides a clear signature of neutron interactions. |
| 33 | + |
| 34 | +## Helium-3 Tube Detectors – December 2024 / January 2025 |
| 35 | + |
| 36 | +Our goal is to characterize the background radiation detected by the Helium-3 neutron counters. To achieve this, we operated the detectors continuously throughout December 2024 and January 2025 in a stable background environment. In this notebook, we analyze the time-resolved background count data to establish a baseline response. In this analysis, we are interested in identifying statistically significant deviations in future experiments involving neutron-producing sources. |
| 37 | + |
| 38 | +<!-- #endregion --> |
| 39 | + |
| 40 | +```python colab={"base_uri": "https://localhost:8080/"} id="PxFHoPsndN0t" outputId="fea91b93-d5c9-4342-bc69-3eb4a22e6ec6" |
| 41 | +# RUN THIS IF YOU ARE USING GOOGLE COLAB |
| 42 | +import sys |
| 43 | +import os |
| 44 | +!git clone https://github.com/project-ida/arpa-e-experiments.git |
| 45 | +sys.path.insert(0,'/content/arpa-e-experiments') |
| 46 | +os.chdir('/content/arpa-e-experiments') |
| 47 | +``` |
| 48 | + |
| 49 | +```python id="EKZ_R9o2dRek" |
| 50 | +# Libraries and helper functions |
| 51 | + |
| 52 | +import pandas as pd |
| 53 | +import numpy as np |
| 54 | +import matplotlib.pyplot as plt |
| 55 | +import scipy.stats as stats |
| 56 | +from scipy.stats import norm |
| 57 | +from scipy.stats import poisson |
| 58 | + |
| 59 | +import ipywidgets as widgets |
| 60 | +from IPython.display import display |
| 61 | + |
| 62 | +from IPython.display import Image |
| 63 | +from IPython.display import Video |
| 64 | +from IPython.display import HTML |
| 65 | + |
| 66 | +# Use our custom helper functions |
| 67 | +# - process_data |
| 68 | +# - plot_panels |
| 69 | +# - plot_panels_with_scatter |
| 70 | +# - print_info |
| 71 | +from libs.helpers import * |
| 72 | +``` |
| 73 | + |
| 74 | +<!-- #region id="lzKFEAF6dagV" --> |
| 75 | +## Step 1 – Data Collection |
| 76 | + |
| 77 | +Let’s begin by collecting raw data from the Helium-3 neutron detectors. |
| 78 | + |
| 79 | +We have collected long-term time-resolved background data using our Helium-3 detectors, covering the period from December 14th, 2024 at 00:01:01 to January 23rd, 2025 at 23:58:59. These data consist of neutron counts recorded at regular time intervals. |
| 80 | + |
| 81 | +We store the data as pandas DataFrames, which allows us to easily manipulate, visualize, and analyze the results in the steps that follow. |
| 82 | +<!-- #endregion --> |
| 83 | + |
| 84 | +```python id="fdZ7r-YJdcrc" |
| 85 | +he3_dec = pd.read_csv( |
| 86 | + 'http://nucleonics.mit.edu/csv-files/he3-detectors-background2-2.csv', |
| 87 | + parse_dates=['time'], |
| 88 | + date_format="ISO8601", |
| 89 | + index_col='time' |
| 90 | +) |
| 91 | + |
| 92 | +he3_jan = pd.read_csv( |
| 93 | + 'http://nucleonics.mit.edu/csv-files/he3-detectors-background-2.csv', |
| 94 | + #'https://lenr.mit.edu/call-rscript.php?filename=he3-detectors-background&graphno=2f&database=experiments&random=41', |
| 95 | + parse_dates=['time'], |
| 96 | + date_format="ISO8601", |
| 97 | + index_col='time' |
| 98 | +) |
| 99 | + |
| 100 | +# To facilitate our analysis, let's concatenate december and january |
| 101 | +he3_all = pd.concat([he3_dec, he3_jan]) |
| 102 | +he3_all = he3_all.sort_index() |
| 103 | + |
| 104 | +``` |
| 105 | + |
| 106 | +<!-- #region id="PzugPnB5d5JT" --> |
| 107 | +## Step 2 - Visualizing Neutron and Gamma counts |
| 108 | + |
| 109 | +Now that we have collected the raw data (i.e. electric signal history) that interests us, let us have a look at the measured neutron and gamma counts. |
| 110 | +<!-- #endregion --> |
| 111 | + |
| 112 | +```python colab={"base_uri": "https://localhost:8080/", "height": 463} id="bUOQ2fYdd712" outputId="6decf25e-d690-4483-ea8e-6240dd4ee76b" |
| 113 | +plt.figure(figsize=(8, 4)) |
| 114 | +plt.plot(he3_all['Counts ch50-1000']) |
| 115 | +plt.xlabel('Time') |
| 116 | +plt.ylabel('Counts') |
| 117 | +plt.xticks(rotation=45) |
| 118 | +plt.title(f"He3 counts December 2024 and January 2025") |
| 119 | +plt.show() |
| 120 | +# plt.savefig("He3-counts-dec.png", dpi=600) |
| 121 | +``` |
| 122 | + |
| 123 | +```python colab={"base_uri": "https://localhost:8080/", "height": 335} id="7HJkR9hpexLK" outputId="dc599529-5ba8-4b34-c772-6c0848d53898" |
| 124 | +he3_all['Counts ch50-1000'].describe() |
| 125 | +``` |
| 126 | + |
| 127 | +```python colab={"base_uri": "https://localhost:8080/", "height": 564} id="3BWIPYvwexio" outputId="67de1e10-e136-46c1-d4b3-c1a00eca54d5" |
| 128 | +# Ensure the index is datetime |
| 129 | +he3_all.index = pd.to_datetime(he3_all.index) |
| 130 | + |
| 131 | +# Group by day |
| 132 | +grouped_by_day = he3_all.groupby(he3_all.index.date) |
| 133 | + |
| 134 | +# Define bins for the histogram |
| 135 | +bins = np.arange(he3_all['Counts ch50-1000'].min(), he3_all['Counts ch50-1000'].max() + 1, 1) # Use integer bins |
| 136 | + |
| 137 | +# Plot all histograms as line plots |
| 138 | +plt.figure(figsize=(10, 6)) |
| 139 | + |
| 140 | +for day, group in grouped_by_day: |
| 141 | + hist_values, bin_edges = np.histogram(group['Counts ch50-1000'], bins=bins, density=True) |
| 142 | + plt.plot(bin_edges[:-1], hist_values, alpha=0.1, label=str(day)) |
| 143 | + |
| 144 | +plt.xlabel("Counts per Minute") |
| 145 | +plt.ylabel("Normalized Frequency") |
| 146 | +plt.title("Daily Histograms of Background He-3 Counts (Line Plot)") |
| 147 | +plt.grid(True) |
| 148 | +plt.show() |
| 149 | + |
| 150 | +``` |
| 151 | + |
| 152 | +<!-- #region id="zxgDMmwGfr8K" --> |
| 153 | +From the plot above, it is unclear whether the background ditribution corresponds to a Poisson ditribution with a large $\lambda$ or a Gaussian distribution. Let us begin by attempting to fit the data to a Poisson distribution. |
| 154 | +<!-- #endregion --> |
| 155 | + |
| 156 | +```python id="RX4TIdihhA8K" |
| 157 | +he3_all.index = pd.to_datetime(he3_all.index) |
| 158 | + |
| 159 | +grouped_by_day = he3_all.groupby(he3_all.index.date) |
| 160 | + |
| 161 | +bins = np.arange(he3_all["Counts ch50-1000"].min(), |
| 162 | + he3_all["Counts ch50-1000"].max() + 1, 1) |
| 163 | + |
| 164 | +# Compute daily histograms |
| 165 | +histograms = [] |
| 166 | +for day, group in grouped_by_day: |
| 167 | + hist_values, bin_edges = np.histogram(group["Counts ch50-1000"], bins=bins, density=True) |
| 168 | + histograms.append(hist_values) |
| 169 | + |
| 170 | +histograms = np.array(histograms) |
| 171 | +mean_histogram = np.mean(histograms, axis=0) |
| 172 | +std_histogram = np.std(histograms, axis=0) |
| 173 | +bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2 |
| 174 | + |
| 175 | +# Fit a normal distribution to the mean histogram (via expanded sample) |
| 176 | +expanded_data = np.repeat(bin_centers, (mean_histogram * 1000).astype(int)) |
| 177 | +mu, sigma = norm.fit(expanded_data) |
| 178 | +x = np.linspace(bin_centers.min(), bin_centers.max(), 500) |
| 179 | +normal_pdf = norm.pdf(x, mu, sigma) |
| 180 | +normal_pdf *= np.trapz(mean_histogram, bin_centers) |
| 181 | + |
| 182 | +# Function to plot with Z-sigma band |
| 183 | +def plot_with_z_band(Z): |
| 184 | + lower = np.maximum(mean_histogram - Z * std_histogram, 0) |
| 185 | + upper = mean_histogram + Z * std_histogram |
| 186 | + |
| 187 | + plt.figure(figsize=(10, 6)) |
| 188 | + |
| 189 | + # Plot individual daily histograms |
| 190 | + for hist in histograms: |
| 191 | + plt.plot(bin_centers, hist, alpha=0.1, color='gray') |
| 192 | + |
| 193 | + # Plot mean histogram and normal fit |
| 194 | + plt.plot(bin_centers, mean_histogram, color='black', linewidth=2, label='Mean Daily Histogram') |
| 195 | + plt.plot(x, normal_pdf, 'r--', linewidth=2, label=f'Normal Fit (μ={mu:.2f}, σ={sigma:.2f})') |
| 196 | + |
| 197 | + # Plot Z-sigma band |
| 198 | + plt.fill_between(bin_centers, lower, upper, color='blue', alpha=0.2, label=f'±{Z}σ Band') |
| 199 | + |
| 200 | + # Labels and formatting |
| 201 | + plt.xlabel("Counts per Minute (ch50–1000)") |
| 202 | + plt.ylabel("Normalized Frequency") |
| 203 | + plt.title(f"He-3 Background Daily Histograms with ±{Z}σ Band") |
| 204 | + plt.legend() |
| 205 | + plt.grid(True) |
| 206 | + plt.tight_layout() |
| 207 | + plt.show() |
| 208 | + |
| 209 | +# Create Z-score slider |
| 210 | +z_slider = widgets.IntSlider(value=3, min=1, max=5, step=1, description='Z-score:', continuous_update=False) |
| 211 | + |
| 212 | +# Interactive output |
| 213 | +interactive_plot = widgets.interactive_output(plot_with_z_band, {'Z': z_slider}) |
| 214 | + |
| 215 | +# Display |
| 216 | +display(z_slider, interactive_plot) |
| 217 | + |
| 218 | +``` |
| 219 | + |
| 220 | +```python id="g7WTO2_DiOss" |
| 221 | +# Ensure datetime index |
| 222 | +he3_all.index = pd.to_datetime(he3_all.index) |
| 223 | + |
| 224 | +# Group by day |
| 225 | +grouped_by_day = he3_all.groupby(he3_all.index.date) |
| 226 | + |
| 227 | +# Define bins |
| 228 | +bins = np.arange(he3_all["Counts ch50-1000"].min(), |
| 229 | + he3_all["Counts ch50-1000"].max() + 1, 1) |
| 230 | + |
| 231 | +# Compute daily histograms |
| 232 | +histograms = [] |
| 233 | +for day, group in grouped_by_day: |
| 234 | + hist_values, bin_edges = np.histogram(group["Counts ch50-1000"], bins=bins, density=True) |
| 235 | + histograms.append(hist_values) |
| 236 | + |
| 237 | +# Convert list to array and compute mean + std |
| 238 | +histograms = np.array(histograms) |
| 239 | +mean_histogram = np.mean(histograms, axis=0) |
| 240 | +std_histogram = np.std(histograms, axis=0) |
| 241 | +k_values = bin_edges[:-1] |
| 242 | + |
| 243 | +# Estimate Poisson background mean |
| 244 | +lambda_ = he3_all["Counts ch50-1000"].mean() |
| 245 | + |
| 246 | +# Poisson fit |
| 247 | +poisson_pmf = stats.poisson.pmf(k_values, mu=lambda_) |
| 248 | +poisson_pmf_normalized = poisson_pmf / np.sum(poisson_pmf) |
| 249 | +poisson_pmf_normalized *= np.sum(mean_histogram) |
| 250 | + |
| 251 | +# Interactive plot function |
| 252 | +def plot_histogram_with_z(Z): |
| 253 | + upper = mean_histogram + Z * std_histogram |
| 254 | + lower = np.maximum(mean_histogram - Z * std_histogram, 0) |
| 255 | + |
| 256 | + plt.figure(figsize=(10, 6)) |
| 257 | + |
| 258 | + # Daily histograms |
| 259 | + for hist in histograms: |
| 260 | + plt.plot(k_values, hist, alpha=0.2, color='gray') |
| 261 | + |
| 262 | + # Mean histogram |
| 263 | + plt.plot(k_values, mean_histogram, color='black', linewidth=2, label='Mean Histogram') |
| 264 | + |
| 265 | + # Z-sigma band |
| 266 | + plt.fill_between(k_values, lower, upper, color='black', alpha=0.1, label=f'±{Z}σ Band') |
| 267 | + |
| 268 | + # Poisson fit |
| 269 | + plt.plot(k_values, poisson_pmf_normalized, 'r--', linewidth=2, label=f'Poisson Fit (λ={lambda_:.2f})') |
| 270 | + |
| 271 | + # Labels and formatting |
| 272 | + plt.xlabel("Counts per Minute (ch50–1000)") |
| 273 | + plt.ylabel("Normalized Frequency") |
| 274 | + plt.title(f"He-3 Daily Histograms with ±{Z}σ Band and Poisson Fit") |
| 275 | + plt.legend() |
| 276 | + plt.grid(True) |
| 277 | + plt.tight_layout() |
| 278 | + plt.show() |
| 279 | + |
| 280 | +# Z-score slider |
| 281 | +z_slider = widgets.IntSlider(value=3, min=1, max=5, step=1, description='Z-score:', continuous_update=False) |
| 282 | + |
| 283 | +# Display |
| 284 | +interactive_plot = widgets.interactive_output(plot_histogram_with_z, {'Z': z_slider}) |
| 285 | +display(z_slider, interactive_plot) |
| 286 | + |
| 287 | +``` |
| 288 | + |
| 289 | +<!-- #region id="3zUTAdVujXne" --> |
| 290 | +Ultimately, it is hard to differentiate between these by eye. This might be a result of the central limit theorem ? I think we expect that with a biigger $\lambda$, our Poisson distribution would tend to a normal distribution. |
| 291 | +<!-- #endregion --> |
| 292 | + |
| 293 | +```python id="ZNEIgEG0ikMx" |
| 294 | +# Estimate Poisson mean for He-3 data |
| 295 | +lambda_he3 = he3_all["Counts ch50-1000"].mean() |
| 296 | + |
| 297 | +# Define the interactive plotting function |
| 298 | +def plot_he3_outliers(Z): |
| 299 | + # Calculate the Z-score-based Poisson threshold |
| 300 | + threshold_poisson = lambda_he3 + Z * np.sqrt(lambda_he3) |
| 301 | + |
| 302 | + # Filter for outliers |
| 303 | + poisson_outliers = he3_all[he3_all["Counts ch50-1000"] > threshold_poisson] |
| 304 | + |
| 305 | + # Plot the time series and outliers |
| 306 | + plt.figure(figsize=(14, 5)) |
| 307 | + plt.plot(he3_all.index, he3_all["Counts ch50-1000"], |
| 308 | + label="He-3 Counts per Minute", color='green', linewidth=1) |
| 309 | + |
| 310 | + plt.scatter(poisson_outliers.index, poisson_outliers["Counts ch50-1000"], |
| 311 | + color='red', label=f"{len(poisson_outliers)} Outliers (> {threshold_poisson:.2f})", zorder=5) |
| 312 | + |
| 313 | + plt.axhline(threshold_poisson, color='gray', linestyle='--', linewidth=1.5, |
| 314 | + label=f"Z = {Z} Threshold") |
| 315 | + |
| 316 | + plt.title(f"He-3 Count Time Series with Z={Z} Outlier Threshold") |
| 317 | + plt.xlabel("Time") |
| 318 | + plt.ylabel("Counts per Minute (ch50–1000)") |
| 319 | + plt.legend() |
| 320 | + plt.grid(True) |
| 321 | + plt.tight_layout() |
| 322 | + plt.show() |
| 323 | + |
| 324 | +# Create the Z-score slider |
| 325 | +z_slider_he3 = widgets.IntSlider(value=3, min=1, max=10, step=1, |
| 326 | + description='Z-score:', continuous_update=False) |
| 327 | + |
| 328 | +# Connect slider to plotting function |
| 329 | +interactive_plot_he3 = widgets.interactive_output(plot_he3_outliers, {'Z': z_slider_he3}) |
| 330 | + |
| 331 | +# Display slider and plot |
| 332 | +display(z_slider_he3, interactive_plot_he3) |
| 333 | + |
| 334 | +``` |
| 335 | + |
| 336 | +```python id="m9WCJBdMjrwb" |
| 337 | + |
| 338 | +``` |
0 commit comments