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"`` (:data:`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 (:doc:`user_guide/coronagraph`), :doc:`user_guide/snr` states the noise equation and the SNR ceiling, :doc:`user_guide/concepts` tabulates the four photometric apertures, :doc:`configuration` lists the twelve ESC filters and the bundled files, and :doc:`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 :func:`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 :meth:`~esc_etc.Simulation.get_snr`, :meth:`~esc_etc.Simulation.get_int_time_for_snr` and :meth:`~esc_etc.Simulation.is_saturated`. ``lifetime`` is validated and raises without a named filter. :meth:`~esc_etc.Simulation.reset` recurses into the components. ``Sensor.reset()`` returns to the automatic peak-pixel model instead of breaking :meth:`~esc_etc.Simulation.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=``. :func:`~esc_etc.plot_snr_vs_time` takes ``sweep=`` (one curve per value of any ``update``-style parameter, original value restored) and replaces ``plot_snr_vs_zodi``; :func:`~esc_etc.plot_aperture` and :func:`~esc_etc.plot_overview` are new; ``label=`` on the single-curve helpers. :func:`~esc_etc.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 :attr:`Telescope.resel ` :math:`= \pi(\lambda/2D)^2` in detector pixels (about 20 at the default configuration) instead of a separate :math:`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). :meth:`~esc_etc.Simulation.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). :meth:`~esc_etc.Simulation.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 :func:`~esc_etc.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 :math:`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 :class:`~esc_etc.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. :func:`~esc_etc.get_scene` and :meth:`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 :data:`esc_etc.FILTERS` by wheel slot (``"R1"``) and designation (``"F620N"``). ``filter=`` on :meth:`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). :class:`~esc_etc.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: :class:`~esc_etc.Telescope`, :class:`~esc_etc.Sensor`, :class:`~esc_etc.Scene` / :class:`~esc_etc.SceneElement` / :func:`~esc_etc.get_scene`, :class:`~esc_etc.Coronagraph`, :class:`~esc_etc.AnalyticCoronagraphNoiseModel`, :class:`~esc_etc.CountRates`, and :class:`~esc_etc.Simulation`. - **Pluggable noise model.** The noise model is swappable via the ``noise_model`` seam on :class:`~esc_etc.Simulation`, enabling custom noise implementations (e.g. a future 2-D PSF image model) without subclassing the simulation engine. - :meth:`Simulation.from_default ` and the per-component ``from_config`` / ``from_default`` constructors replace direct use of ``Observatory``. The legacy :class:`~esc_etc.Observatory` class and :func:`~esc_etc.esc_calc_snr` function are retained as **deprecated** compatibility wrappers and will be removed in a future release. Plotting -------- - **New** :mod:`esc_etc.plotting` **module** with lazuli-styled matplotlib helpers: :func:`~esc_etc.plot_count_rates` (noise-contributor bar breakdown), :func:`~esc_etc.plot_snr_vs_time` (SNR vs integration time), :func:`~esc_etc.plot_offaxis_throughput` (coronagraph off-axis throughput vs separation), :func:`~esc_etc.plot_bandpass` (sensor bandpass curve), and :func:`~esc_etc.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 :func:`~esc_etc.lazuli_style` / :func:`~esc_etc.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** :doc:`user_guide/saturation` **page** covering :meth:`~esc_etc.Simulation.saturation_time` and :meth:`~esc_etc.Simulation.is_saturated`, what sets the per-pixel rate (:attr:`~esc_etc.Sensor.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``.