Quick start
Every snippet on this page uses the public top-level API of esc_etc.
Times given as plain numbers are seconds; astropy quantities in any time unit
work too.
The mental model
A calculation is built from four pieces, combined into a
Simulation:
Telescope (aperture, surface, filter) ─┐
Sensor (detector + optical throughput) ├──► Simulation ──► SNR / int. time / saturation
Scene (star, planet, zodi, exozodi) │
Coronagraph (contrast, ppgain, separation) ─┘
The
Telescopeholds the collecting area, the focal geometry, the filter’s central wavelength and the resolution element (resel). The resel is the area of one diffraction-limited PSF core, a disc of diameter \(\lambda/D\) on the sky, and is the default photometric aperture.The
Sensorcouples the detector (read noise, dark current, gain, well depth) with the optical bandpass and the photometric aperture every noise term uses.The
Sceneholds the host star, the planet (given as a magnitude difference from the host) and the zodiacal and exozodiacal backgrounds, each aSceneElement.The
Coronagraphholds the raw dark-hole contrast, the post-processing gain, the planet separation and the focal-plane mask throughput curve. Coronagraph and contrast defines these terms.
1. Build a simulation from the defaults
Simulation.from_default assembles
all four components from the bundled configuration of the Lazuli 3 m
telescope and the ESC:
from esc_etc import Simulation
sim = Simulation.from_default(
hoststarmag=-0.353, # host-star Vega magnitude in the ESC band
planetdeltamag=20.0, # planet 20 mag fainter than the host: a flux ratio of 1e-8
contrast=1e-8, # raw dark-hole contrast (coronagraph.raw_dh_contrast)
ppgain=10, # assumed post-processing gain on the residual starlight
separation=0.4, # planet separation in arcsec, about 9 lambda/D at 630 nm
)
With these numbers the planet sits exactly at the raw contrast level, so the
residual starlight in the aperture is as bright as the planet. contrast=
here sets the same quantity that the rest of the documentation calls
raw_dh_contrast. Concepts and architecture shows how to build each
component yourself and assemble them with
Simulation.from_components.
2. Compute signal-to-noise
get_snr() returns a dictionary. The signal-to-noise
itself is under the "snr" key:
import astropy.units as u
result = sim.get_snr(total_time=3600 * u.s, frame_time=100 * u.s)
result = sim.get_snr(3600, 100) # plain numbers are seconds
result["snr"] # signal-to-noise ratio, about 9.87 here
result["signal_e"] # planet electrons accumulated in total_time
result["noise_e"] # total noise (electrons)
result["M"] # speckle rate divided by ppgain: the post-processing noise floor
total_time is the on-target integration and frame_time is how often
the detector is read out. The number of reads is ceil(total_time /
frame_time), never fewer than one, and sets how much read noise accumulates.
Saturation bounds the frame time from above, read noise from below; see
Saturation. The full budget is in Signal-to-noise and integration time.
3. Invert: integration time for a target SNR
inv = sim.get_int_time_for_snr(snr=5, frame_time=100 * u.s)
inv["int_time"] # whole-frame integration time that reaches SNR = 5
inv["int_time_continuous"] # unrounded closed-form value
inv["noise_terms"] # [dark, read, zodi, exozodi, speckle, reference] rates
The speckle noise floor grows with time exactly as the signal does, so each
configuration has a maximum reachable signal-to-noise: ppgain times the
planet-to-speckle rate ratio, which is 10 for the defaults above. A target
above that limit comes back as nan.
4. Check saturation
sim.saturation_time() # frame time (s) at which the brightest pixel fills the well
sim.is_saturated(100) # True / False for a 100 s frame
The pixel in question is the brightest one in the planet’s aperture, where the starlight has been suppressed to the raw contrast. The stellar core behind the focal-plane mask (the occulting spot that blocks the star) is not modelled, so for a bright host the detector-safe frame time can be much shorter; see Saturation.
5. Change parameters and recompute
Every component’s tunable parameters can be changed in place with
update(), using child__param double-underscore
syntax, and recomputed. reset() restores the
original values.
sim.update(coronagraph__separation=0.6) # wider separation
sim.update(coronagraph__ppgain=20) # twice the assumed post-processing gain
sim.update(sensor__read_noise=5) # quieter detector; plain numbers are in the sensor's own units
sim.update(sensor__aperture="optimal") # a different photometric aperture
sim.get_snr(3600, 100)["snr"]
sim.reset() # back to the initial configuration
The bandpass is the end-of-life throughput curve of the F630N filter
unless you say otherwise. Pick another filter with filter= (a key of
esc_etc.FILTERS), and lifetime="bol" for the beginning-of-life
curve:
sim = Simulation.from_default(filter="F620N", lifetime="bol")
6. Plot the results
Every standard plot is a method; each accepts ax= and returns (fig, ax):
fig, axes = sim.plot() # 2 x 3 overview
fig, ax = sim.plot_count_rates() # bar chart of count-rate contributors
fig, ax = sim.plot_snr_vs_time() # SNR vs integration time
fig, ax = sim.plot_snr_vs_time(sweep={"coronagraph__ppgain": [1, 10, 100]})
The plots apply the packaged style through a context manager, so your own
matplotlib settings are untouched. use_lazuli_style() applies
the same style to your own figures. See Plotting.
Where to go next
User guide for the physics behind each piece.
Tutorials for the four runnable notebooks.
API reference for the full auto-generated API reference.