Changelog
v0.1.0 (2026-10-12)
First PyPI release. Everything in this section, through the initial highlights at the end, ships in it.
lazuli-exois now required from PyPI (>=0.1.2) instead of a GitHub tag, which removes the direct-URL dependency that PyPI refuses to host (#107).The user guide is rewritten for a first-time reader: the SNR page states the noise model and shows the source-limited and background-limited regimes, the saturation and scenes pages gain figures drawn with the new plot methods, and change-history asides are gone from the prose.
The tutorial notebooks are committed with their outputs, so the figures and printed numbers are visible on GitHub without running them; the pre-commit hook that stripped outputs is gone.
CITATION.cffis removed until there is something to cite.wcc_etc.ioexposesSENSORFILTER_FOCUS,SENSORFILTER_IMPLEMENTEDandKIND_NAMESas public names (in__all__, documented). The underscore spellings remain as aliases so wcc-sim keeps importing until it moves to the public ones.Simulation-level plot methods, each taking
ax=Noneand returning(fig, ax):Simulation.plot_snr(aliasplot),plot_snr_vs_mag,plot_sweep,plot_peak_pixel(with thesaturation_eceiling and onset marker),plot_exptime_for_snr,plot_snr_vs_aperture, andImageSimulator.plot_psf.Sensor.plot,SceneElement.plotandSimulatedImage.plotalias the existingshow/plot_image;Sensor.showandSceneElement.shownow return(fig, ax)like the rest (they returned the bare figure). The image plotters acceptunits="arcsec"alongsidemasandpix, andstretch="log"is now a matplotlibLogNorm(clipped, so read-noise-negative pixels keep the floor colour) whose floor sitsdyn_range(default 1e4) below the peak;vmin=/vmax=fold into it.plottingdeclares__all__. The tutorials use the new methods instead of hand-rolled matplotlib, and their text now matches the code: saturation issensor.saturation_e(the full well on the Sony, the ADC on the qCMOS in high gain), the H-alpha labels arezwo:halpha*, andupdate()with an unknown key raises. The installation, concepts, configuration and API-reference pages are brought up to date.The two Huygens defocus
.txtexports are removed from the repository; the FITS products are the only bundled form (DEFOCUS_*_TXT_PATHis gone). Tests that read a raw export use a small synthetic one written totmp_path.One saturation ceiling:
Sensor.saturation_e= min(full well, (adc_max - bias) * gain), withSensor.saturation_limitreporting which binds ("well"/"adc").Simulation.is_saturated,psfsim.saturation_mask_from_image_eandImageSimulator.simulateall compare electrons to it. Previouslyis_saturatedtested only the ADC clip, so the Sony IMX455 reported unsaturated between the ~16.3 ke- well and the ~17.1 ke- ADC ceiling, and the image mask ignored bias (#64).Packaging:
MANIFEST.inis an explicit allowlist andpackage-datanames only the two bundled defocus PSF FITS, so a build no longer depends on what is in the developer’s working tree (188 MB from a dev tree before). The unuseddata/flats/(36 MB) and the Picklesdat_uvi/atlas are removed;hsiao07.datis now the phase-0 slice (2 columns, 130 kB instead of 8 MB) and loads withget_scene("hsiao07", ...)instead of raising. Wheel and sdist are 8 MB; CI fails a build over 20 MB or carrying local products (#62, #101).DATA_SOURCES.mdlists every bundled dataset with its citation or origin;radial_data.pycarries its attribution (#101).Release hygiene (#101): the CALSPEC Vega spectrum and the Johnson V curve are bundled (
data/astrophysics/alpha_lyr_stis_011.fits,data/throughput/other_filters/johnson_v_004_syn.fits) instead of downloaded from STScI at first use, so zero points cannot drift and no network is needed;io.PACKAGE_PATHuses the publicimportlib.resourcesAPI; the unusedflaskdependency is gone and the others carry minimum versions; the README clone link is HTTPS (#4) and a “Known limitations” section lists #98, #99, #100, #104; the stale mkdocs files anddocs/superpowers/are removed; this file is the one changelog (the Sphinx page includes it).Input guards for inputs that used to give a plausible wrong answer (#102):
Simulation.update()raisesKeyErroron an unknown or ambiguous key instead of warning and ignoring it; a file spectrum that does not cover the bandpass warns with the uncovered Angstrom range (the edge value is still extrapolated); ansdss_*normalization band without an explicitmagsyswarns that SDSS magnitudes are AB;Sensor(qe=...)other than 1 raises (QE lives in the throughput file);parse_and_interpolatewarns when the ZWO gain / read-noise / dark-current tables are clamped outside their range, and a zwo config withoutsensor_tempraises a clearValueErrorinstead of anAttributeError.Tag-driven PyPI publishing (
publish.yml):v*rc*tags go to TestPyPI, plainvX.Y.Ztags to PyPI, via trusted publishing. CI checks the tag against thepyproject.tomlversion, rejects direct-URL dependencies, audits the artifacts and smoke-tests the installed wheel (#108).Sersic hosts: the 5x5 pixels around the cusp are now integrated exactly (adaptive quadrature split at the cusp) instead of sampled on the
oversamplesub-grid, which overshot the analytic total by up to 3x (n=4) and 4 orders of magnitude (n=8) for hosts with r_eff below a pixel. On-grid totals are now within ~1e-4 of analytic for n in [0.5, 8] and r_eff >= 0.18 pixel, and never exceed 1 (#84).One centre convention for PSFs and hosts: a render with
center=Noneputs the source on the integer pixel(npix-1)//2(psfsim.grid_center), on odd and even grids;recentershifts from the PSF’s actual centre, socenter=(cx, cy)is honoured to sub-pixel precision for Airy and custom PSFs alike; andextended_rate_imageplaces the host by convolution with the source PSF, so a zero-offset host sits on the source (it was half a pixel off on even grids, including the default 128-pixel SNR grid) (#84).Sersic-profile hosts (structured background):
host_prop={"profile": "sersic", "r_eff": 1.0, "n": 1, "ellip": 0.3, "pa": 45, "dx": 0.8, "dy": 0}with angular parameters (arcsec / degrees).magis the total host magnitude, or μ_e (mag/arcsec² at r_eff) withsurface_brightness=True. The profile is rendered and PSF-convolved once and shared byImageSimulator.simulate,get_snr,get_image_exptime_for_snr,get_peak_pixelandis_saturated. New modulewcc_etc.extended; the deprecated analytic Airy path warns that it ignores profiles.
Earlier in this cycle
The qCMOS (HWK4123) now carries its two gain modes.
qcmos.tomlhas a[sensor.gain_modes.<mode>]table per mode and again_modedefault of"high"(32x: 7.42 ADU/e, 0.25 e- read noise);"low"(1x: 0.242 ADU/e, 2.33 e- read noise) is selected withgain_mode="low"onfrom_sensorfilter,from_sensor_and_sceneorfrom_name. The 12-bit ADC is real: in high gain it clips at 552 e-, far below the 7500 e- full well (was 7000), so faint-source “saturation” there is physical; low gain is well-limited. Gain is stored in e-/ADU with its provenance stated; the oldgain = 0.112was a single unlabelled value (#94, #61).Both sensor configs carry an informational
min_frame_time(s): 0.25 for a full-frame IMX455 read, 0.008 for the qCMOS at 125 fps. Nothing enforces it yet.Defocused filters (
zwo:r+1,zwo:bb2, …) no longer use the 70 mas Airy-core aperture by default, which held only 2 to 8 percent of the flux and under-reported SNR by 3 to 7x. With no aperture argument the aperture now follows the PSF: 70 mas for the Airy PSF, the SNR-optimized radius otherwise.Simulation(r_aper_mas=...)still pins a fixed radius. The defaultr_aper_masisNone;sim.metaonly carries it when set (#95).get_scene("emission", mag=..., lines=...)now raises instead of silently renormalizing the absolute line fluxes to the magnitude (a 400x error for a 1e-15 erg/s/cm^2 line).get_scene_element("emission", ...)defaults tomag=None, andsurface_brightness=Truewithmag=Nonescales the per-arcsec^2 spectrum by the pixel area instead of crashing (#96).The x2 read-noise margin on the Sony/ZWO path is now the config key
read_noise_margin(2.0 inzwo.tomlandqcmos.toml, whoseread_noiseis back to the measured 0.28 e-) instead of a hidden factor inSensor.from_config. Results are unchanged.get_snrreports the effective per-pixel read noise asread_noise_e(#97).
H-alpha filters
Three H-alpha bands on the Sony (IMX455) sensors now have EOL telescope + WCC throughput curves and are usable with
from_sensorfilter:zwo:halpha2(2 nm, position 10),zwo:halpha6(6 nm, position 9, formerly the N-II placeholder) andzwo:halpha20(20 nm, position 21, formerly the O-III placeholder). Thenii/oiiikeys are removed;hbeta/heiiremain unimplemented placeholders.
Plotting and PSF introspection
New public API, each replacing something the docs or a notebook previously had to hand-roll:
default_psf— the PSF used whenpsf=is omitted, replacing reads of the private_default_psf. Falls back to a diffraction-limitedAiryPSF. Every internal call site, includingpolychromatic_psf, now goes through it.render_psf— the bare normalized PSF on the detector grid, replacing manual construction of the private render context. It builds that context the same waysimulatedoes, so it honours the source-weighted effective wavelength from issue #65.plate_scale_masanddefault_psf.plot_bandpass_mplandshow— filter throughput curves.label=and**kwargspassthrough onplot_radial_mpl,plot_encircled_energy_mplandplot_bandpass_mpl, so overlaid curves can carry a legend without reaching intoax.lines.wave=andflux_unit=onget_spectrumandshow, so a source can be plotted over a chosen grid in F_lambda rather than only PHOTLAM.
Behaviour change
simulatenow defaults todefault_psfinstead of alwaysAiryPSF. For a simulator built byfrom_sensorfilterwith a defocused label,simulate()withoutpsf=now renders that filter’s defocus PSF, matching whatget_image_snrhas always done. Every other construction path is unaffected. Passpsf=explicitly to override.Host and diffuse elements are classified by
surface_brightness, not by element name (issue #63). A host given as an ordinary integrated magnitude used to have its total count rate added to every detector pixel, overstating its shot-noise contribution byn_pix / enclosed_fraction— 64.6x for the representative mag 20 source / mag 16 host scene (38.6 M e- of host charge instead of 597 k e-). Such an element is now treated as unresolved and co-located with the source and is rendered through the same PSF; onlysurface_brightness=Trueelements are per-pixel. See the user guide (host-spatial-treatment).This also unifies the charge budget:
get_peak_pixel,is_saturated, theget_image_snrsaturation check andImageSimulator.simulatepreviously disagreed about whether host charge counted, so identical scenes gave different saturation answers depending on which API you called. They now all see the same electrons, andget_peak_pixelincludes an unresolved host (it no longer drops it).Source-weighted PSF wavelength (issue #65, option A). The PSF is now rendered at the photon-weighted effective wavelength of the bandpass times the source SED —
effective_wavelength— instead of the filter pivot wavelength, which is the same for every source. Through the WCC broad band this moves the render wavelength from 582.1 nm to 540.7 nm (O5V) or 716.5 nm (M5V) and corrects the modelled central-pixel fraction by −9 % to +36 %; the pivot-only model predicted premature saturation for red sources. Newspectralmodule witheffective_wavelengthandphoton_weighted_subbands.Polychromatic PSFs:
PolychromaticPSFcoadds a base PSF rendered atn_subequal-photon-weight sub-bands;polychromatic_psfbuilds one for a simulation’s band and source.Defocus products carry their optics. The bundled Zemax Huygens defocus PSFs now ship as FITS with
PIXSCALE/WAVELEN/FNUM/DEFOCUSWheaders (regenerate withhuygens_txt_to_fits), andDEFOCUS_1WAVE_PATH/DEFOCUS_2WAVE_PATHpoint at them.DefocusPSFandCustomPSFgained awavelength_scalingargument ("despace"— the default —"waves", or"none") and now raise instead of silently ignoringctx.wavelength_mandctx.fnumwhen they carry no reference metadata. See the user guide (resampled-psf-scaling).Migration: a
CustomPSFbuilt from a bare array needs eitherref_wavelength_mandref_fnum, orwavelength_scaling="none".
Initial highlights
Initial public release. Highlights:
Scene abstraction:
SceneandSceneElementmanage source, host, and background elements, built throughget_scene.Selectable magnitude system:
magsys="vegamag"(default) ormagsys="abmag"(case-insensitive) onSceneElementandget_scene. Note that magnitudes without an explicitmagsys— including the built-inzodibackground (mag=22.5) — are Vega magnitudes. The AB offset is negligible in Johnson V (~0.002 mag) but grows toward the red (~0.26 mag in R, ~1.9 mag in K).Separate
SensorandTelescopeobjects, with sensorbit_depth/bias_level/adc_maxproperties.2-D image SNR by default:
get_snrdelegates toget_image_snrand returns a dict (index["snr"]). The analytic Airy form remains asget_snr_airy(deprecated) for cross-checks.Exposure-time inversion:
get_exptime_for_snr.PSF simulator and PSF-aware SNR:
AiryPSF,DefocusPSF,CustomPSF,ImageSimulator, and aperture optimization inget_image_snr.Saturation flagging:
get_peak_pixelandis_saturated.Parametric source spectra in
get_scene:blackbody,flat(F_\nu/F_\lambda),powerlaw, andemission; shape parameters are rebuildable viaupdate.Background spectrum support and a selection of filters.
Convenience plotting (
plotting): matplotlib and bokeh variants of image, image-row, radial-profile, and encircled-energy plots, plus a shared house style (set_wcc_style/WCC_STYLE) and dispatch methods onSimulatedImage.