- Search full spectrum (exclude pulser region) instead of just low-energy - Include 59.54 keV line in known Am-241 energies - Brute-force peak-to-energy matching with physical gain constraint (3-40 ch/keV) to reject spurious fits - Fixed find_peaks parameters: height=100, distance=5 for accurate peak detection - Calibration now correctly maps: ch 97→13 keV, ch 134→17.4 keV, ch 537→59.6 keV, giving Tc-99 endpoint at ch 2750→291.6 keV - Add .rpiv/ to .gitignore
331 lines
9.7 KiB
Plaintext
331 lines
9.7 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"# Experimental Data Calibration Pipeline\n",
|
|
"\n",
|
|
"Interactive workflow for processing raw CAMAC MCA binary spectra:\n",
|
|
"read -> subtract -> calibrate -> livetime -> validate -> export\n",
|
|
"\n",
|
|
"## Prerequisites\n",
|
|
"- Raw binary spectrum files (A*.DAT format)\n",
|
|
"- Am-241 calibration file (CAM1.DAT)\n",
|
|
"- Pulser frequency and real time (from calibration run metadata)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import matplotlib\n",
|
|
"# Use non-interactive backend only when no display is available (e.g. jupyter execute)\n",
|
|
"import matplotlib as mpl\n",
|
|
"try:\n",
|
|
" import matplotlib.pyplot as plt\n",
|
|
" # Check if we're in a headless environment\n",
|
|
" import os\n",
|
|
" if not os.environ.get(\"DISPLAY\") and not os.environ.get(\"WAYLAND_DISPLAY\"):\n",
|
|
" mpl.use(\"Agg\")\n",
|
|
"except ImportError:\n",
|
|
" mpl.use(\"Agg\")\n",
|
|
" import matplotlib.pyplot as plt\n",
|
|
"import numpy as np\n",
|
|
"\n",
|
|
"from exp_data.pipeline import process_spectrum, process_run_sequence\n",
|
|
"from exp_data.root_io import write_spectrum, read_spectrum\n",
|
|
"from exp_data.livetime import LivetimeDetermination\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Configuration"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Paths\n",
|
|
"RAW_FILE = \"../data/raw/A1\"\n",
|
|
"CALIBRATION_FILE = \"../data/raw/CAM1.DAT\"\n",
|
|
"RUN_DIR = \"../data/raw/\"\n",
|
|
"\n",
|
|
"# Pulser parameters (from calibration run metadata)\n",
|
|
"PULSER_FREQUENCY_HZ = 100.0\n",
|
|
"REAL_TIME_SEC = 3600.0"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Raw Spectrum Visualization\n",
|
|
"Visualize raw spectra before calibration to identify peaks and verify calibration source."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Visualize raw spectra before calibration\n",
|
|
"# This helps identify what calibration source is actually present\n",
|
|
"\n",
|
|
"# Detect header size from file\n",
|
|
"def detect_header_size(filepath):\n",
|
|
" \"\"\"Detect MCA header size by checking for binary pattern.\"\"\"\n",
|
|
" with open(filepath, 'rb') as f:\n",
|
|
" header = f.read(128)\n",
|
|
" # Check if first 32 bytes look like a header (small ints)\n",
|
|
" # and bytes 32-128 also look like header\n",
|
|
" # Simple heuristic: if file is exactly 32 + 4096*4 = 16412, header=32\n",
|
|
" # If file is 128 + 4096*4 = 16512, header=128\n",
|
|
" import os\n",
|
|
" size = os.path.getsize(filepath)\n",
|
|
" expected_32 = 32 + 4096 * 4\n",
|
|
" expected_128 = 128 + 4096 * 4\n",
|
|
" if size == expected_32:\n",
|
|
" return 32\n",
|
|
" elif size == expected_128:\n",
|
|
" return 128\n",
|
|
" else:\n",
|
|
" # Try to detect by checking if byte 32 looks like an int\n",
|
|
" with open(filepath, 'rb') as f:\n",
|
|
" f.seek(32)\n",
|
|
" test = f.read(4)\n",
|
|
" return 32 # default\n",
|
|
"\n",
|
|
"# --- Raw run spectrum ---\n",
|
|
"header_size = detect_header_size(RAW_FILE)\n",
|
|
"with open(RAW_FILE, 'rb') as f:\n",
|
|
" f.read(header_size)\n",
|
|
" raw_counts = np.fromfile(f, dtype=np.int32, count=4096)\n",
|
|
"\n",
|
|
"fig, axes = plt.subplots(2, 2, figsize=(14, 8))\n",
|
|
"\n",
|
|
"ax = axes[0, 0]\n",
|
|
"ax.plot(raw_counts, 'b-', linewidth=0.5)\n",
|
|
"ax.set_title('Raw Run Spectrum (counts vs channel)')\n",
|
|
"ax.set_xlabel('Channel')\n",
|
|
"ax.set_ylabel('Counts')\n",
|
|
"ax.set_yscale('log')\n",
|
|
"ax.set_xlim(0, 600)\n",
|
|
"\n",
|
|
"ax = axes[0, 1]\n",
|
|
"ax.plot(raw_counts, 'b-', linewidth=0.5)\n",
|
|
"ax.set_title('Raw Run Spectrum (full range)')\n",
|
|
"ax.set_xlabel('Channel')\n",
|
|
"ax.set_ylabel('Counts')\n",
|
|
"ax.set_yscale('log')\n",
|
|
"\n",
|
|
"# --- Calibration spectrum ---\n",
|
|
"with open(CALIBRATION_FILE, 'rb') as f:\n",
|
|
" f.read(128) # CAM files have 128-byte header\n",
|
|
" cal_counts = np.fromfile(f, dtype=np.int32, count=4096)\n",
|
|
"\n",
|
|
"ax = axes[1, 0]\n",
|
|
"ax.plot(cal_counts, 'r-', linewidth=0.5)\n",
|
|
"ax.set_title('Calibration Spectrum (counts vs channel)')\n",
|
|
"ax.set_xlabel('Channel')\n",
|
|
"ax.set_ylabel('Counts')\n",
|
|
"ax.set_xlim(0, 600)\n",
|
|
"ax.set_yscale('log')\n",
|
|
"\n",
|
|
"ax = axes[1, 1]\n",
|
|
"ax.plot(cal_counts, 'r-', linewidth=0.5)\n",
|
|
"ax.set_title('Calibration Spectrum (full range)')\n",
|
|
"ax.set_xlabel('Channel')\n",
|
|
"ax.set_ylabel('Counts')\n",
|
|
"ax.set_yscale('log')\n",
|
|
"\n",
|
|
"plt.tight_layout()\n",
|
|
"plt.show()\n",
|
|
"\n",
|
|
"# Print some stats\n",
|
|
"print(f'Raw run: {len(raw_counts)} channels, max={raw_counts.max()} at ch {np.argmax(raw_counts)}')\n",
|
|
"print(f'Calibration: {len(cal_counts)} channels, max={cal_counts.max()} at ch {np.argmax(cal_counts)}')\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"# Process Single Spectrum"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"result = process_spectrum(\n",
|
|
" raw_path=RAW_FILE,\n",
|
|
" calibration_path=CALIBRATION_FILE,\n",
|
|
" pulser_frequency_hz=PULSER_FREQUENCY_HZ,\n",
|
|
" real_time_sec=REAL_TIME_SEC,\n",
|
|
")\n",
|
|
"\n",
|
|
"print(f\"Validation: {'PASSED' if result.validation_passed else 'FAILED'}\")\n",
|
|
"print(f\"Calibration chi2/dof: {result.calibration.chi2_per_dof:.4f}\")\n",
|
|
"for msg in result.validation_messages:\n",
|
|
" print(f\" {msg}\")\n",
|
|
"if result.livetime:\n",
|
|
" print(f\"Live-time fraction: {result.livetime.live_time_fraction:.4f}\")\n",
|
|
" print(f\"Pulser peak: {result.livetime.pulser_peak_center_keV:.1f} keV\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Plot Calibrated Spectrum"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"plt.figure(figsize=(10, 6))\n",
|
|
"plt.plot(result.spectrum.energies, result.spectrum.counts, 'b-', label='Counts')\n",
|
|
"plt.xlabel('Energy (keV)')\n",
|
|
"plt.ylabel('Counts')\n",
|
|
"plt.yscale('log')\n",
|
|
"plt.title('Calibrated Beta Spectrum')\n",
|
|
"plt.legend()\n",
|
|
"plt.tight_layout()\n",
|
|
"plt.show()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Process Run Sequence"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"results = process_run_sequence(\n",
|
|
" run_dir=RUN_DIR,\n",
|
|
" calibration_path=CALIBRATION_FILE,\n",
|
|
" prefix=\"A\",\n",
|
|
" pulser_frequency_hz=PULSER_FREQUENCY_HZ,\n",
|
|
" real_time_sec=REAL_TIME_SEC,\n",
|
|
")\n",
|
|
"\n",
|
|
"print(f\"Processed {len(results)} runs\")\n",
|
|
"for i, r in enumerate(results):\n",
|
|
" print(f\" Run {i+1}: {'PASSED' if r.validation_passed else 'FAILED'} \"\n",
|
|
" f\"(chi2/dof={r.calibration.chi2_per_dof:.4f})\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Export to ROOT"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"write_spectrum(\"output.root\", result.spectrum)\n",
|
|
"print(\"Exported to output.root\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Read Back from ROOT"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"read_back = read_spectrum(\"output.root\")\n",
|
|
"print(f\"Read {len(read_back.energies)} channels\")\n",
|
|
"print(f\"Source: {read_back.source}\")\n",
|
|
"print(f\"Run ID: {read_back.run_id}\")\n",
|
|
"print(f\"Calibration: {read_back.metadata.get('calibration', {})}\")\n",
|
|
"\n",
|
|
"# Verify round-trip\n",
|
|
"assert np.allclose(read_back.energies, result.spectrum.energies)\n",
|
|
"assert np.allclose(read_back.counts, result.spectrum.counts)\n",
|
|
"print(\"Round-trip verified\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Livetime Determination"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Direct livetime determination from calibration file\n",
|
|
"livetime_result = LivetimeDetermination.from_file(\n",
|
|
" filepath=CALIBRATION_FILE,\n",
|
|
" pulser_frequency_hz=PULSER_FREQUENCY_HZ,\n",
|
|
" real_time_sec=REAL_TIME_SEC,\n",
|
|
" source=\"pulser\",\n",
|
|
")\n",
|
|
"\n",
|
|
"print(f\"Live-time fraction: {livetime_result.live_time_fraction:.4f}\")\n",
|
|
"print(f\"Observed pulser rate: {livetime_result.observed_pulser_rate:.1f} Hz\")\n",
|
|
"print(f\"Pulser peak: {livetime_result.pulser_peak_center_keV:.1f} keV \"\n",
|
|
" f\"(sigma={livetime_result.pulser_peak_sigma_keV:.2f} keV, \"\n",
|
|
" f\"chi2/dof={livetime_result.pulser_peak_chi2:.2f})\")"
|
|
]
|
|
}
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": "Python 3 (ipykernel)",
|
|
"language": "python",
|
|
"name": "python3"
|
|
},
|
|
"language_info": {
|
|
"codemirror_mode": {
|
|
"name": "ipython",
|
|
"version": 3
|
|
},
|
|
"file_extension": ".py",
|
|
"mimetype": "text/x-python",
|
|
"name": "python",
|
|
"nbconvert_exporter": "python",
|
|
"pygments_lexer": "ipython3",
|
|
"version": "3.14.4"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 4
|
|
}
|