Lifetime of Muons Generated by Cosmic Rays — Week 2
1 Where We Are in the Sequence
Week 2 of 2: Analysis and Interpretation
Last week you set up the muon lifetime detector, characterized the electronics chain, performed a timing calibration, and started an overnight data collection. This week you will export the data, apply your calibration, and perform a complete analysis to extract the muon lifetime. You will compare your result to the vacuum value and explore the physics of muon capture on carbon nuclei.
Last week: Physics background → PMT setup → Electronics characterization → Timing calibration → Start data collection
This week: Data export → Calibration → Rebinning → Exponential fitting → Physical interpretation
Connection to previous work: The curve fitting workflow here is very similar to what you used in the Gaussian Beams lab—you will use curve_fit with weighted uncertainties, make two-panel plots (data + residuals), and calculate reduced chi-squared. The main differences are the fit function (exponential vs. error function) and the uncertainty model (Poisson counting statistics vs. detector noise).
2 Learning Goals
2.1 Prelab
After completing the prelab, you will be able to:
- Perform a weighted exponential fit to synthetic decay data using
scipy.optimize.curve_fit. - Assign Poisson uncertainties (\(\sqrt{N}\)) to binned count data and explain why this is appropriate.
- Predict whether the measured muon lifetime in scintillator will be above or below the vacuum value, and explain why.
2.2 In-Lab
After completing the in-lab work, you will be able to:
- Export MAESTRO data as an SPE file and load it into Python using the provided SPE reader.
- Apply the timing calibration from Week 1 to convert channel numbers to microseconds.
- Rebin data to improve statistics and explain the trade-off with time resolution.
- Perform a weighted exponential fit to extract the muon lifetime, with proper uncertainty estimation.
- Create a two-panel plot (data + fit and residuals) and evaluate goodness of fit using reduced chi-squared.
- Compare the measured lifetime to the vacuum value and quantify the discrepancy.
- Estimate the nuclear capture rate for \(\mu^-\) on carbon.
3 Overview of Your Work
Prelab (~45 minutes at home): Practice the full fitting pipeline on synthetic data. This ensures your analysis code is working before you apply it to real data.
In-Lab (~2.5 hours):
- Export your MAESTRO data and load it into Python
- Apply calibration and inspect the spectrum
- Rebin and assign uncertainties
- Fit the muon lifetime
- Interpret your results physically
- Estimate the \(\mu^-\) capture rate
See the deliverables checklist at the end of this guide.
4 Prelab: Exponential Fitting Exercise
Before coming to lab, practice the complete analysis pipeline using synthetic data. This exercise mirrors exactly what you will do with real data, so by the time you arrive in lab, your code should be ready.
The Python scripts 01_spe_reader.py and 02_muon_analysis.py (available on the course website under Resources) provide all the functions you need.
4.1 Generate Synthetic Data
Use the synthetic data generator to create a test dataset:
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit
# Generate synthetic exponential decay data
np.random.seed(42) # For reproducibility
# Parameters
tau_true = 2.05 # "True" lifetime in microseconds
N0_true = 500 # Amplitude
bg_true = 2.0 # Background counts per bin
n_bins = 100 # Number of time bins
t_max = 10.0 # Maximum time in microseconds
# Create time array and expected counts
time = np.linspace(0.2, t_max, n_bins) # Start above threshold
expected = N0_true * np.exp(-time / tau_true) + bg_true
# Generate Poisson-distributed counts
counts = np.random.poisson(expected)4.2 Assign Uncertainties
Each bin contains a count \(N\) drawn from a Poisson distribution. The standard deviation of a Poisson distribution with mean \(\lambda\) is \(\sqrt{\lambda}\). Since our best estimate of \(\lambda\) is the observed count \(N\), we assign:
\[\sigma_i = \sqrt{N_i}\] (1)
# Assign Poisson uncertainties
counts_err = np.sqrt(counts.astype(float))
# Handle any bins with zero counts (avoid division by zero)
counts_err = np.where(counts_err > 0, counts_err, 1.0)Why is \(\sqrt{N}\) the right uncertainty? What happens to the relative uncertainty (\(\sigma/N\)) as \(N\) increases? This is why rebinning (combining channels) helps—larger counts mean smaller relative uncertainties.
4.3 Fit the Exponential Model
The model for muon decay in the detector is:
\[N(t) = N_0 \, e^{-t/\tau} + B\] (2)
where \(N_0\) is the amplitude, \(\tau\) is the lifetime, and \(B\) is a constant background from accidental coincidences.
# Define the model
def exponential_decay(t, N0, tau, background):
return N0 * np.exp(-t / tau) + background
# Initial parameter guesses
p0 = [np.max(counts), 2.0, np.mean(counts[-10:])]
# Weighted fit with Poisson errors
popt, pcov = curve_fit(
exponential_decay, time, counts,
p0=p0,
sigma=counts_err,
absolute_sigma=True # Important: tells curve_fit that sigma is in data units
)
perr = np.sqrt(np.diag(pcov))
print(f"N0: {popt[0]:.1f} +/- {perr[0]:.1f}")
print(f"tau: {popt[1]:.3f} +/- {perr[1]:.3f} us")
print(f"background: {popt[2]:.2f} +/- {perr[2]:.2f}")The absolute_sigma=True flag tells curve_fit that your uncertainties are in the same units as your data (counts), not relative weights. This ensures the covariance matrix, and therefore the parameter uncertainties, are physically meaningful.
4.4 Plot Data, Fit, and Residuals
Create a two-panel figure following the same pattern as the Gaussian Beams beam profile analysis:
# Generate smooth fit curve
t_smooth = np.linspace(time.min(), time.max(), 500)
fit_smooth = exponential_decay(t_smooth, *popt)
# Residuals at data points
fit_at_data = exponential_decay(time, *popt)
residuals = counts - fit_at_data
# Two-panel plot
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8),
gridspec_kw={'height_ratios': [3, 1]}, sharex=True)
# Top: data and fit
ax1.errorbar(time, counts, yerr=counts_err, fmt='o', markersize=3,
capsize=2, label='Synthetic data', alpha=0.8)
ax1.plot(t_smooth, fit_smooth, 'r-', linewidth=2,
label=f'Fit: τ = {popt[1]:.3f} μs')
ax1.set_ylabel('Counts per bin')
ax1.set_title('Prelab: Synthetic Muon Decay Fit')
ax1.legend()
ax1.grid(True, alpha=0.3)
# Bottom: residuals
ax2.errorbar(time, residuals, yerr=counts_err, fmt='o', markersize=3,
capsize=2, alpha=0.8)
ax2.axhline(y=0, color='r', linestyle='--')
ax2.set_xlabel('Time (μs)')
ax2.set_ylabel('Residuals')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()4.5 Calculate Reduced Chi-Squared
The chi-squared statistic measures how well the model describes the data:
\[\chi^2 = \sum_i \left(\frac{N_i - f(t_i)}{\sigma_i}\right)^2\] (3)
The reduced chi-squared is \(\chi^2_\nu = \chi^2 / \nu\), where \(\nu = N_\text{data} - N_\text{params}\) is the number of degrees of freedom.
chi2 = np.sum((residuals / counts_err) ** 2)
dof = len(counts) - 3 # 3 fit parameters
chi2_red = chi2 / dof
print(f"Chi-squared: {chi2:.1f}")
print(f"Degrees of freedom: {dof}")
print(f"Reduced chi-squared: {chi2_red:.2f}")Interpretation: \(\chi^2_\nu \approx 1\) indicates a good fit. \(\chi^2_\nu \gg 1\) suggests the model doesn’t describe the data (or uncertainties are underestimated). \(\chi^2_\nu \ll 1\) suggests the uncertainties are overestimated.
4.6 Verify Parameter Recovery
Check that your fit recovers the known input lifetime:
print(f"\nTrue lifetime: {tau_true:.3f} us")
print(f"Fitted lifetime: {popt[1]:.3f} +/- {perr[1]:.3f} us")
discrepancy = abs(popt[1] - tau_true) / perr[1]
print(f"Discrepancy: {discrepancy:.1f} sigma")The fitted value should agree with the true value within ~1–2\(\sigma\).
4.7 Prelab Prediction
Before coming to lab, consider this question:
The free muon lifetime in vacuum is 2.197 \(\mu\)s. In your experiment, you are measuring the lifetime in plastic scintillator (essentially carbon).
Will the lifetime you measure be:
- (a) Equal to the vacuum value?
- (b) Shorter than the vacuum value?
- (c) Longer than the vacuum value?
Hint: Negative muons (\(\mu^-\)) can undergo nuclear capture (\(\mu^- + p \rightarrow n + \nu_\mu\)) in addition to free decay. Positive muons (\(\mu^+\)) cannot. About 45% of cosmic-ray muons at the surface are negatively charged. Think about how these two populations contribute to the overall observed decay rate.
Record your prediction and reasoning. You will test this quantitatively in the lab.
5 In-Lab: Data Export and Loading
5.1 Export from MAESTRO
- In MAESTRO, note the current live time and real time (displayed at the bottom of the window).
- Stop the acquisition: Acquire → Stop.
- Export the spectrum: File → Export. Save as an ASCII SPE file (
.Spe). - Transfer the file to your analysis computer.
5.2 Load with Python
Use the read_spe_file() function from 01_spe_reader.py to load the data:
from importlib import import_module
spe_reader = import_module("01_spe_reader")
# Load your data
data = spe_reader.read_spe_file("your_data_file.Spe")
channels = np.arange(data['num_channels'])
counts = data['counts']
print(f"Channels: {data['num_channels']}")
print(f"Live time: {data['live_time']:.0f} s")
print(f"Total counts: {np.sum(counts)}")5.3 Plot Raw Spectrum
Plot the raw spectrum to see the overall shape:
fig = spe_reader.plot_raw_spectrum(channels, counts,
title="Muon Lifetime - Raw MCA Spectrum")
plt.show()What to look for: You should see a peak at low channels (corresponding to short decay times, where the exponential decay rate is highest), falling off toward higher channels. There may be a flat region at high channels from accidental coincidences (background). The first few channels may have zero counts due to the discriminator threshold.
6 In-Lab: Apply Calibration
Use the calibration parameters you measured in Week 1 to convert channel numbers to times in microseconds:
# Your Week 1 calibration values
cal_slope = ... # us/channel (from your Q6 fit)
cal_intercept = ... # us (from your Q6 fit)
cable_delay = ... # us (from your Q4 measurement)
# Convert channels to time
time_all = cal_slope * channels + cal_intercept
# Subtract the cable delay
time_all = time_all - cable_delayPlot the spectrum again, now with the x-axis in microseconds:
plt.figure(figsize=(10, 5))
plt.step(time_all, counts, where='mid', linewidth=0.8)
plt.xlabel('Time (μs)')
plt.ylabel('Counts per channel')
plt.title('Muon Decay Spectrum (Calibrated)')
plt.grid(True, alpha=0.3)
plt.show()7 In-Lab: Rebin and Assign Uncertainties
With thousands of channels, most individual channels have very few counts (or even zero). Rebinning groups adjacent channels together, improving statistics at the cost of time resolution.
# Rebin by a factor of 10
bin_factor = 10
n_bins = len(counts) // bin_factor
rebinned_counts = counts[:n_bins * bin_factor].reshape(n_bins, bin_factor).sum(axis=1)
rebinned_time = time_all[:n_bins * bin_factor].reshape(n_bins, bin_factor).mean(axis=1)
# Trim the threshold region (channels with zero counts at the beginning)
# and any bins with zero counts
threshold_bin = ... # Identify by inspection where data begins
mask = (rebinned_counts > 0) & (rebinned_time > 0)
time = rebinned_time[mask]
counts_data = rebinned_counts[mask]
# Poisson uncertainties
counts_err = np.sqrt(counts_data.astype(float))7.1 Question 7: Understanding Uncertainties
(a) For a bin with 100 counts, what is the relative uncertainty (\(\sigma/N\))? What about for a bin with 10 counts? With 1 count?
(b) How does rebinning by a factor of 10 change the counts per bin and the relative uncertainty? Explain why this is beneficial.
(c) Why would it be problematic to include bins with zero counts in the fit? (Think about the Poisson uncertainty formula and what happens in the chi-squared calculation.)
8 In-Lab: Fit Exponential Decay
8.1 Question 8: Muon Lifetime Fit
(a) Plot your rebinned data with error bars on both linear and semilog scales:
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
# Linear scale
ax1.errorbar(time, counts_data, yerr=counts_err, fmt='o', markersize=3,
capsize=2, alpha=0.8)
ax1.set_xlabel('Time (μs)')
ax1.set_ylabel('Counts per bin')
ax1.set_title('Linear Scale')
ax1.grid(True, alpha=0.3)
# Semilog scale
ax2.errorbar(time, counts_data, yerr=counts_err, fmt='o', markersize=3,
capsize=2, alpha=0.8)
ax2.set_yscale('log')
ax2.set_xlabel('Time (μs)')
ax2.set_ylabel('Counts per bin')
ax2.set_title('Semilog Scale')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()On the semilog plot, a pure exponential decay appears as a straight line. Does your data look approximately linear on the semilog plot? Where does it deviate, and why? (Think about background.)
(b) Fit the exponential decay model \(N(t) = N_0 \, e^{-t/\tau} + B\) to your data. Use the technique from the prelab exercise:
def exponential_decay(t, N0, tau, background):
return N0 * np.exp(-t / tau) + background
# Estimate initial guesses from the data
p0 = [np.max(counts_data), 2.0, np.mean(counts_data[-10:])]
popt, pcov = curve_fit(exponential_decay, time, counts_data,
p0=p0, sigma=counts_err, absolute_sigma=True)
perr = np.sqrt(np.diag(pcov))
print(f"N0: {popt[0]:.1f} +/- {perr[0]:.1f}")
print(f"tau: {popt[1]:.3f} +/- {perr[1]:.3f} us")
print(f"background: {popt[2]:.2f} +/- {perr[2]:.2f}")(c) Create a two-panel plot showing the data with the fit curve (top) and residuals (bottom). Use the code structure from your prelab exercise.
(d) Calculate the reduced chi-squared. Is the fit good? If \(\chi^2_\nu\) is significantly different from 1, discuss possible reasons.
(e) Compare your measured lifetime to the vacuum value of 2.197 \(\mu\)s. How many standard deviations away is your measurement?
\[\text{discrepancy} = \frac{|\tau_\text{measured} - \tau_\text{vacuum}|}{\sigma_\tau}\] (4)
9 In-Lab: Mu-Minus Capture Analysis
9.1 Question 9: Nuclear Capture of Negative Muons
Your measured lifetime in scintillator should be shorter than the vacuum value. This is because negative muons (\(\mu^-\)) can be captured by carbon nuclei in addition to decaying freely:
\[\mu^- + p \rightarrow n + \nu_\mu\] (5)
When a \(\mu^-\) captures, the products (a neutron and a neutrino) typically deposit little or no visible energy in the scintillator, so capture events produce no stop signal. They are simply missing from your data. However, the possibility of capture means that \(\mu^-\) disappear faster than \(\mu^+\), so the decay electrons you do observe from \(\mu^-\) follow a shorter effective lifetime.
(a) Why can \(\mu^-\) undergo nuclear capture but \(\mu^+\) cannot? (Think about charge conservation and what nuclei contain.)
(b) When a \(\mu^-\) can either decay freely or be captured, the rates for the two processes add:
\[\frac{1}{\tau_{\mu^-}} = \frac{1}{\tau_\text{free}} + \frac{1}{\tau_\text{capture}}\] (6)
Explain in your own words why the rates add rather than the lifetimes. (Hint: Think about probability per unit time.)
(c) Your data is actually a sum of two exponentials: \(\mu^+\) decays with lifetime \(\tau_\text{free} = 2.197\;\mu\)s, and \(\mu^-\) decays with a shorter effective lifetime \(\tau_{\mu^-}\). The single-exponential fit you performed in Q8 returns a value dominated by the longer-lived \(\mu^+\) component, so it cannot directly give you \(\tau_{\mu^-}\).
To extract the capture rate, fit a two-component model with the \(\mu^+\) lifetime fixed to the known vacuum value:
\[N(t) = N_+ \, e^{-t/\tau_\text{free}} + N_- \, e^{-t/\tau_{\mu^-}} + B\] (7)
def two_component_decay(t, N_plus, N_minus, tau_mu_minus, background):
"""Two-exponential model: mu+ (fixed lifetime) + mu- (free lifetime)."""
tau_free = 2.197 # Fixed to vacuum value
return (N_plus * np.exp(-t / tau_free)
+ N_minus * np.exp(-t / tau_mu_minus)
+ background)
# Initial guesses
p0 = [0.55 * np.max(counts_data), 0.45 * np.max(counts_data), 1.0,
np.mean(counts_data[-10:])]
# Bounds: all amplitudes positive, tau_mu_minus between 0.1 and 2.197 us
bounds = ([0, 0, 0.1, 0], [np.inf, np.inf, 2.197, np.inf])
popt2, pcov2 = curve_fit(two_component_decay, time, counts_data,
p0=p0, sigma=counts_err, absolute_sigma=True,
bounds=bounds)
perr2 = np.sqrt(np.diag(pcov2))
print(f"N+: {popt2[0]:.1f} +/- {perr2[0]:.1f}")
print(f"N-: {popt2[1]:.1f} +/- {perr2[1]:.1f}")
print(f"tau_mu-: {popt2[2]:.3f} +/- {perr2[2]:.3f} us")
print(f"background: {popt2[3]:.2f} +/- {perr2[3]:.2f}")(d) From your fitted \(\tau_{\mu^-}\), extract the capture lifetime using Equation 6:
\[\frac{1}{\tau_\text{capture}} = \frac{1}{\tau_{\mu^-}} - \frac{1}{\tau_\text{free}}\]
tau_mu_minus = popt2[2]
tau_free = 2.197
tau_capture = 1.0 / (1.0/tau_mu_minus - 1.0/tau_free)
print(f"Estimated capture lifetime: {tau_capture:.1f} us")
print(f"Accepted value for carbon: ~2.0 us")The accepted value for the \(\mu^-\) capture lifetime in carbon is approximately 2.0 \(\mu\)s. How does your estimate compare? Given the limited statistics (~5000 events split between two components), do not be surprised if your uncertainty is large.
(e) Compare the reduced chi-squared of the two-component fit to your single-exponential fit from Q8. Does the two-component model fit the data better? With only ~5000 events, the improvement may be small — discuss why resolving two exponentials with similar lifetimes is inherently difficult.
10 Reflection
Answer the following questions in your notebook:
Compare the experimental workflow of this lab to the Gaussian Beams lab. What similarities and differences do you see in how you went from raw detector signals to a quantitative measurement?
At what point in the measurement chain is the most information lost? (Think about: PMT pulse shape → discriminator → TAC → MCA → rebinning.) Is this information loss necessary?
You measured a single number (the muon lifetime) from thousands of individual decay events. How did the statistical treatment (Poisson uncertainties, chi-squared) allow you to make a quantitative statement about the precision of your result?
11 Deliverables
Before leaving lab, ensure you have:
Key results table (fill in your notebook):
| Quantity | Your Value | Accepted Value |
|---|---|---|
| Single-exp lifetime | ___ ± ___ μs | 2.197 μs (vacuum) |
| Single-exp reduced χ² | ___ | ~1.0 |
| Discrepancy from vacuum | ___ σ | — |
| τ_μ- (two-component fit) | ___ ± ___ μs | ~1.05 μs |
| Two-component reduced χ² | ___ | ~1.0 |
| Capture lifetime (estimated) | ___ μs | ~2.0 μs (carbon) |