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_defaultandesc_calc_snrnow default tofilter="F630N"(esc_etc.DEFAULT_FILTER) with the end-of-life curve, where they used the generic box-filter chain from the configuration files.filter=Nonekeeps 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 inesc_calc_snr).Filter curve provenance (issue #29). The 24 bundled throughput curves,
DATA_SOURCES.mdand 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_configcomputes 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 inesc_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.ymlreleases on tags:v*rc*to TestPyPI, otherv*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 -Wdoes.License. The package is released under the BSD 3-Clause license (it was GPL-3.0-or-later), matching wcc-etc;
LICENSEis the only license file, the separateCOPYRIGHTis gone. Gudmundur Stefansson is the maintainer. The legacy demo notebook,etc_esc_requirements.pyand the internal planning documents are no longer tracked.Sensor units.
sim.update(sensor__read_noise=5)and the same fordark_current,well_depthandgainnow read a plain number in the sensor’s own unit; before, the nextget_snrraisedUnitConversionError.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/astelescopeandesc, addressed in code asuasal_config.TELESCOPEanduasal_config.INSTRUMENT. The UASAL package names (config_um,config_um_esc; “UM” was the UA code name for Lazuli) appear only inuasal_config.UPSTREAM, the sync script andSNAPSHOT.txt. Docs say Lazuli instead of “UM 3 m”.Packaging for PyPI. The UASAL configuration (
config_umandconfig_um_esc) is vendored as a snapshot inside the package and read byesc_etc.uasal_config; the git dependencies are gone, so the package installs from a wheel.scipyis declared, Python 3.11 or newer is required, andesc_etc.get_data_path()returns the realdata/directory.scripts/sync_uasal_configs.pyrefreshes the snapshot.Correctness fixes (issue #27). SNR no longer depends on the unit
total_timeis given in (1 * u.hand3600 * u.sagree) and plain numbers are read as seconds inget_snr(),get_int_time_for_snr()andis_saturated().lifetimeis validated and raises without a named filter.reset()recurses into the components.Sensor.reset()returns to the automatic peak-pixel model instead of breakingsaturation_time().encircled_energymust lie in(0, 1].Sensor.from_configrefuses 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 acceptingax=.plot_snr_vs_time()takessweep=(one curve per value of anyupdate-style parameter, original value restored) and replacesplot_snr_vs_zodi;plot_aperture()andplot_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.pyand executed against the 3 m default; they had last been run on the legacy 2.43 m config.Repository.
tests.ymlruns 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.intrims the sdist to the package and its tests;DATA_SOURCES.mdlists every bundled data file and its provenance;_version.pyis untracked; profiling output, scratch notebooks and the rootrequirements.txtare removed.
Physics
One photometric aperture (issue #10).
Sensor.num_psf_pixelsis nowTelescope.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_psfis 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 contributeSensor.peak_pixel_fractionof 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 isSensor.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()returnsint_timerounded up to whole frames plusint_time_continuous, both in seconds. Non-positive times, target SNR,ppgainor separation and negative contrast raiseValueError; separations outside the FPM table raise instead of clamping; a saturatingframe_timewarns. A rejectedupdateleaves no trace. The defaultplot_snr_vs_time()grid starts at one frame.Planet spectrum is used (issue #13). The planet’s
spectrumis normalizeddeltamagfainter than the host indeltamag_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.spectrumanddeltamag_bandare mutable;planet.magis unused.Explicit reference bands (issue #14). Every
SceneElementcarries a mutableband(None= ESC bandpass, a synphot filter name, or aSpectralElement); spectra are normalized there, then integrated through the ESC bandpass.get_scene()andSimulation.from_defaulttakehoststar_band("johnson_v"for catalog photometry) andmag_system. Zodi is explicitly Johnson V;exobg_fluxis documented as a delta magnitude. Invalid magnitude systems raiseValueErrorimmediately, and the deprecatedflux_unitsargument 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_eoland are listed inesc_etc.FILTERSby wheel slot ("R1") and designation ("F620N").filter=onSimulation.from_default,Telescope.from_defaultandSensor.from_defaultuses 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 matchesconfig_um_esc, on a 0.1 nm grid; the vortex off-axis factor and the Lyot area are deliberately left out becauseCoronagraph.off_axis_throughputandTelescope.surfaceapply 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_defaulton every component now reads the ESC instrument fromconfig_um_esc(3.0 m clear aperture, ASI294MM, silver ESC optics, 0.9 dichroic) instead ofconfig_stp_esc(2.43 m, ASI6200MM, Al+MgF2), matching the telescope inconfig_um. (Both packages are now vendored; see Unreleased.) The bandpass chain appliesdichroic_throughputwhen 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).
Coronagraphgainssubtraction("none"default, or"rdi"),ref_brightness_ratioandref_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_timeis on-target science time; results carryscience_time,charged_timeandreference_noise_e, andnoise_termsgains a sixth, reference entry.ppgainis documented as an assumed residual- speckle factor.Switchable noise aperture with encircled energy, and EOL/BOL throughput sets.
Sensorgains two mutable parameters.apertureselects 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 UAstp_etc_escn 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 asee_fraction) is the fraction of a point source’s light credited inside it; planet and speckle rates are multiplied by it, zodi and exozodi useaperture_solid_angleand detector termsnum_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.0recovers the previous numbers;aperture="box"withencircled_energy=1.0reproduces the UA convention.filter=gainslifetime="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, andSimulation.Pluggable noise model. The noise model is swappable via the
noise_modelseam onSimulation, enabling custom noise implementations (e.g. a future 2-D PSF image model) without subclassing the simulation engine.Simulation.from_defaultand the per-componentfrom_config/from_defaultconstructors replace direct use ofObservatory. The legacyObservatoryclass andesc_calc_snr()function are retained as deprecated compatibility wrappers and will be removed in a future release.
Plotting
New
esc_etc.plottingmodule 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), andplot_spectra()(scene-element spectra overlay).A packaged
lazuli.mplstyleships as package data and is applied inside each helper via a context manager (no global state mutation). Style utilitieslazuli_style()/use_lazuli_style()and the color tokensTEAL/RED/AMBER/BLUE/GREEN/DARKBLUE/LAZULI_COLORSare 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, and04_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 vianbsphinx, and an autodoc API reference. Built and published to GitHub Pages by.github/workflows/deploy-docs.yml; a.readthedocs.yamlis included for Read the Docs.New Saturation page covering
saturation_time()andis_saturated(), what sets the per-pixel rate (well_depth,num_psf_pixels), and the frame-time trade-off against read noise.A
docsextra inpyproject.tomlfor 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.