Skip to content

Commit 191f00a

Browse files
author
GitHub Actions
committed
Updated source files for modified notebooks
1 parent 8ca862a commit 191f00a

1 file changed

Lines changed: 338 additions & 0 deletions

File tree

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
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> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<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

Comments
 (0)