{ "cells": [ { "cell_type": "markdown", "id": "88d462cb", "metadata": {}, "source": [ "# Saturation flag demo\n", "\n", "Demonstrates the detector **saturation flag** (ADU clip).\n", "\n", "A source saturates when its **brightest pixel** reaches the detector ADC full scale\n", "(`adc_max = 2**bit_depth - 1`, in ADU). The peak-pixel value combines the source PSF\n", "peak, the per-pixel sky background, the per-pixel dark current, and the additive bias.\n", "\n", "Key API:\n", "- `sensor.bit_depth`, `sensor.bias_level`, `sensor.adc_max`\n", "- `sim.peak_pixel_fraction()` — fraction of total source energy in the brightest pixel\n", "- `sim.get_peak_pixel(time, units='adu')`\n", "- `sim.is_saturated(time)`" ] }, { "cell_type": "code", "execution_count": null, "id": "f8f42ba0", "metadata": { "execution": { "iopub.execute_input": "2026-06-18T13:15:04.364607Z", "iopub.status.busy": "2026-06-18T13:15:04.364311Z", "iopub.status.idle": "2026-06-18T13:15:06.807024Z", "shell.execute_reply": "2026-06-18T13:15:06.806666Z" } }, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "import numpy as np\n", "\n", "import wcc_etc\n", "\n", "wcc_etc.set_wcc_style()" ] }, { "cell_type": "markdown", "id": "49351420", "metadata": {}, "source": [ "## Build a scene and simulation\n", "\n", "A moderately bright star on the ZWO/Sony sensor (16-bit) in the r band." ] }, { "cell_type": "code", "execution_count": null, "id": "f50a235f", "metadata": { "execution": { "iopub.execute_input": "2026-06-18T13:15:06.808570Z", "iopub.status.busy": "2026-06-18T13:15:06.808468Z", "iopub.status.idle": "2026-06-18T13:15:07.074215Z", "shell.execute_reply": "2026-06-18T13:15:07.073930Z" } }, "outputs": [], "source": [ "scene = wcc_etc.get_scene(\n", " name=\"G5V\",\n", " mag=15,\n", " host=None,\n", " background=\"zodi\",\n", " bandpass=\"johnson_r\",\n", " background_prop={\"bandpass\": \"johnson_r\", \"mag\": 22.5},\n", ")\n", "\n", "sim = wcc_etc.Simulation.from_sensor_and_scene(\"sony:r\", scene)" ] }, { "cell_type": "markdown", "id": "a8151085", "metadata": {}, "source": [ "## Sensor ADC properties\n", "\n", "`adc_max` is the clip ceiling in ADU (`u.ct`)." ] }, { "cell_type": "code", "execution_count": null, "id": "da28d6a5", "metadata": { "execution": { "iopub.execute_input": "2026-06-18T13:15:07.075676Z", "iopub.status.busy": "2026-06-18T13:15:07.075592Z", "iopub.status.idle": "2026-06-18T13:15:07.077470Z", "shell.execute_reply": "2026-06-18T13:15:07.077279Z" } }, "outputs": [], "source": [ "print(f\"bit_depth : {sim.sensor.bit_depth}\")\n", "print(f\"adc_max : {sim.sensor.adc_max}\")\n", "print(f\"bias_level : {sim.sensor.bias_level}\")\n", "print(f\"gain : {sim.sensor.gain}\")" ] }, { "cell_type": "markdown", "id": "cde26247", "metadata": {}, "source": [ "## Peak-pixel fraction\n", "\n", "The fraction of the total source energy that lands in the single brightest pixel,\n", "from the detector-grid PSF render. (Depends on wavelength, f-number, pixel size, and jitter.)" ] }, { "cell_type": "code", "execution_count": null, "id": "7b81b7d7", "metadata": { "execution": { "iopub.execute_input": "2026-06-18T13:15:07.078517Z", "iopub.status.busy": "2026-06-18T13:15:07.078452Z", "iopub.status.idle": "2026-06-18T13:15:07.185500Z", "shell.execute_reply": "2026-06-18T13:15:07.185271Z" } }, "outputs": [], "source": [ "frac = sim.peak_pixel_fraction()\n", "print(\n", " f\"peak_pixel_fraction = {frac:.4f} ({100 * frac:.1f}% of the source flux in the brightest pixel)\"\n", ")" ] }, { "cell_type": "markdown", "id": "b351e171", "metadata": {}, "source": [ "## Single-exposure check" ] }, { "cell_type": "code", "execution_count": null, "id": "863f02fa", "metadata": { "execution": { "iopub.execute_input": "2026-06-18T13:15:07.186706Z", "iopub.status.busy": "2026-06-18T13:15:07.186648Z", "iopub.status.idle": "2026-06-18T13:15:07.188901Z", "shell.execute_reply": "2026-06-18T13:15:07.188683Z" } }, "outputs": [], "source": [ "for t in [1, 10, 60]:\n", " peak = sim.get_peak_pixel(t, units=\"adu\")\n", " print(\n", " f\"t = {t:>4} s peak = {peak.value:12.1f} ADU saturated = {bool(sim.is_saturated(t))}\"\n", " )" ] }, { "cell_type": "markdown", "id": "cfe17e22", "metadata": {}, "source": [ "## Sweep exposure time\n", "\n", "`get_peak_pixel` and `is_saturated` broadcast over an array of times." ] }, { "cell_type": "code", "execution_count": null, "id": "18348d84", "metadata": { "execution": { "iopub.execute_input": "2026-06-18T13:15:07.189998Z", "iopub.status.busy": "2026-06-18T13:15:07.189940Z", "iopub.status.idle": "2026-06-18T13:15:07.393069Z", "shell.execute_reply": "2026-06-18T13:15:07.392860Z" } }, "outputs": [], "source": [ "texp = np.logspace(-1, 3, 200) # 0.1 s to 1000 s\n", "peak_adu = sim.get_peak_pixel(texp, units=\"adu\").value\n", "saturated = sim.is_saturated(texp)\n", "adc_max = sim.sensor.adc_max.value\n", "\n", "# first exposure time that saturates\n", "t_sat = texp[saturated][0] if saturated.any() else None\n", "\n", "fig, ax = plt.subplots(figsize=(7, 4))\n", "ax.loglog(texp, peak_adu, label=\"peak pixel\")\n", "ax.axhline(adc_max, color=\"crimson\", ls=\"--\", label=f\"adc_max = {adc_max:.0f} ADU\")\n", "if t_sat is not None:\n", " ax.axvline(t_sat, color=\"k\", ls=\":\", label=f\"saturates at ~{t_sat:.2g} s\")\n", "ax.set_xlabel(\"Exposure time [s]\")\n", "ax.set_ylabel(\"Peak pixel [ADU]\")\n", "ax.set_title(\"Brightest-pixel value vs exposure time (G5V, r=15, sony:r)\")\n", "ax.legend()\n", "ax.grid(True, which=\"both\", lw=0.3, alpha=0.4)\n", "plt.show()\n", "\n", "print(\n", " \"saturates at ~ {:.3g} s\".format(t_sat)\n", " if t_sat is not None\n", " else \"never saturates in this range\"\n", ")" ] }, { "cell_type": "markdown", "id": "f026b3a0", "metadata": {}, "source": [ "## Effect of source brightness\n", "\n", "Brighter stars (lower magnitude) saturate sooner." ] }, { "cell_type": "code", "execution_count": null, "id": "927c3673", "metadata": { "execution": { "iopub.execute_input": "2026-06-18T13:15:07.394308Z", "iopub.status.busy": "2026-06-18T13:15:07.394220Z", "iopub.status.idle": "2026-06-18T13:15:07.793022Z", "shell.execute_reply": "2026-06-18T13:15:07.792755Z" } }, "outputs": [], "source": [ "fig, ax = plt.subplots(figsize=(7, 4))\n", "for mag in [12, 15, 18]:\n", " sim.update(source__mag=mag)\n", " peak = sim.get_peak_pixel(texp, units=\"adu\").value\n", " sat = sim.is_saturated(texp)\n", " t_s = texp[sat][0] if sat.any() else None\n", " label = f\"r = {mag}\" + (f\" (sat ~{t_s:.2g} s)\" if t_s is not None else \" (no sat)\")\n", " ax.loglog(texp, peak, label=label)\n", "\n", "ax.axhline(adc_max, color=\"crimson\", ls=\"--\", label=\"adc_max\")\n", "ax.set_xlabel(\"Exposure time [s]\")\n", "ax.set_ylabel(\"Peak pixel [ADU]\")\n", "ax.set_title(\"Saturation onset vs source magnitude (sony:r)\")\n", "ax.legend()\n", "ax.grid(True, which=\"both\", lw=0.3, alpha=0.4)\n", "plt.show()\n", "\n", "# restore\n", "sim.update(source__mag=15);" ] }, { "cell_type": "markdown", "id": "7edf2bff", "metadata": {}, "source": [ "## Sensor comparison: 16-bit vs 12-bit\n", "\n", "The qcmos sensor is 12-bit (`adc_max = 4095`), so it clips far sooner than the 16-bit ZWO/Sony\n", "for the same source — lower full-scale, and a different gain." ] }, { "cell_type": "code", "execution_count": null, "id": "9f2aa600", "metadata": { "execution": { "iopub.execute_input": "2026-06-18T13:15:07.794306Z", "iopub.status.busy": "2026-06-18T13:15:07.794238Z", "iopub.status.idle": "2026-06-18T13:15:07.975151Z", "shell.execute_reply": "2026-06-18T13:15:07.974883Z" } }, "outputs": [], "source": [ "sim_q = wcc_etc.Simulation.from_sensor_and_scene(\"qcmos:r\", scene)\n", "\n", "for label, s in [(\"sony:r (16-bit)\", sim), (\"qcmos:r (12-bit)\", sim_q)]:\n", " peak = s.get_peak_pixel(texp, units=\"adu\").value\n", " sat = s.is_saturated(texp)\n", " t_s = texp[sat][0] if sat.any() else None\n", " t_s_str = f\"{t_s:.3g} s\" if t_s is not None else \"no sat in range\"\n", " print(\n", " f\"{label:>18} adc_max = {s.sensor.adc_max.value:7.0f} ADU saturates at ~ {t_s_str}\"\n", " )" ] }, { "cell_type": "code", "execution_count": null, "id": "d96839b6", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "py313", "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.13.5" } }, "nbformat": 4, "nbformat_minor": 5 }