Changelog

This page summarizes notable changes. The authoritative history is the git log. A detailed list of the changes made since the original fork is kept in docs/ETC_Changelog.rst.

0.1.0 (2026-10-12)

First public release on PyPI. The sections after this one record the work that led up to it.

  • Default bandpass is the flight baseline (issue #31). Simulation.from_default, Telescope.from_default, Sensor.from_default and esc_calc_snr now default to filter="F630N" (esc_etc.DEFAULT_FILTER) with the end-of-life curve, where they used the generic box-filter chain from the configuration files. filter=None keeps that chain as the legacy option. Default point-source and zodi rates fall x0.736 and the anchors are re-pinned. The default separation is 0.2 arcsec everywhere (it was 0.4 in the object API and 0.2 in esc_calc_snr).

  • Filter curve provenance (issue #29). The 24 bundled throughput curves, DATA_SOURCES.md and the docs cite the Lazuli throughput budget by date only; the internal budget document, review milestone and margin numbers are no longer quoted. The curve data is unchanged.

  • Release hygiene (issue #27). esc_calc_snr(plot=True) draws the overview instead of ignoring the argument. Sensor.from_config computes the plate scale with units (a 2 ppm change from the exact arcsec-per-radian factor), and the instrument numbers that are not in the configuration files are named module constants in esc_etc.sensor. The legacy STP 6.5 m test module, which could only run with University of Arizona packages that are not public, is removed with its test data.

  • Publishing. publish.yml releases on tags: v*rc* to TestPyPI, other v* tags to PyPI, through trusted publishing, after the same build, audit and installed-wheel smoke test as the CI build job.

  • Docs hosting. Read the Docs is the only publisher; the GitHub Pages workflow is removed and the Read the Docs build fails on warnings, as a local sphinx-build -W does.

  • License. The package is released under the BSD 3-Clause license (it was GPL-3.0-or-later), matching wcc-etc; LICENSE is the only license file, the separate COPYRIGHT is gone. Gudmundur Stefansson is the maintainer. The legacy demo notebook, etc_esc_requirements.py and the internal planning documents are no longer tracked.

  • Sensor units. sim.update(sensor__read_noise=5) and the same for dark_current, well_depth and gain now read a plain number in the sensor’s own unit; before, the next get_snr raised UnitConversionError.

  • Documentation. The user guide opens with a coronagraphy primer (Coronagraph and contrast), Signal-to-noise and integration time states the noise equation and the SNR ceiling, Concepts and architecture tabulates the four photometric apertures, Configuration and filters lists the twelve ESC filters and the bundled files, and Known limitations collects the open physics items. The docs toolchain installs through the [docs] extra, as in wcc-etc.

  • Lazuli naming. The bundled configuration lives under esc_etc/data/lazuli/ as telescope and esc, addressed in code as uasal_config.TELESCOPE and uasal_config.INSTRUMENT. The UASAL package names (config_um, config_um_esc; “UM” was the UA code name for Lazuli) appear only in uasal_config.UPSTREAM, the sync script and SNAPSHOT.txt. Docs say Lazuli instead of “UM 3 m”.

  • Packaging for PyPI. The UASAL configuration (config_um and config_um_esc) is vendored as a snapshot inside the package and read by esc_etc.uasal_config; the git dependencies are gone, so the package installs from a wheel. scipy is declared, Python 3.11 or newer is required, and esc_etc.get_data_path() returns the real data/ directory. scripts/sync_uasal_configs.py refreshes the snapshot.

  • Correctness fixes (issue #27). SNR no longer depends on the unit total_time is given in (1 * u.h and 3600 * u.s agree) and plain numbers are read as seconds in get_snr(), get_int_time_for_snr() and is_saturated(). lifetime is validated and raises without a named filter. reset() recurses into the components. Sensor.reset() returns to the automatic peak-pixel model instead of breaking saturation_time(). encircled_energy must lie in (0, 1]. Sensor.from_config refuses a filter whose central wavelength differs from the telescope’s.

  • Plot methods. sim.plot() draws a 2 x 3 overview, and every plot is a method on the object that owns the data (sim.plot_snr_vs_time(), sim.sensor.plot_bandpass(), sim.coronagraph.plot_offaxis_throughput(), sim.scene.plot_spectra(), …), each accepting ax=. plot_snr_vs_time() takes sweep= (one curve per value of any update-style parameter, original value restored) and replaces plot_snr_vs_zodi; plot_aperture() and plot_overview() are new; label= on the single-curve helpers. lazuli_style() no longer switches Jupyter’s inline backend off after the first figure.

  • Tutorials are generated by scripts/build_tutorials.py and executed against the 3 m default; they had last been run on the legacy 2.43 m config.

  • Repository. tests.yml runs pytest, ruff (F rules and formatting gated) and a build job (sdist and wheel, twine check, an artifact audit, and a smoke test of the installed wheel) on every pull request; MANIFEST.in trims the sdist to the package and its tests; DATA_SOURCES.md lists every bundled data file and its provenance; _version.py is untracked; profiling output, scratch notebooks and the root requirements.txt are removed.

Physics

  • One photometric aperture (issue #10). Sensor.num_psf_pixels is now Telescope.resel \(= \pi(\lambda/2D)^2\) in detector pixels (about 20 at the default configuration) instead of a separate \(15 \times 15\) PSF box, so dark current, read noise, zodi and exozodi all use the same extraction aperture. Sensor.calc_psf is removed. The FPM off-axis throughput is documented as the planet’s throughput into that aperture and raw contrast as leakage relative to the off-axis PSF in it. Default-configuration SNR anchors moved accordingly.

  • Peak-pixel saturation model (issue #11). saturation_time() now fills the brightest dark-hole pixel: point sources contribute Sensor.peak_pixel_fraction of their in-aperture rate (default from the Airy pattern at the plate scale), diffuse backgrounds spread over the aperture, dark current is included, and the limit is Sensor.headroom (default 0.8) times the well depth. Both new sensor parameters are mutable. The occulted host remains out of scope.

  • Integer frame accounting and input validation (issue #15). Read noise scales with ceil(total_time / frame_time) reads (whole frames plus a shorter final one, at least one). get_int_time_for_snr() returns int_time rounded up to whole frames plus int_time_continuous, both in seconds. Non-positive times, target SNR, ppgain or separation and negative contrast raise ValueError; separations outside the FPM table raise instead of clamping; a saturating frame_time warns. A rejected update leaves no trace. The default plot_snr_vs_time() grid starts at one frame.

  • Planet spectrum is used (issue #13). The planet’s spectrum is normalized deltamag fainter than the host in deltamag_band (default: the ESC band, which reproduces the old \(10^{-0.4\,\Delta m}\) scaling; or a filter name such as "johnson_v") and observed through the ESC bandpass, so planet colour matters when the contrast is quoted elsewhere. spectrum and deltamag_band are mutable; planet.mag is unused.

  • Explicit reference bands (issue #14). Every SceneElement carries a mutable band (None = ESC bandpass, a synphot filter name, or a SpectralElement); spectra are normalized there, then integrated through the ESC bandpass. get_scene() and Simulation.from_default take hoststar_band ("johnson_v" for catalog photometry) and mag_system. Zodi is explicitly Johnson V; exobg_flux is documented as a delta magnitude. Invalid magnitude systems raise ValueError immediately, and the deprecated flux_units argument is now honored instead of ignored.

  • End-of-life ESC filter curves. Twelve telescope + ESC throughput curves (EOL, budget reserves, detector QE and EOL contamination included, vortex and Lyot factors excluded) ship in esc_etc/data/esc_filters_eol and are listed in esc_etc.FILTERS by wheel slot ("R1") and designation ("F620N"). filter= on Simulation.from_default, Telescope.from_default and Sensor.from_default uses the curve as the whole sensor bandpass and the design wavelength as the telescope’s central wavelength (resel, lambda/D). Default (None) keeps the generic config box filter. The curves come from the Lazuli throughput budget of 2026-03-25, whose optics chain matches config_um_esc, on a 0.1 nm grid; the vortex off-axis factor and the Lyot area are deliberately left out because Coronagraph.off_axis_throughput and Telescope.surface apply them per target. Blue-arm curves are provisional (placeholder gold-DM and polarizer curves in the budget).

  • Lazuli 3 m default (config_um_esc). from_default on every component now reads the ESC instrument from config_um_esc (3.0 m clear aperture, ASI294MM, silver ESC optics, 0.9 dichroic) instead of config_stp_esc (2.43 m, ASI6200MM, Al+MgF2), matching the telescope in config_um. (Both packages are now vendored; see Unreleased.) The bandpass chain applies dichroic_throughput when the config has one, and the detector tables are clamped at their ends so the -20 C nominal temperature no longer raises. Default source rates rise 5.9x (area 1.56 x throughput 3.52 x FPM 1.07) and every golden anchor was re-pinned accordingly.

  • Reference-subtraction noise and time (issue #12). Coronagraph gains subtraction ("none" default, or "rdi"), ref_brightness_ratio and ref_time_ratio. Under RDI the scaled random variance of the reference image is added to the science variance (Nemati et al. 2023) and the inversion accounts for it. total_time is on-target science time; results carry science_time, charged_time and reference_noise_e, and noise_terms gains a sixth, reference entry. ppgain is documented as an assumed residual- speckle factor.

  • Switchable noise aperture with encircled energy, and EOL/BOL throughput sets. Sensor gains two mutable parameters. aperture selects the extraction aperture every SNR term uses: "resel" (disk of diameter lambda/D, default), "optimal" (radius 0.7 lambda/D, the read-noise-limited optimum), "box" (the UA stp_etc_esc n x n box, n = ceil(2.44 lambda F# / pixel)) or "matched" (N_eff = 1/sum p_i^2, EE = 1). encircled_energy (None = Airy value for that aperture, exposed as ee_fraction) is the fraction of a point source’s light credited inside it; planet and speckle rates are multiplied by it, zodi and exozodi use aperture_solid_angle and detector terms num_psf_pixels. The default therefore now charges the 46% encircled energy of the resel: default point-source rates fall x0.456 and the golden anchors are re-pinned. encircled_energy=1.0 recovers the previous numbers; aperture="box" with encircled_energy=1.0 reproduces the UA convention. filter= gains lifetime="eol" (default: EOL contamination and reserves) or "bol" (instrument delivery, no reserves); both curve sets come from the Lazuli throughput budget without the vortex and Lyot factors.

Object-oriented API

  • Composable class API. The ETC exposes a suite of composable classes at the top level: Telescope, Sensor, Scene / SceneElement / get_scene(), Coronagraph, AnalyticCoronagraphNoiseModel, CountRates, and Simulation.

  • Pluggable noise model. The noise model is swappable via the noise_model seam on Simulation, enabling custom noise implementations (e.g. a future 2-D PSF image model) without subclassing the simulation engine.

  • Simulation.from_default and the per-component from_config / from_default constructors replace direct use of Observatory. The legacy Observatory class and esc_calc_snr() function are retained as deprecated compatibility wrappers and will be removed in a future release.

Plotting

  • New esc_etc.plotting module with lazuli-styled matplotlib helpers: plot_count_rates() (noise-contributor bar breakdown), plot_snr_vs_time() (SNR vs integration time), plot_offaxis_throughput() (coronagraph off-axis throughput vs separation), plot_bandpass() (sensor bandpass curve), and plot_spectra() (scene-element spectra overlay).

  • A packaged lazuli.mplstyle ships as package data and is applied inside each helper via a context manager (no global state mutation). Style utilities lazuli_style() / use_lazuli_style() and the color tokens TEAL / RED / AMBER / BLUE / GREEN / DARKBLUE / LAZULI_COLORS are exported from the top-level namespace.

Tutorials

  • New numbered tutorial-notebook suite in notebooks/ teaching the object-oriented API and the plotting helpers: 01_getting_started, 02_components_and_parameters, 03_coronagraph_and_contrast, and 04_integration_time_snr_saturation. Each notebook executes end to end.

Documentation site

  • Sphinx documentation site under docs/sphinx/ (Read the Docs theme): installation, quickstart, configuration, a user guide, the tutorial notebooks rendered via nbsphinx, and an autodoc API reference. Built and published to GitHub Pages by .github/workflows/deploy-docs.yml; a .readthedocs.yaml is included for Read the Docs.

  • New Saturation page covering saturation_time() and is_saturated(), what sets the per-pixel rate (well_depth, num_psf_pixels), and the frame-time trade-off against read noise.

  • A docs extra in pyproject.toml for a one-line local docs build (pip install -e ".[docs]").

Earlier history

The package was forked from the UASAL Exposure Time Calculator (originally by Aaron Goldtooth) in July 2024. The refactors between then and the changes above (config-driven structure, an installable package with setuptools_scm versioning, corrected unit handling and read-noise treatment, and Johnson-V sky-background normalization) are documented in docs/ETC_Changelog.rst.