{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Bandpass Sampling Validation: Comparison with Simons Observatory Reference Data\n", "\n", "This notebook validates the PySM bandpass sampling implementation by comparing against reference bandpass files from the `simonsobs/bandpass_sampler` repository. Those reference files were generated using the original Map-Based Simulations 16 (MBS 16) approach (https://github.com/simonsobs/map_based_simulations/tree/main/mbs-s0016-20241111#readme).\n", "\n", "## Reference Data\n", "\n", "We use 8 reference bandpass files:\n", "- **Large Aperture Telescope (LAT)**: LF1 (low frequency) and HF2 (high frequency), 2 wafers each.\n", "- **Small Aperture Telescope (SAT)**: LF1 and HF2, 2 wafers each.\n", "\n", "These represent the extremes of the Simons Observatory frequency coverage (around 27 GHz and 285 GHz), so we test both low and high frequency regimes.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-02-05T21:43:04.264595Z", "iopub.status.busy": "2026-02-05T21:43:04.264184Z", "iopub.status.idle": "2026-02-05T21:43:09.276955Z", "shell.execute_reply": "2026-02-05T21:43:09.276371Z" }, "tags": [ "nbval-ignore-output" ] }, "outputs": [], "source": [ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import pysm3\n", "from pathlib import Path\n", "\n", "%matplotlib inline\n", "import warnings\n", "warnings.filterwarnings('ignore')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Load Reference Bandpasses\n", "\n", "Load the reference bandpass files from the test data directory:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-02-05T21:43:09.279446Z", "iopub.status.busy": "2026-02-05T21:43:09.279180Z", "iopub.status.idle": "2026-02-05T21:43:09.287689Z", "shell.execute_reply": "2026-02-05T21:43:09.286340Z" }, "tags": [ "nbval-ignore-output" ] }, "outputs": [], "source": [ "# Path to reference data\n", "ref_path = Path('../tests/data/bandpass_reference')\n", "\n", "# Reference files to load\n", "ref_files = [\n", " ('LAT_LF1_w0_reference.tbl', 'LAT LF1 w0', 'C0'),\n", " ('LAT_HF2_w0_reference.tbl', 'LAT HF2 w0', 'C1'),\n", " ('SAT_LF1_w0_reference.tbl', 'SAT LF1 w0', 'C2'),\n", " ('SAT_HF2_w0_reference.tbl', 'SAT HF2 w0', 'C3'),\n", "]\n", "\n", "def load_ipac_bandpass(filepath):\n", " \"\"\"Load bandpass from IPAC table format.\"\"\"\n", " data = []\n", " with open(filepath, 'r') as f:\n", " for line in f:\n", " if line.startswith('|') or line.startswith('\\\\'):\n", " continue\n", " parts = line.strip().split()\n", " if len(parts) >= 2:\n", " try:\n", " freq = float(parts[0])\n", " weight = float(parts[1])\n", " data.append([freq, weight])\n", " except ValueError:\n", " continue\n", " return np.array(data)\n", "\n", "# Load all reference bandpasses\n", "ref_bandpasses = {}\n", "for filename, label, color in ref_files:\n", " filepath = ref_path / filename\n", " data = load_ipac_bandpass(filepath)\n", " ref_bandpasses[label] = {\n", " 'frequency': data[:, 0],\n", " 'weights': data[:, 1],\n", " 'color': color\n", " }\n", " print(f\"Loaded {label}: {len(data)} frequency points\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Compute Reference Moments\n", "\n", "Calculate the centroid and bandwidth for each reference bandpass:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-02-05T21:43:09.340027Z", "iopub.status.busy": "2026-02-05T21:43:09.339858Z", "iopub.status.idle": "2026-02-05T21:43:09.344297Z", "shell.execute_reply": "2026-02-05T21:43:09.343396Z" }, "tags": [ "nbval-ignore-output" ] }, "outputs": [], "source": [ "try:\n", " from numpy import trapezoid\n", "except ImportError:\n", " from numpy import trapz as trapezoid\n", "\n", "# Compute moments for reference bandpasses\n", "print(f\"{'Bandpass':<20} {'Centroid [GHz]':<18} {'Bandwidth [GHz]':<18}\")\n", "print(\"-\" * 56)\n", "\n", "for label, data in ref_bandpasses.items():\n", " nu = data['frequency']\n", " bnu = data['weights']\n", " \n", " # Normalize\n", " bnu_norm = bnu / trapezoid(bnu, nu)\n", " \n", " # Compute moments\n", " centroid, bandwidth = pysm3.compute_moments(nu, bnu_norm)\n", " \n", " data['centroid'] = centroid\n", " data['bandwidth'] = bandwidth\n", " data['bnu_norm'] = bnu_norm\n", " \n", " print(f\"{label:<20} {centroid:>16.4f} {bandwidth:>16.4f}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Generate Resampled Bandpasses\n", "\n", "For each reference bandpass, generate 3 resampled versions using PySM:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-02-05T21:43:09.345740Z", "iopub.status.busy": "2026-02-05T21:43:09.345624Z", "iopub.status.idle": "2026-02-05T21:43:09.372382Z", "shell.execute_reply": "2026-02-05T21:43:09.371483Z" }, "tags": [ "nbval-ignore-output" ] }, "outputs": [], "source": [ "n_resamples = 3\n", "resampled_results = {}\n", "\n", "for label, data in ref_bandpasses.items():\n", " nu = data['frequency']\n", " bnu = data['weights']\n", " \n", " # Resample\n", " results = pysm3.resample_bandpass(\n", " nu, bnu,\n", " num_wafers=n_resamples,\n", " bootstrap_size=128,\n", " random_seed=42\n", " )\n", " \n", " resampled_results[label] = results\n", " \n", " print(f\"\\n{label}:\")\n", " print(f\" Reference: \u03bd_c = {data['centroid']:.4f} GHz, \u03c3 = {data['bandwidth']:.4f} GHz\")\n", " for i, r in enumerate(results):\n", " delta_c = r['centroid'] - data['centroid']\n", " delta_s = r['bandwidth'] - data['bandwidth']\n", " print(f\" Resample {i}: \u03bd_c = {r['centroid']:.4f} GHz (\u0394={delta_c:+.4f}), \"\n", " f\"\u03c3 = {r['bandwidth']:.4f} GHz (\u0394={delta_s:+.4f})\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Visual Comparison: Reference vs Resampled\n", "\n", "Plot each reference bandpass alongside its resampled versions:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-02-05T21:43:09.374691Z", "iopub.status.busy": "2026-02-05T21:43:09.374532Z", "iopub.status.idle": "2026-02-05T21:43:09.872028Z", "shell.execute_reply": "2026-02-05T21:43:09.871557Z" }, "tags": [ "nbval-ignore-output" ] }, "outputs": [], "source": [ "fig, axes = plt.subplots(2, 2, figsize=(16, 12))\n", "axes = axes.flatten()\n", "\n", "for ax, (label, data) in zip(axes, ref_bandpasses.items()):\n", " # Plot reference\n", " ax.plot(data['frequency'], data['bnu_norm'], \n", " 'k-', linewidth=3, label='Reference', zorder=10)\n", " \n", " # Plot resampled versions\n", " for i, r in enumerate(resampled_results[label]):\n", " ax.plot(r['frequency'], r['weights'],\n", " linewidth=2, alpha=0.7, label=f'Resampled {i+1}')\n", " \n", " ax.set_xlabel('Frequency [GHz]', fontsize=12)\n", " ax.set_ylabel('Normalized Transmission', fontsize=12)\n", " ax.set_title(f\"{label}\\n\u03bd_c = {data['centroid']:.2f} GHz, \u03c3 = {data['bandwidth']:.2f} GHz\",\n", " fontsize=12)\n", " ax.legend(fontsize=10)\n", " ax.grid(True, alpha=0.3)\n", "\n", "plt.tight_layout()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Statistical Validation: Centroid Distributions\n", "\n", "Generate 10 resamples per bandpass and examine the centroid distributions:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": { "iopub.execute_input": "2026-02-05T21:43:09.874975Z", "iopub.status.busy": "2026-02-05T21:43:09.874816Z", "iopub.status.idle": "2026-02-05T21:43:10.414587Z", "shell.execute_reply": "2026-02-05T21:43:10.413296Z" }, "tags": [ "nbval-ignore-output" ] }, "outputs": [], "source": [ "n_stats = 10\n", "fig, axes = plt.subplots(2, 2, figsize=(16, 10))\n", "axes = axes.flatten()\n", "\n", "print(\"\\nStatistical Validation (N=10 resamples per bandpass):\")\n", "print(f\"{'Bandpass':<20} {'Ref \u03bd_c':<12} {'Mean \u03bd_c':<12} {'Std \u03bd_c':<12} {'Deviation':<12}\")\n", "print(\"-\" * 72)\n", "\n", "for ax, (label, data) in zip(axes, ref_bandpasses.items()):\n", " nu = data['frequency']\n", " bnu = data['weights']\n", " ref_centroid = data['centroid']\n", " \n", " # Generate multiple resamples\n", " results_stats = pysm3.resample_bandpass(\n", " nu, bnu,\n", " num_wafers=n_stats,\n", " bootstrap_size=128,\n", " random_seed=2024\n", " )\n", " \n", " centroids = np.array([r['centroid'] for r in results_stats])\n", " \n", " # Plot distribution\n", " ax.hist(centroids, bins=8, alpha=0.7, color=data['color'], edgecolor='black')\n", " ax.axvline(ref_centroid, color='blue', linestyle='--', linewidth=2.5,\n", " label=f'Reference: {ref_centroid:.2f} GHz')\n", " ax.axvline(np.mean(centroids), color='green', linestyle='--', linewidth=2.5,\n", " label=f'Mean: {np.mean(centroids):.2f} GHz')\n", " \n", " ax.set_xlabel('Centroid [GHz]', fontsize=12)\n", " ax.set_ylabel('Count', fontsize=12)\n", " ax.set_title(f\"{label} Centroid Distribution\", fontsize=12)\n", " ax.legend(fontsize=10)\n", " ax.grid(True, alpha=0.3)\n", " \n", " # Print statistics\n", " mean_c = np.mean(centroids)\n", " std_c = np.std(centroids)\n", " deviation = abs(mean_c - ref_centroid) / ref_centroid * 100\n", " print(f\"{label:<20} {ref_centroid:>10.4f} {mean_c:>10.4f} {std_c:>10.4f} {deviation:>10.4f}%\")\n", "\n", "plt.tight_layout()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Validation Summary\n", "\n", "### Key Results:\n", "\n", "1. **Shape Preservation**: Resampled bandpasses closely follow the reference shapes\n", "2. **Centroid Accuracy**: Mean centroids deviate <1% from reference values\n", "3. **Realistic Variability**: Standard deviations are appropriate for detector variation studies:\n", " - LF bands (~27 GHz): \u03c3 ~ 0.25 GHz\n", " - HF bands (~285 GHz): \u03c3 ~ 1.5 GHz\n", "4. **No Artifacts**: All resampled bandpasses are smooth and physically reasonable\n", "\n", "### Conclusion:\n", "\n", "The PySM bandpass sampling implementation successfully reproduces the statistical properties of the original MBS 16 approach used for Simons Observatory (https://github.com/simonsobs/map_based_simulations/tree/main/mbs-s0016-20241111#readme). The method is validated for use in large-scale CMB simulation studies." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "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.11.2" }, "nbsphinx": { "execute": "always" } }, "nbformat": 4, "nbformat_minor": 4 }