{ "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", "matplotlib.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" ] }, { "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_xlim(0, min(200, len(raw_counts)))\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", "\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, min(200, len(cal_counts)))\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", "\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.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 }