Note
This page is generated from the Jupyter notebook
notebooks/03_from_sensorfilter.ipynb in the
repository. You can download it and run it interactively.
from_sensorfilter — auto-selecting the PSF from the filter focus level
The WCC ETC supports building a simulation directly from a canonical sensorfilter label (kind:band, e.g. 'zwo:r') with Simulation.from_sensorfilter and ImageSimulator.from_sensorfilter.
Unlike from_sensor_and_scene, these classmethods do two things automatically:
Pick the throughput curve for that sensor/filter combination.
Pick the default PSF from the filter’s
focus_level, storing it on_default_psf.
Focus levels map to PSFs as follows:
|
PSF |
example label |
|---|---|---|
|
|
|
|
|
|
|
|
|
Because the PSF is stored on the simulation, get_image_snr(time=...) uses the right PSF without any ``psf=`` argument. Defocusing spreads the star over more pixels, so at a fixed aperture the SNR drops relative to the in-focus case.
Imports and scene setup
We use a single G5V source at r = 15 against a zodiacal background.
[ ]:
import matplotlib.pyplot as plt
import wcc_etc
wcc_etc.set_wcc_style()
from wcc_etc import AiryPSF, DefocusPSF
def make_scene(mag=15):
"""G5V source on a zodi background, johnson_r bandpass."""
return wcc_etc.get_scene(
name="G5V",
mag=mag,
background="zodi",
bandpass="johnson_r",
background_prop={"bandpass": "johnson_r", "mag": 22.5},
)
scene = make_scene(mag=15)
scene
1. In-focus filter (zwo:r) → AiryPSF
Build a Simulation from the canonical in-focus label 'zwo:r'. Its focus_level is 0wave, so from_sensorfilter stores an AiryPSF as the default.
[ ]:
sim_infocus = wcc_etc.Simulation.from_sensorfilter("zwo:r", make_scene(15))
print("label : zwo:r")
print("_default_psf :", type(sim_infocus._default_psf).__name__)
assert isinstance(sim_infocus._default_psf, AiryPSF)
print("is AiryPSF :", isinstance(sim_infocus._default_psf, AiryPSF))
2. Defocused filters (zwo:r+1, zwo:bb2) → DefocusPSF
The +1 / -1 / 2 suffixes on a label mark a defocused sensor. from_sensorfilter auto-selects the matching DefocusPSF (1-wave or 2-wave).
[ ]:
builds = {
"zwo:r": wcc_etc.Simulation.from_sensorfilter("zwo:r", make_scene(15)),
"zwo:r+1": wcc_etc.Simulation.from_sensorfilter("zwo:r+1", make_scene(15)),
"zwo:bb2": wcc_etc.Simulation.from_sensorfilter("zwo:bb2", make_scene(15)),
}
print(f"{'label':<10}{'focus':<8}{'_default_psf'}")
print("-" * 34)
labels_focus = {"zwo:r": "0wave", "zwo:r+1": "1wave", "zwo:bb2": "2wave"}
for label, sim in builds.items():
print(f"{label:<10}{labels_focus[label]:<8}{type(sim._default_psf).__name__}")
assert isinstance(builds["zwo:r+1"]._default_psf, DefocusPSF)
assert isinstance(builds["zwo:bb2"]._default_psf, DefocusPSF)
3. Visualizing the auto-selected PSFs
We render a bright (r = 11) point source through each build with ImageSimulator.from_sensorfilter, passing the auto-selected _default_psf to simulate. The in-focus image is a tight Airy core; the 1-wave and 2-wave defocus images spread the light into progressively larger doughnuts.
[ ]:
panels = [
("zwo:r", "in-focus (AiryPSF)"),
("zwo:r+1", "1-wave defocus (DefocusPSF)"),
("zwo:bb2", "2-wave defocus (DefocusPSF)"),
]
fig, axes = plt.subplots(1, 3, figsize=(13, 4.4))
for ax, (label, title) in zip(axes, panels):
img = wcc_etc.ImageSimulator.from_sensorfilter(
label, make_scene(11), npix=160, oversample=5
)
sim_img = img.simulate(time=5, psf=img.sim._default_psf, add_noise=False)
# SimulatedImage.plot_image draws straight into our axis (noiseless, log stretch)
sim_img.plot_image(
ax=ax,
noise=False,
stretch="log",
cmap="magma",
colorbar=False,
title=f"{label}\n{title}",
)
fig.suptitle("Auto-selected PSF per focus level (log stretch)", fontsize=13)
plt.tight_layout()
plt.show()
4. get_image_snr uses _default_psf automatically
No psf= argument is needed: get_image_snr(time=60) picks up whatever PSF from_sensorfilter stored. At a fixed aperture, spreading the flux out via defocus lowers the SNR — zwo:r > zwo:r+1 > zwo:bb2.
[ ]:
time = 60 # seconds
print(f"{'label':<10}{'PSF':<14}{'SNR (60 s, no psf= arg)'}")
print("-" * 46)
snr_results = {}
for label in ["zwo:r", "zwo:r+1", "zwo:bb2"]:
sim = wcc_etc.Simulation.from_sensorfilter(label, make_scene(15))
snr = sim.get_image_snr(time=time)["snr"] # uses _default_psf automatically
snr_results[label] = snr
print(f"{label:<10}{type(sim._default_psf).__name__:<14}{snr:8.2f}")
print()
print(
f"in-focus / 1-wave SNR ratio: {snr_results['zwo:r'] / snr_results['zwo:r+1']:.2f}x"
)
print(
f"in-focus / 2-wave SNR ratio: {snr_results['zwo:r'] / snr_results['zwo:bb2']:.2f}x"
)
assert snr_results["zwo:r"] > snr_results["zwo:r+1"] > snr_results["zwo:bb2"]
5. Guards
from_sensorfilter only accepts canonical labels and only those with a throughput curve:
An unknown label raises
ValueError.A known-but-unimplemented label (a narrowband filter with no throughput curve yet, e.g.
zwo:halpha) raisesNotImplementedError.
Note nicknames like 'sony:r' are not valid here — use from_sensor_and_scene for those.
[ ]:
try:
wcc_etc.Simulation.from_sensorfilter("zwo:does_not_exist", make_scene(15))
except ValueError as e:
print("ValueError :", e)
try:
wcc_etc.Simulation.from_sensorfilter("zwo:halpha", make_scene(15))
except NotImplementedError as e:
print("NotImplementedError:", e)
Summary
Simulation.from_sensorfilter(label, scene)andImageSimulator.from_sensorfilter(label, scene, npix=..., oversample=...)build from a canonicalkind:bandlabel.The PSF is chosen from the filter’s
focus_leveland stored on_default_psf(AiryPSFfor0wave;DefocusPSFfor1wave/2wave).get_image_snr/get_image_exptime_for_snruse_default_psfautomatically — nopsf=needed — and defocus lowers SNR at fixed aperture.Unknown labels raise
ValueError; known-but-unimplemented labels raiseNotImplementedError. For nicknames ('sony:r'), usefrom_sensor_and_scene.