pyEDITH Tutorial: Spectroscopy Mode#

This tutorial will guide you through using pyEDITH in spectroscopy mode. We’ll explore how to set up parameters, run the Exposure Time Calculator (ETC), and analyze the results for spectroscopic observations.

Note

If you’re new to pyEDITH or need a refresher on basic concepts, please refer to the Imaging Mode Tutorial first. The imaging tutorial covers fundamental concepts that are also applicable to spectroscopy mode.

1. Usage#

In this section, we will use the classes previously defined to run spectroscopic noise simulations.

1.1 Setup and Imports#

First, let’s import the necessary modules and set up our environment:

[1]:
import pyEDITH as pE
import numpy as np
import matplotlib.pyplot as plt
import os
import pickle

# Set verbosity to INFO, showing info, warnings and errors. Other options are "warning" (warnings and errors),
# "quiet" (only errors), and "debug" (all logs)
pE.set_verbosity(level='info')

# Set the necessary environment variables
# Replace with your paths or add to your .bashrc/.zshrc:
# export SCI_ENG_DIR="/path/to/Sci-Eng-Interface/hwo_sci_eng"
# export YIP_CORO_DIR="/path/to/yips"

# Loading HWO style package to make pretty plots
import hwostyle
hwostyle.use("light")
colors = hwostyle.palette

/Users/ealei/Coding/pyEDITH/.venv/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm
[pyEDITH] INFO [2026-05-19 11:18:12,312] Logging level set to: INFO

We now set up a parameters dictionary. This is where all the input parameters are stored for EDITH.

[2]:
parameters = {}
parameters["observing_mode"] = "IFS" # tells ETC to use spectroscopy (IFS) mode

1.1.1 Observation#

We now set up the Observation class. Eventually, this is where the exposure time or SNR will be placed after calculation. To set up the observation object, we need to specify a few overarching parameters:

[3]:
# define a wavelength grid in microns
parameters["wavelength"] = np.linspace(0.2, 1.8, 1000)

# number of wavelengths
parameters["nlambd"] = len(parameters["wavelength"])

# the SNR you want for each spectral bin
parameters["snr"] = 7*np.ones_like(parameters["wavelength"])

# factor to multiply the background by (used for differential imaging)
parameters["CRb_multiplier"] = 2.

To get a more realistic wavelength grid, we can tell the ETC to regrid our user-supplied wavelength grid and spectra onto a new wavelength grid. This is optional. If you don’t want to re-grid, then set parameters["regrid_wavelength"] = False

[4]:
# set the flag to do this. We also need to specify a few other parameters.
parameters["regrid_wavelength"] = True

# we're going to define three spectral channels. These are the spectral resolutions for each channel
# i.e. all spectral bins in a given channel will have a fixed resolution.
parameters["spectral_resolution"] = np.array([7, 140, 40])

# the lower wavelength bounds for each spectral channel
parameters['lam_low'] = [0.3, 0.4, 1.]

# the upper wavelength bounds for each spectral channel
parameters['lam_high'] = [0.4, 1., 1.7]

There are more parameters we can define (see below), but these are the basics for now. With these parameters, we can populate the Observation class.

[5]:
observation = pE.Observation()

# load the specified configuration in the parameters dict
observation.load_configuration(parameters)
observation.set_output_arrays()
observation.validate_configuration()


[pyEDITH] INFO [2026-05-19 11:18:12,326] Calculating a new wavelength grid and re-gridding spectra...

1.1.2 Astrophysical Scene#

Up next, the Astrophysical Scene object. We will use a blackbody spectrum for a Solar-like star and a template for an Earth-like reflectance spectrum for the purposes of this tutorial.

[6]:
from synphot import SourceSpectrum, SpectralElement, Observation
from synphot.models import BlackBodyNorm1D, Empirical1D
from synphot import units as synphot_u
import astropy.units as u
import astropy.constants as c

def compute_blackbody_photon_flux(temp, wavelengths):
    """Generate photon flux density (photon/s/cm^2/um) for a blackbody at 1 cm^2."""
    bb = SourceSpectrum(BlackBodyNorm1D, temperature=temp)


    flux_photlam = bb(wavelengths)#.value * 1/(u.s * u.cm**2 * u.AA) # photons/s/cm2/A
    return flux_photlam

def compute_star_flux_observed(stellar_flux_surface, R_star, dist):
    """Scale stellar surface flux to observer distance."""
    scale = (R_star / dist)**2
    return stellar_flux_surface * scale.decompose()


# Temperature and distance to star
Tstar = 5800 * u.K
dist = 10 * u.pc

# calculate the observed Fstar
Fstar = compute_blackbody_photon_flux(Tstar, parameters["wavelength"]*u.um)
Fstar = Fstar.to(u.photon / (u.s * u.cm**2 * u.nm)) # convert to pyEdith units
Fstar_obs_10pc = Fstar * (1000*u.pc/dist)**2 # this gives the flux at 1 kpc. Scale to the distance of the star

# calculate the observed Fp/Fs
earth_wl, earth_refl = np.loadtxt("../inputs/earth_refl_demo.txt", delimiter=",").T
from scipy.interpolate import interp1d
earth_interp_func = interp1d(earth_wl, earth_refl)
FpFs = earth_interp_func(parameters["wavelength"])
Fplan_obs = Fstar_obs_10pc * FpFs

fig = plt.figure(dpi=100)
ax = plt.gca()
plt.plot(parameters["wavelength"]*u.um, FpFs, linewidth=2)
plt.xlabel("Wavelength (um)", fontsize=14, fontweight='bold')
plt.ylabel("Fp/Fs", fontsize=14, fontweight='bold')
plt.title("Earth reflectance demo", fontsize=14, fontweight='bold')
plt.tick_params(axis='both', which='major', labelsize=12, width=1.5, length=6)
plt.tick_params(axis='both', which='minor', width=1, length=3)
for spine in ax.spines.values():
    spine.set_visible(True)
    spine.set_linewidth(1.5)
plt.grid(True, alpha=0.3, linestyle='--', linewidth=0.8)
plt.tight_layout()

fig = plt.figure(dpi=100)
ax = plt.gca()
plt.plot(parameters["wavelength"]*u.um, Fstar_obs_10pc, label="Star", linewidth=2)
plt.plot(parameters["wavelength"]*u.um, Fplan_obs, label="Planet", linewidth=2)
plt.xlabel("Wavelength (um)", fontsize=14, fontweight='bold')
plt.ylabel("Flux (photons/s/cm^2/nm)", fontsize=14, fontweight='bold')
plt.yscale("log")
plt.tick_params(axis='both', which='major', labelsize=12, width=1.5, length=6)
plt.tick_params(axis='both', which='minor', width=1, length=3)
for spine in ax.spines.values():
    spine.set_visible(True)
    spine.set_linewidth(1.5)
plt.grid(True, alpha=0.3, linestyle='--', linewidth=0.8)
plt.legend(loc='best', fontsize=11, frameon=True, shadow=True, fancybox=True,
           framealpha=0.95, edgecolor='gray')
plt.tight_layout()


_images/spectroscopy_tutorial_11_0.png
_images/spectroscopy_tutorial_11_1.png

We need several more parameters to define the astrophysical scene. These include parameters related to the star, planet, zodi/exozodi…

As an example, lets take the star HD 102365. It is a sun-like star (spectral type G2V) ~10 pc away. It is also a Tier A target star from the NASA ExEP HWO target star list.

[7]:
# STAR
# distance to the system in pc
parameters["distance"] = 10.

# radius of the star in solar radii
parameters["stellar_radius"] = 1.

# Fstar modeled as a blackbody (see above)
parameters["Fstar_10pc"] = Fstar_obs_10pc.value

# PLANET
# planetary separation in arcsec
parameters["separation"] = 0.1

#Fp/Fs from Earth template (see above)
parameters["Fp/Fs"] = FpFs

# SCENE
# number of zodis for exozodi estimate
parameters["nzodis"] = 1.

# approximate ra and dec of HD 102365. WARNING: do not use these numbers for science.
parameters["ra"] = 176.6292
parameters["dec"] = -40.5003

# perfect exozodi subtraction
parameters["ez_PPF"] = np.inf


With these parameters, we can populate the AstrophysicalScene class.

[8]:
scene = pE.AstrophysicalScene()
scene.load_configuration(parameters)
scene.calculate_zodi_exozodi(parameters)
scene.validate_configuration()
[pyEDITH] INFO [2026-05-19 11:18:12,632] Flux zero point calculated at 5.5e-05 cm in units of ph / (s cm3)
[pyEDITH] INFO [2026-05-19 11:18:12,636] Flux zero point calculated at [2.00000000e-05 2.01601602e-05 2.03203203e-05 2.04804805e-05
 2.06406406e-05 2.08008008e-05 2.09609610e-05 2.11211211e-05
 2.12812813e-05 2.14414414e-05 2.16016016e-05 2.17617618e-05
 2.19219219e-05 2.20820821e-05 2.22422422e-05 2.24024024e-05
 2.25625626e-05 2.27227227e-05 2.28828829e-05 2.30430430e-05
 2.32032032e-05 2.33633634e-05 2.35235235e-05 2.36836837e-05
 2.38438438e-05 2.40040040e-05 2.41641642e-05 2.43243243e-05
 2.44844845e-05 2.46446446e-05 2.48048048e-05 2.49649650e-05
 2.51251251e-05 2.52852853e-05 2.54454454e-05 2.56056056e-05
 2.57657658e-05 2.59259259e-05 2.60860861e-05 2.62462462e-05
 2.64064064e-05 2.65665666e-05 2.67267267e-05 2.68868869e-05
 2.70470470e-05 2.72072072e-05 2.73673674e-05 2.75275275e-05
 2.76876877e-05 2.78478478e-05 2.80080080e-05 2.81681682e-05
 2.83283283e-05 2.84884885e-05 2.86486486e-05 2.88088088e-05
 2.89689690e-05 2.91291291e-05 2.92892893e-05 2.94494494e-05
 2.96096096e-05 2.97697698e-05 2.99299299e-05 3.00900901e-05
 3.02502503e-05 3.04104104e-05 3.05705706e-05 3.07307307e-05
 3.08908909e-05 3.10510511e-05 3.12112112e-05 3.13713714e-05
 3.15315315e-05 3.16916917e-05 3.18518519e-05 3.20120120e-05
 3.21721722e-05 3.23323323e-05 3.24924925e-05 3.26526527e-05
 3.28128128e-05 3.29729730e-05 3.31331331e-05 3.32932933e-05
 3.34534535e-05 3.36136136e-05 3.37737738e-05 3.39339339e-05
 3.40940941e-05 3.42542543e-05 3.44144144e-05 3.45745746e-05
 3.47347347e-05 3.48948949e-05 3.50550551e-05 3.52152152e-05
 3.53753754e-05 3.55355355e-05 3.56956957e-05 3.58558559e-05
 3.60160160e-05 3.61761762e-05 3.63363363e-05 3.64964965e-05
 3.66566567e-05 3.68168168e-05 3.69769770e-05 3.71371371e-05
 3.72972973e-05 3.74574575e-05 3.76176176e-05 3.77777778e-05
 3.79379379e-05 3.80980981e-05 3.82582583e-05 3.84184184e-05
 3.85785786e-05 3.87387387e-05 3.88988989e-05 3.90590591e-05
 3.92192192e-05 3.93793794e-05 3.95395395e-05 3.96996997e-05
 3.98598599e-05 4.00200200e-05 4.01801802e-05 4.03403403e-05
 4.05005005e-05 4.06606607e-05 4.08208208e-05 4.09809810e-05
 4.11411411e-05 4.13013013e-05 4.14614615e-05 4.16216216e-05
 4.17817818e-05 4.19419419e-05 4.21021021e-05 4.22622623e-05
 4.24224224e-05 4.25825826e-05 4.27427427e-05 4.29029029e-05
 4.30630631e-05 4.32232232e-05 4.33833834e-05 4.35435435e-05
 4.37037037e-05 4.38638639e-05 4.40240240e-05 4.41841842e-05
 4.43443443e-05 4.45045045e-05 4.46646647e-05 4.48248248e-05
 4.49849850e-05 4.51451451e-05 4.53053053e-05 4.54654655e-05
 4.56256256e-05 4.57857858e-05 4.59459459e-05 4.61061061e-05
 4.62662663e-05 4.64264264e-05 4.65865866e-05 4.67467467e-05
 4.69069069e-05 4.70670671e-05 4.72272272e-05 4.73873874e-05
 4.75475475e-05 4.77077077e-05 4.78678679e-05 4.80280280e-05
 4.81881882e-05 4.83483483e-05 4.85085085e-05 4.86686687e-05
 4.88288288e-05 4.89889890e-05 4.91491491e-05 4.93093093e-05
 4.94694695e-05 4.96296296e-05 4.97897898e-05 4.99499499e-05
 5.01101101e-05 5.02702703e-05 5.04304304e-05 5.05905906e-05
 5.07507508e-05 5.09109109e-05 5.10710711e-05 5.12312312e-05
 5.13913914e-05 5.15515516e-05 5.17117117e-05 5.18718719e-05
 5.20320320e-05 5.21921922e-05 5.23523524e-05 5.25125125e-05
 5.26726727e-05 5.28328328e-05 5.29929930e-05 5.31531532e-05
 5.33133133e-05 5.34734735e-05 5.36336336e-05 5.37937938e-05
 5.39539540e-05 5.41141141e-05 5.42742743e-05 5.44344344e-05
 5.45945946e-05 5.47547548e-05 5.49149149e-05 5.50750751e-05
 5.52352352e-05 5.53953954e-05 5.55555556e-05 5.57157157e-05
 5.58758759e-05 5.60360360e-05 5.61961962e-05 5.63563564e-05
 5.65165165e-05 5.66766767e-05 5.68368368e-05 5.69969970e-05
 5.71571572e-05 5.73173173e-05 5.74774775e-05 5.76376376e-05
 5.77977978e-05 5.79579580e-05 5.81181181e-05 5.82782783e-05
 5.84384384e-05 5.85985986e-05 5.87587588e-05 5.89189189e-05
 5.90790791e-05 5.92392392e-05 5.93993994e-05 5.95595596e-05
 5.97197197e-05 5.98798799e-05 6.00400400e-05 6.02002002e-05
 6.03603604e-05 6.05205205e-05 6.06806807e-05 6.08408408e-05
 6.10010010e-05 6.11611612e-05 6.13213213e-05 6.14814815e-05
 6.16416416e-05 6.18018018e-05 6.19619620e-05 6.21221221e-05
 6.22822823e-05 6.24424424e-05 6.26026026e-05 6.27627628e-05
 6.29229229e-05 6.30830831e-05 6.32432432e-05 6.34034034e-05
 6.35635636e-05 6.37237237e-05 6.38838839e-05 6.40440440e-05
 6.42042042e-05 6.43643644e-05 6.45245245e-05 6.46846847e-05
 6.48448448e-05 6.50050050e-05 6.51651652e-05 6.53253253e-05
 6.54854855e-05 6.56456456e-05 6.58058058e-05 6.59659660e-05
 6.61261261e-05 6.62862863e-05 6.64464464e-05 6.66066066e-05
 6.67667668e-05 6.69269269e-05 6.70870871e-05 6.72472472e-05
 6.74074074e-05 6.75675676e-05 6.77277277e-05 6.78878879e-05
 6.80480480e-05 6.82082082e-05 6.83683684e-05 6.85285285e-05
 6.86886887e-05 6.88488488e-05 6.90090090e-05 6.91691692e-05
 6.93293293e-05 6.94894895e-05 6.96496496e-05 6.98098098e-05
 6.99699700e-05 7.01301301e-05 7.02902903e-05 7.04504505e-05
 7.06106106e-05 7.07707708e-05 7.09309309e-05 7.10910911e-05
 7.12512513e-05 7.14114114e-05 7.15715716e-05 7.17317317e-05
 7.18918919e-05 7.20520521e-05 7.22122122e-05 7.23723724e-05
 7.25325325e-05 7.26926927e-05 7.28528529e-05 7.30130130e-05
 7.31731732e-05 7.33333333e-05 7.34934935e-05 7.36536537e-05
 7.38138138e-05 7.39739740e-05 7.41341341e-05 7.42942943e-05
 7.44544545e-05 7.46146146e-05 7.47747748e-05 7.49349349e-05
 7.50950951e-05 7.52552553e-05 7.54154154e-05 7.55755756e-05
 7.57357357e-05 7.58958959e-05 7.60560561e-05 7.62162162e-05
 7.63763764e-05 7.65365365e-05 7.66966967e-05 7.68568569e-05
 7.70170170e-05 7.71771772e-05 7.73373373e-05 7.74974975e-05
 7.76576577e-05 7.78178178e-05 7.79779780e-05 7.81381381e-05
 7.82982983e-05 7.84584585e-05 7.86186186e-05 7.87787788e-05
 7.89389389e-05 7.90990991e-05 7.92592593e-05 7.94194194e-05
 7.95795796e-05 7.97397397e-05 7.98998999e-05 8.00600601e-05
 8.02202202e-05 8.03803804e-05 8.05405405e-05 8.07007007e-05
 8.08608609e-05 8.10210210e-05 8.11811812e-05 8.13413413e-05
 8.15015015e-05 8.16616617e-05 8.18218218e-05 8.19819820e-05
 8.21421421e-05 8.23023023e-05 8.24624625e-05 8.26226226e-05
 8.27827828e-05 8.29429429e-05 8.31031031e-05 8.32632633e-05
 8.34234234e-05 8.35835836e-05 8.37437437e-05 8.39039039e-05
 8.40640641e-05 8.42242242e-05 8.43843844e-05 8.45445445e-05
 8.47047047e-05 8.48648649e-05 8.50250250e-05 8.51851852e-05
 8.53453453e-05 8.55055055e-05 8.56656657e-05 8.58258258e-05
 8.59859860e-05 8.61461461e-05 8.63063063e-05 8.64664665e-05
 8.66266266e-05 8.67867868e-05 8.69469469e-05 8.71071071e-05
 8.72672673e-05 8.74274274e-05 8.75875876e-05 8.77477477e-05
 8.79079079e-05 8.80680681e-05 8.82282282e-05 8.83883884e-05
 8.85485485e-05 8.87087087e-05 8.88688689e-05 8.90290290e-05
 8.91891892e-05 8.93493493e-05 8.95095095e-05 8.96696697e-05
 8.98298298e-05 8.99899900e-05 9.01501502e-05 9.03103103e-05
 9.04704705e-05 9.06306306e-05 9.07907908e-05 9.09509510e-05
 9.11111111e-05 9.12712713e-05 9.14314314e-05 9.15915916e-05
 9.17517518e-05 9.19119119e-05 9.20720721e-05 9.22322322e-05
 9.23923924e-05 9.25525526e-05 9.27127127e-05 9.28728729e-05
 9.30330330e-05 9.31931932e-05 9.33533534e-05 9.35135135e-05
 9.36736737e-05 9.38338338e-05 9.39939940e-05 9.41541542e-05
 9.43143143e-05 9.44744745e-05 9.46346346e-05 9.47947948e-05
 9.49549550e-05 9.51151151e-05 9.52752753e-05 9.54354354e-05
 9.55955956e-05 9.57557558e-05 9.59159159e-05 9.60760761e-05
 9.62362362e-05 9.63963964e-05 9.65565566e-05 9.67167167e-05
 9.68768769e-05 9.70370370e-05 9.71971972e-05 9.73573574e-05
 9.75175175e-05 9.76776777e-05 9.78378378e-05 9.79979980e-05
 9.81581582e-05 9.83183183e-05 9.84784785e-05 9.86386386e-05
 9.87987988e-05 9.89589590e-05 9.91191191e-05 9.92792793e-05
 9.94394394e-05 9.95995996e-05 9.97597598e-05 9.99199199e-05
 1.00080080e-04 1.00240240e-04 1.00400400e-04 1.00560561e-04
 1.00720721e-04 1.00880881e-04 1.01041041e-04 1.01201201e-04
 1.01361361e-04 1.01521522e-04 1.01681682e-04 1.01841842e-04
 1.02002002e-04 1.02162162e-04 1.02322322e-04 1.02482482e-04
 1.02642643e-04 1.02802803e-04 1.02962963e-04 1.03123123e-04
 1.03283283e-04 1.03443443e-04 1.03603604e-04 1.03763764e-04
 1.03923924e-04 1.04084084e-04 1.04244244e-04 1.04404404e-04
 1.04564565e-04 1.04724725e-04 1.04884885e-04 1.05045045e-04
 1.05205205e-04 1.05365365e-04 1.05525526e-04 1.05685686e-04
 1.05845846e-04 1.06006006e-04 1.06166166e-04 1.06326326e-04
 1.06486486e-04 1.06646647e-04 1.06806807e-04 1.06966967e-04
 1.07127127e-04 1.07287287e-04 1.07447447e-04 1.07607608e-04
 1.07767768e-04 1.07927928e-04 1.08088088e-04 1.08248248e-04
 1.08408408e-04 1.08568569e-04 1.08728729e-04 1.08888889e-04
 1.09049049e-04 1.09209209e-04 1.09369369e-04 1.09529530e-04
 1.09689690e-04 1.09849850e-04 1.10010010e-04 1.10170170e-04
 1.10330330e-04 1.10490490e-04 1.10650651e-04 1.10810811e-04
 1.10970971e-04 1.11131131e-04 1.11291291e-04 1.11451451e-04
 1.11611612e-04 1.11771772e-04 1.11931932e-04 1.12092092e-04
 1.12252252e-04 1.12412412e-04 1.12572573e-04 1.12732733e-04
 1.12892893e-04 1.13053053e-04 1.13213213e-04 1.13373373e-04
 1.13533534e-04 1.13693694e-04 1.13853854e-04 1.14014014e-04
 1.14174174e-04 1.14334334e-04 1.14494494e-04 1.14654655e-04
 1.14814815e-04 1.14974975e-04 1.15135135e-04 1.15295295e-04
 1.15455455e-04 1.15615616e-04 1.15775776e-04 1.15935936e-04
 1.16096096e-04 1.16256256e-04 1.16416416e-04 1.16576577e-04
 1.16736737e-04 1.16896897e-04 1.17057057e-04 1.17217217e-04
 1.17377377e-04 1.17537538e-04 1.17697698e-04 1.17857858e-04
 1.18018018e-04 1.18178178e-04 1.18338338e-04 1.18498498e-04
 1.18658659e-04 1.18818819e-04 1.18978979e-04 1.19139139e-04
 1.19299299e-04 1.19459459e-04 1.19619620e-04 1.19779780e-04
 1.19939940e-04 1.20100100e-04 1.20260260e-04 1.20420420e-04
 1.20580581e-04 1.20740741e-04 1.20900901e-04 1.21061061e-04
 1.21221221e-04 1.21381381e-04 1.21541542e-04 1.21701702e-04
 1.21861862e-04 1.22022022e-04 1.22182182e-04 1.22342342e-04
 1.22502503e-04 1.22662663e-04 1.22822823e-04 1.22982983e-04
 1.23143143e-04 1.23303303e-04 1.23463463e-04 1.23623624e-04
 1.23783784e-04 1.23943944e-04 1.24104104e-04 1.24264264e-04
 1.24424424e-04 1.24584585e-04 1.24744745e-04 1.24904905e-04
 1.25065065e-04 1.25225225e-04 1.25385385e-04 1.25545546e-04
 1.25705706e-04 1.25865866e-04 1.26026026e-04 1.26186186e-04
 1.26346346e-04 1.26506507e-04 1.26666667e-04 1.26826827e-04
 1.26986987e-04 1.27147147e-04 1.27307307e-04 1.27467467e-04
 1.27627628e-04 1.27787788e-04 1.27947948e-04 1.28108108e-04
 1.28268268e-04 1.28428428e-04 1.28588589e-04 1.28748749e-04
 1.28908909e-04 1.29069069e-04 1.29229229e-04 1.29389389e-04
 1.29549550e-04 1.29709710e-04 1.29869870e-04 1.30030030e-04
 1.30190190e-04 1.30350350e-04 1.30510511e-04 1.30670671e-04
 1.30830831e-04 1.30990991e-04 1.31151151e-04 1.31311311e-04
 1.31471471e-04 1.31631632e-04 1.31791792e-04 1.31951952e-04
 1.32112112e-04 1.32272272e-04 1.32432432e-04 1.32592593e-04
 1.32752753e-04 1.32912913e-04 1.33073073e-04 1.33233233e-04
 1.33393393e-04 1.33553554e-04 1.33713714e-04 1.33873874e-04
 1.34034034e-04 1.34194194e-04 1.34354354e-04 1.34514515e-04
 1.34674675e-04 1.34834835e-04 1.34994995e-04 1.35155155e-04
 1.35315315e-04 1.35475475e-04 1.35635636e-04 1.35795796e-04
 1.35955956e-04 1.36116116e-04 1.36276276e-04 1.36436436e-04
 1.36596597e-04 1.36756757e-04 1.36916917e-04 1.37077077e-04
 1.37237237e-04 1.37397397e-04 1.37557558e-04 1.37717718e-04
 1.37877878e-04 1.38038038e-04 1.38198198e-04 1.38358358e-04
 1.38518519e-04 1.38678679e-04 1.38838839e-04 1.38998999e-04
 1.39159159e-04 1.39319319e-04 1.39479479e-04 1.39639640e-04
 1.39799800e-04 1.39959960e-04 1.40120120e-04 1.40280280e-04
 1.40440440e-04 1.40600601e-04 1.40760761e-04 1.40920921e-04
 1.41081081e-04 1.41241241e-04 1.41401401e-04 1.41561562e-04
 1.41721722e-04 1.41881882e-04 1.42042042e-04 1.42202202e-04
 1.42362362e-04 1.42522523e-04 1.42682683e-04 1.42842843e-04
 1.43003003e-04 1.43163163e-04 1.43323323e-04 1.43483483e-04
 1.43643644e-04 1.43803804e-04 1.43963964e-04 1.44124124e-04
 1.44284284e-04 1.44444444e-04 1.44604605e-04 1.44764765e-04
 1.44924925e-04 1.45085085e-04 1.45245245e-04 1.45405405e-04
 1.45565566e-04 1.45725726e-04 1.45885886e-04 1.46046046e-04
 1.46206206e-04 1.46366366e-04 1.46526527e-04 1.46686687e-04
 1.46846847e-04 1.47007007e-04 1.47167167e-04 1.47327327e-04
 1.47487487e-04 1.47647648e-04 1.47807808e-04 1.47967968e-04
 1.48128128e-04 1.48288288e-04 1.48448448e-04 1.48608609e-04
 1.48768769e-04 1.48928929e-04 1.49089089e-04 1.49249249e-04
 1.49409409e-04 1.49569570e-04 1.49729730e-04 1.49889890e-04
 1.50050050e-04 1.50210210e-04 1.50370370e-04 1.50530531e-04
 1.50690691e-04 1.50850851e-04 1.51011011e-04 1.51171171e-04
 1.51331331e-04 1.51491491e-04 1.51651652e-04 1.51811812e-04
 1.51971972e-04 1.52132132e-04 1.52292292e-04 1.52452452e-04
 1.52612613e-04 1.52772773e-04 1.52932933e-04 1.53093093e-04
 1.53253253e-04 1.53413413e-04 1.53573574e-04 1.53733734e-04
 1.53893894e-04 1.54054054e-04 1.54214214e-04 1.54374374e-04
 1.54534535e-04 1.54694695e-04 1.54854855e-04 1.55015015e-04
 1.55175175e-04 1.55335335e-04 1.55495495e-04 1.55655656e-04
 1.55815816e-04 1.55975976e-04 1.56136136e-04 1.56296296e-04
 1.56456456e-04 1.56616617e-04 1.56776777e-04 1.56936937e-04
 1.57097097e-04 1.57257257e-04 1.57417417e-04 1.57577578e-04
 1.57737738e-04 1.57897898e-04 1.58058058e-04 1.58218218e-04
 1.58378378e-04 1.58538539e-04 1.58698699e-04 1.58858859e-04
 1.59019019e-04 1.59179179e-04 1.59339339e-04 1.59499499e-04
 1.59659660e-04 1.59819820e-04 1.59979980e-04 1.60140140e-04
 1.60300300e-04 1.60460460e-04 1.60620621e-04 1.60780781e-04
 1.60940941e-04 1.61101101e-04 1.61261261e-04 1.61421421e-04
 1.61581582e-04 1.61741742e-04 1.61901902e-04 1.62062062e-04
 1.62222222e-04 1.62382382e-04 1.62542543e-04 1.62702703e-04
 1.62862863e-04 1.63023023e-04 1.63183183e-04 1.63343343e-04
 1.63503504e-04 1.63663664e-04 1.63823824e-04 1.63983984e-04
 1.64144144e-04 1.64304304e-04 1.64464464e-04 1.64624625e-04
 1.64784785e-04 1.64944945e-04 1.65105105e-04 1.65265265e-04
 1.65425425e-04 1.65585586e-04 1.65745746e-04 1.65905906e-04
 1.66066066e-04 1.66226226e-04 1.66386386e-04 1.66546547e-04
 1.66706707e-04 1.66866867e-04 1.67027027e-04 1.67187187e-04
 1.67347347e-04 1.67507508e-04 1.67667668e-04 1.67827828e-04
 1.67987988e-04 1.68148148e-04 1.68308308e-04 1.68468468e-04
 1.68628629e-04 1.68788789e-04 1.68948949e-04 1.69109109e-04
 1.69269269e-04 1.69429429e-04 1.69589590e-04 1.69749750e-04
 1.69909910e-04 1.70070070e-04 1.70230230e-04 1.70390390e-04
 1.70550551e-04 1.70710711e-04 1.70870871e-04 1.71031031e-04
 1.71191191e-04 1.71351351e-04 1.71511512e-04 1.71671672e-04
 1.71831832e-04 1.71991992e-04 1.72152152e-04 1.72312312e-04
 1.72472472e-04 1.72632633e-04 1.72792793e-04 1.72952953e-04
 1.73113113e-04 1.73273273e-04 1.73433433e-04 1.73593594e-04
 1.73753754e-04 1.73913914e-04 1.74074074e-04 1.74234234e-04
 1.74394394e-04 1.74554555e-04 1.74714715e-04 1.74874875e-04
 1.75035035e-04 1.75195195e-04 1.75355355e-04 1.75515516e-04
 1.75675676e-04 1.75835836e-04 1.75995996e-04 1.76156156e-04
 1.76316316e-04 1.76476476e-04 1.76636637e-04 1.76796797e-04
 1.76956957e-04 1.77117117e-04 1.77277277e-04 1.77437437e-04
 1.77597598e-04 1.77757758e-04 1.77917918e-04 1.78078078e-04
 1.78238238e-04 1.78398398e-04 1.78558559e-04 1.78718719e-04
 1.78878879e-04 1.79039039e-04 1.79199199e-04 1.79359359e-04
 1.79519520e-04 1.79679680e-04 1.79839840e-04 1.80000000e-04] cm in units of ph / (s cm3)
[pyEDITH] WARNING [2026-05-19 11:18:12,637] `FstarV_10pc` not specified in parameters. Calculating internally...

Important

If you chose to let the ETC re-grid your input spectra (using user supplied settings shown above, then you have to run this extra step). This will regrid the spectra to the specified resolution of the various channels.

[9]:
if parameters["regrid_wavelength"] is True:
    scene.regrid_spectra(parameters, observation)

[pyEDITH] INFO [2026-05-19 11:18:12,672] Re-gridding spectra onto ETC wavelength grid...
[10]:
plt.figure(dpi=100)
ax = plt.gca()
plt.step(observation.wavelength, (getattr(scene, "Fs_over_F0")*getattr(scene, "F0")*getattr(scene, "Fp_over_Fs")), linewidth=2, color=colors.cyan)
plt.xlabel("Wavelength [um]", fontsize=14, fontweight='bold')
plt.ylabel("Flux [photons/nm/s/cm^2]", fontsize=14, fontweight='bold')
plt.title("Absolute Flux of the Planet")
plt.tick_params(axis='both', which='major', labelsize=12, width=1.5, length=6)
plt.tick_params(axis='both', which='minor', width=1, length=3)
for spine in ax.spines.values():
    spine.set_visible(True)
    spine.set_linewidth(1.5)
plt.grid(True, alpha=0.3, linestyle='--', linewidth=0.8)
plt.tight_layout()

plt.figure(dpi=100)
ax = plt.gca()
plt.step(observation.wavelength, (getattr(scene, "Fs_over_F0")*getattr(scene, "F0")), linewidth=2, color=colors.cyan)
plt.xlabel("Wavelength [um]", fontsize=14, fontweight='bold')
plt.ylabel("Flux [photons/nm/s/cm^2]", fontsize=14, fontweight='bold')
plt.title("Absolute Flux of the Star")
plt.tick_params(axis='both', which='major', labelsize=12, width=1.5, length=6)
plt.tick_params(axis='both', which='minor', width=1, length=3)
for spine in ax.spines.values():
    spine.set_visible(True)
    spine.set_linewidth(1.5)
plt.grid(True, alpha=0.3, linestyle='--', linewidth=0.8)
plt.tight_layout()

_images/spectroscopy_tutorial_18_0.png
_images/spectroscopy_tutorial_18_1.png

1.1.3 Observatory#

Set up the observatory: telescope, coronagraph, detector. This compiles the telescope, coronagraph, and detector and places them all in a single object. These parameters mostly come from the EAC YAML files, but we have to specify a few:

[11]:
# tells ETC to use EAC1 yaml files throughputs
parameters["observatory_preset"] = "EAC1"

# extra throughput of the IFS
parameters["IFS_eff"]  = 1.

# number of detector pixels per spectral bin
parameters["npix_multiplier"] = np.ones_like(parameters["wavelength"])

# post processing factor (30 is a good realistic value)
parameters["noisefloor_PPF"] = 30

# Where to truncate the PSF (30% of the maximum value)
parameters["psf_trunc_ratio"] = 0.3

[12]:
observatory_config = pE.parse_input.get_observatory_config(parameters)
observatory = pE.Observatory()
observatory.create_observatory(observatory_config)
observatory.load_configuration(
     parameters, observation, scene
)
observatory.validate_configuration()
[pyEDITH] INFO [2026-05-19 11:18:12,831] Observatory Configuration:
[pyEDITH] INFO [2026-05-19 11:18:12,831]   Using preset: EAC1
[pyEDITH] INFO [2026-05-19 11:18:12,832]
[pyEDITH] WARNING [2026-05-19 11:18:12,832] Coronagraph 'eac1_optimal_order_6_1d' not found locally. Attempting to fetch from remote database...
[yippy] INFO [2026-05-19 11:18:12,832] Fetching YIP 'eac1_optimal_order_6_1d' (cache: /Users/ealei/Library/Caches/yippy)
[yippy] INFO [2026-05-19 11:18:12,878] YIP 'eac1_optimal_order_6_1d' available at /Users/ealei/Library/Caches/yippy/eac1_optimal_order_6_1d.zip.unzip/eac1_optimal_order_6_1d
[pyEDITH] INFO [2026-05-19 11:18:12,879] Successfully downloaded coronagraph to: /Users/ealei/Library/Caches/yippy/eac1_optimal_order_6_1d.zip.unzip/eac1_optimal_order_6_1d
[yippy] INFO [2026-05-19 11:18:12,896] Creating eac1_optimal_order_6_1d coronagraph
[yippy] WARNING [2026-05-19 11:18:12,897] Unhandled header fields: {'TMULDET', 'TMULCHAR'}
[yippy] WARNING [2026-05-19 11:18:12,898] Using default unit for D: m. Could not extract unit from comment: "circumscribed diameter of the telescope in mete"
[yippy] WARNING [2026-05-19 11:18:12,899] Using default unit for D_INSC: m. Could not extract unit from comment: "inscribed diameter of the telescope in meters"
[yippy] INFO [2026-05-19 11:18:12,982] eac1_optimal_order_6_1d is radially symmetric
[yippy] INFO [2026-05-19 11:18:13,137] Loading performance metrics from /Users/ealei/Library/Caches/yippy/eac1_optimal_order_6_1d.zip.unzip/eac1_optimal_order_6_1d/yippy_cache/performance/trunc_0.30_v2.7.3.fits
[yippy] INFO [2026-05-19 11:18:13,138] Loading throughput and contrast from trunc_0.30_v2.7.3.fits
[yippy] INFO [2026-05-19 11:18:13,148] Successfully loaded performance data from trunc_0.30_v2.7.3.fits
[yippy] INFO [2026-05-19 11:18:13,148] Computing core area curve (PSF trunc ratio = 0.3)...
[yippy] INFO [2026-05-19 11:18:13,477] Computing occulter transmission curve...
[yippy] INFO [2026-05-19 11:18:13,620] Computing core mean intensity curve...
[yippy] INFO [2026-05-19 11:18:13,719] OWA set to max_offset_in_image: 32.00 lam/D
[yippy] INFO [2026-05-19 11:18:13,720] Created eac1_optimal_order_6_1d
[pyEDITH] INFO [2026-05-19 11:18:13,721] Using psf_trunc_ratio to calculate Omega...
[pyEDITH] INFO [2026-05-19 11:18:13,730] Setting the noise floor via user-supplied noisefloor_PPF...
[pyEDITH] INFO [2026-05-19 11:18:13,734] Calculating optics throughput from preset...
[pyEDITH] INFO [2026-05-19 11:18:13,734] Calculating epswarmTrcold as 1 - optics throughput...

1.2 Running the ETC#

We can now calculate the exposure time for the desired SNR in each spectral bin. This calculates the exposure time necessary for each spectral bin to reach the desired SNR (7). After running, the calculated exposure times will be an attribute (exptime) of the pyEDITH object.

[13]:
pE.calculate_exposure_time_or_snr(observation, scene, observatory)

Let’s plot it:

[14]:
plt.figure(dpi=100)
ax = plt.gca()

# Plot finite exposure times
finite_mask = np.isfinite(observation.exptime)
plt.step(observation.wavelength[finite_mask], observation.exptime[finite_mask],
         where="mid", linewidth=2, color=colors.cyan, label='Valid exposures')

# Plot infinite exposure times
inf_mask = np.isinf(observation.exptime)
if np.any(inf_mask):
    max_finite = np.max(observation.exptime[finite_mask])
    plt.scatter(observation.wavelength[inf_mask],
                max_finite * np.ones_like(observation.wavelength[inf_mask]),
                marker="x", color=colors.red, s=100, linewidths=2.5,
                label='Infinite exposures', zorder=5)

plt.xlabel("Wavelength (µm)", fontsize=14, fontweight='bold')
plt.ylabel("Exposure time (s)", fontsize=14, fontweight='bold')
plt.tick_params(axis='both', which='major', labelsize=12, width=1.5, length=6)
plt.tick_params(axis='both', which='minor', width=1, length=3)
plt.yscale("log")

for spine in ax.spines.values():
    spine.set_visible(True)
    spine.set_linewidth(1.5)

plt.grid(True, alpha=0.3, linestyle='--', linewidth=0.8)
plt.legend(loc='best', fontsize=11, frameon=True, shadow=True, fancybox=True,
           framealpha=0.95, edgecolor='gray')
plt.tight_layout()

_images/spectroscopy_tutorial_25_0.png

A more useful functionality for the observer is to calculate the SNR you get for each spectral bin when you give it an exposure time. To do this, we need to specify a different parameter called obstime. This is the exposure time at a reference lambda that will drive the observation. We can take one of the exposure times calculated above.

[15]:
# Given this reference lambda, define the target obstime
ref_lam = 0.5*u.um
ind_reflam = np.argmin(np.abs(observation.wavelength - ref_lam))
observation.obstime = observation.exptime[ind_reflam] # seconds

# Calculate the SNR for all bins
pE.calculate_exposure_time_or_snr(observation, scene, observatory, mode="signal_to_noise")

Let’s plot it:

[16]:
plt.figure(dpi=100)
ax = plt.gca()
plt.step(observation.wavelength, observation.fullsnr, where="mid", linewidth=2, color=colors.cyan)
plt.title(f"Exposure time to achieve SNR={parameters['snr'][0]} at {ref_lam}: {observation.obstime.to(u.hr).round(2)}", fontsize=14, fontweight='bold')
plt.ylabel("SNR", fontsize=14, fontweight='bold')
plt.xlabel("Wavelength (um)", fontsize=14, fontweight='bold')
plt.tick_params(axis='both', which='major', labelsize=12, width=1.5, length=6)
plt.tick_params(axis='both', which='minor', width=1, length=3)
for spine in ax.spines.values():
    spine.set_visible(True)
    spine.set_linewidth(1.5)
plt.grid(True, alpha=0.3, linestyle='--', linewidth=0.8)
plt.tight_layout()

_images/spectroscopy_tutorial_29_0.png

Now let’s say you want to calculate different exposure times for the different spectral channels you may have specified at the beginning of this tutorial. This is how to do it with the ETC:

[17]:
# Get the number of spectral channels from the parameters dictionary
nchannels = len(parameters["spectral_resolution"])

# Define reference wavelengths (in microns) for each spectral channel
# These wavelengths will be used to calculate exposure times for each channel
ref_lams = [0.39, 0.5, 1.1]

# Validate that reference wavelengths are within their respective channel boundaries
# UV channel: lambda < lam_high[0]
assert ref_lams[0] < parameters["lam_high"][0]
# VIS channel: lam_high[0] <= lambda < lam_high[1]
assert (ref_lams[1] >= parameters["lam_high"][0]) & (ref_lams[1] < parameters["lam_high"][1])
# NIR channel: lambda >= lam_high[1]
assert ref_lams[2] >= parameters["lam_high"][1]

# Get the indeces corresponding to the spectral channels:
# Remember, we are using observation.wavelength instead of parameters["wavelength"]
# because it is the re-gridded wavelength grid the ETC uses internally (we specified this earlier)

# UV channel: all wavelengths below the first boundary
inds_UV = observation.wavelength.value < parameters["lam_high"][0]
# VIS channel: wavelengths between first and second boundary
inds_VIS = (observation.wavelength.value >= parameters["lam_high"][0]) & (observation.wavelength.value < parameters["lam_high"][1])
# NIR channel: all wavelengths above the second boundary
inds_NIR =  observation.wavelength.value >= parameters["lam_high"][1]

# Store index arrays in a list for iteration
inds_arr = [inds_UV, inds_VIS, inds_NIR]

# Run the ETC calculation separately for each spectral channel
snr_arrs = []  # Store SNR arrays for each channel
exptime_arr = []  # Store exposure times for each channel

for ref_lam in ref_lams:
    # Find the index of the wavelength grid point closest to the reference wavelength
    ind_reflam = np.argmin(np.abs(observation.wavelength - ref_lam*u.um))

    # Set the observation time based on the exposure time at the reference wavelength (from the previous plot)
    observation.obstime = observation.exptime[ind_reflam]

    # Calculate the SNR for this channel using the ETC
    pE.calculate_exposure_time_or_snr(observation, scene, observatory, mode="signal_to_noise")

    # Store the resulting SNR and exposure time
    snr_arrs.append(np.copy(observation.fullsnr))
    exptime_arr.append(np.copy(observation.exptime[ind_reflam]))

labels=["UV", "VIS", "NIR"]

fig, axes = plt.subplots(2,1, sharex=True, sharey=True, dpi=100)

# Initialize an array to hold the concatenated SNR values from all channels
snr_concat_arr = np.empty(len(observation.wavelength))

# Plot the SNR for each spectral channel in the top subplot
for i in range(len(ref_lams)):
    snr_arr = snr_arrs[i]
    inds = inds_arr[i]

    axes[0].step(observation.wavelength[inds], snr_arr[inds], label=labels[i], where="mid", linewidth=2)

    snr_concat_arr[inds] = snr_arr[inds]

# Top subplot
axes[0].scatter(ref_lams, [7,7,7], color="k", marker="x", zorder=10, s=100)
axes[0].axhline(7, color="k", ls=":", alpha=0.5, linewidth=1)
axes[0].legend(loc='best', fontsize=11, frameon=True, shadow=True, fancybox=True,
               framealpha=0.95, edgecolor='gray')
axes[0].set_ylabel("SNR", fontsize=14, fontweight='bold')
axes[0].tick_params(axis='both', which='major', labelsize=12, width=1.5, length=6)
axes[0].tick_params(axis='both', which='minor', width=1, length=3)
axes[0].grid(True, alpha=0.3, linestyle='--', linewidth=0.8)

for spine in axes[0].spines.values():
    spine.set_visible(True)
    spine.set_linewidth(1.5)

exptime_text = f"UV({ref_lams[0]}um): {exptime_arr[0].to(u.hr).round(2)}\nVIS({ref_lams[1]}um): {exptime_arr[1].to(u.hr).round(2)}\nNIR({ref_lams[2]}um): {exptime_arr[2].to(u.hr).round(2)}"
axes[0].text(0.215, 0.95, f"Exposure times:\n{exptime_text}",  transform=axes[0].transAxes,fontsize=10, ha='right', va='top', bbox=dict(boxstyle='round', facecolor='white', alpha=0.95, edgecolor='gray'))
axes[0].set_ylim(0,15)

# Bottom subplot
axes[1].step(observation.wavelength, snr_concat_arr, color="k", label="concatenated UV+VIS+NIR", where="mid", linewidth=2)
axes[1].scatter(ref_lams, [7,7,7], color="red", marker="x", zorder=10, s=100)
axes[1].axhline(7, color="k", ls=":", alpha=0.5, linewidth=1)
axes[1].set_xlabel("Wavelength [um]", fontsize=14, fontweight='bold')
axes[1].legend(loc='best', fontsize=11, frameon=True, shadow=True, fancybox=True,
               framealpha=0.95, edgecolor='gray')
axes[1].set_ylabel("SNR", fontsize=14, fontweight='bold')
axes[1].tick_params(axis='both', which='major', labelsize=12, width=1.5, length=6)
axes[1].tick_params(axis='both', which='minor', width=1, length=3)
axes[1].grid(True, alpha=0.3, linestyle='--', linewidth=0.8)

for spine in axes[1].spines.values():
    spine.set_visible(True)
    spine.set_linewidth(1.5)

plt.tight_layout()

_images/spectroscopy_tutorial_31_0.png

2. Visualization#

Every variable is stored in either of the defined classes (see Glossary). This means that we can easily plot any variable we need.

Throughput:

[18]:
plt.figure(dpi=100)
ax = plt.gca()
plt.step(observation.wavelength, observatory.total_throughput, where="mid",
         linewidth=2, color=colors.cyan)
plt.xlabel("Wavelength [um]", fontsize=14, fontweight='bold')
plt.ylabel("Throughput", fontsize=14, fontweight='bold')
plt.title("Total Throughput")
plt.tick_params(axis='both', which='major', labelsize=12, width=1.5, length=6)
plt.tick_params(axis='both', which='minor', width=1, length=3)
for spine in ax.spines.values():
    spine.set_visible(True)
    spine.set_linewidth(1.5)
plt.grid(True, alpha=0.3, linestyle='--', linewidth=0.8)
plt.tight_layout()

_images/spectroscopy_tutorial_34_0.png

Coronagraph response maps:

[19]:
plt.figure(dpi=100)
ax = plt.gca()
data1 = np.log10(observatory.coronagraph.Istar)
plt.imshow(data1)
cbar = plt.colorbar()
cbar.ax.tick_params(labelsize=12, width=1.5, length=6)
plt.title("Coronagraph on-axis PSF", fontsize=14, fontweight='bold', pad=15)
plt.tick_params(axis='both', which='major', labelsize=12, width=1.5, length=6)
plt.tick_params(axis='both', which='minor', width=1, length=3)
for spine in ax.spines.values():
    spine.set_visible(True)
    spine.set_linewidth(1.5)
plt.clim(np.nanmin(data1), np.nanmax(data1))
plt.tight_layout()

plt.figure(dpi=100)
ax = plt.gca()
data2 = np.log10(observatory.coronagraph.noisefloor)
plt.imshow(data2)
cbar = plt.colorbar()
cbar.ax.tick_params(labelsize=12, width=1.5, length=6)
plt.title("Coronagraph noisefloor", fontsize=14, fontweight='bold', pad=15)
plt.tick_params(axis='both', which='major', labelsize=12, width=1.5, length=6)
plt.tick_params(axis='both', which='minor', width=1, length=3)
for spine in ax.spines.values():
    spine.set_visible(True)
    spine.set_linewidth(1.5)
plt.clim(np.nanmin(data2), np.nanmax(data2))
plt.tight_layout()

plt.figure(dpi=100)
ax = plt.gca()
data3 = observatory.coronagraph.skytrans
plt.imshow(data3)
cbar = plt.colorbar()
cbar.ax.tick_params(labelsize=12, width=1.5, length=6)
plt.title("Coronagraph skytrans", fontsize=14, fontweight='bold', pad=15)
plt.tick_params(axis='both', which='major', labelsize=12, width=1.5, length=6)
plt.tick_params(axis='both', which='minor', width=1, length=3)
for spine in ax.spines.values():
    spine.set_visible(True)
    spine.set_linewidth(1.5)
plt.clim(np.nanmin(data3), np.nanmax(data3))
plt.tight_layout()


_images/spectroscopy_tutorial_36_0.png
_images/spectroscopy_tutorial_36_1.png
_images/spectroscopy_tutorial_36_2.png

pyEDITH also has a specific function to synthesize observations:

[20]:
obs, noise = pE.utils.synthesize_observation(snr_concat_arr,
                                             scene,
                                             random_seed=42, # seed defaults to None
                                             set_below_zero=0., # if the fake data falls below zero, set the data point as this. default = NaN
                                             )

import matplotlib.pyplot as plt

fig = plt.figure(dpi=100)
ax1 = plt.subplot(2, 1, 1)
ax1.step(observation.wavelength, scene.Fp_over_Fs, color="k", where="mid", zorder=10, linewidth=2)
ax1.errorbar(observation.wavelength, obs, yerr=noise, fmt="o", color=colors.cyan,
             markersize=5, markerfacecolor=colors.cyan, markeredgecolor='white',
             markeredgewidth=0.5, elinewidth=1, capsize=3)
ax1.set_ylabel("Fp/Fs", fontsize=14, fontweight='bold')
ax1.set_title("Synthetic Observation", fontsize=14, fontweight='bold')
exptime_text = f"UV({ref_lams[0]}um): {exptime_arr[0].to(u.hr).round(2)}\nVIS({ref_lams[1]}um): {exptime_arr[1].to(u.hr).round(2)}\nNIR({ref_lams[2]}um): {exptime_arr[2].to(u.hr).round(2)}"



ax1.text(0.94, 0.83, f"Exposure times:\n{exptime_text}",  transform=axes[0].transAxes,fontsize=10, ha='right', va='top', bbox=dict(boxstyle='round', facecolor='white', alpha=0.95, edgecolor='gray'))

ax1.set_ylim(np.median(scene.Fp_over_Fs) - 1.5 * np.median(scene.Fp_over_Fs), np.median(scene.Fp_over_Fs) + 1.5 * np.median(scene.Fp_over_Fs))
ax1.tick_params(axis='both', which='major', labelsize=12, width=1.5, length=6)
ax1.tick_params(axis='both', which='minor', width=1, length=3)
for spine in ax1.spines.values():
    spine.set_visible(True)
    spine.set_linewidth(1.5)
ax1.grid(True, alpha=0.3, linestyle='--', linewidth=0.8)

ax2 = plt.subplot(2, 1, 2, sharex=ax1)
ax2.step(observation.wavelength, snr_concat_arr, where="mid", zorder=10, linewidth=2, color=colors.cyan)
ax2.set_xlabel("Wavelength [um]", fontsize=14, fontweight='bold')
ax2.set_ylabel("SNR", fontsize=14, fontweight='bold')
ax2.axhline(7, color="k", ls=":", linewidth=1,alpha=0.5)
ax2.tick_params(axis='both', which='major', labelsize=12, width=1.5, length=6)
ax2.tick_params(axis='both', which='minor', width=1, length=3)
for spine in ax2.spines.values():
    spine.set_visible(True)
    spine.set_linewidth(1.5)
ax2.grid(True, alpha=0.3, linestyle='--', linewidth=0.8)

plt.tight_layout()

_images/spectroscopy_tutorial_38_0.png

Photon counts can also be plotted (we use a logfile that is automatically created when running with logging == DEBUG, photon_counts.pk):

[21]:
import pickle
import matplotlib.pyplot as plt

# Rerun with debug mode
pE.set_verbosity(level='debug')
pE.calculate_exposure_time_or_snr(observation, scene, observatory)

# pick file that was produced
photon_counts = pickle.load(open("./photon_counts.pk", "rb"))
name_mapping = {
    "CRp": r"Planet ($CR_p$)",
    "CRbs": r"Stellar leakage ($CR_{b,s})$",
    "CRbz": r"Zodi ($CR_{b,z}$)",
    "CRbez": r"Exozodi ($CR_{b,ez}$)",
    "CRbth": r"Thermal ($CR_{b,th}$)",
    "CRbd": r"Detector ($CR_{b,d}$)",
    "CRnf": r"Noise Floor ($CR_{nf}$)",
    "CRb": r"Total Background ($CR_b$)"
}

fig = plt.figure(dpi=100)
ax = plt.gca()

for key in photon_counts.keys():
    if key not in ["PPF_ez", 'omega_lod', "CRnf_s", "CRnf_ez", "CRbbin","CRp","CRb"]:
        label = name_mapping.get(key, key)
        plt.plot(observation.wavelength, photon_counts[key], label=label, lw=1.5)
#Special treatment for CRp and CRb
label = name_mapping.get('CRp', 'CRp')
plt.plot(observation.wavelength, photon_counts['CRp'], label=label, lw=2.5,color='k')
label = name_mapping.get('CRb', 'CRb')
plt.plot(observation.wavelength, photon_counts['CRb'], label=label, lw=2.5,color='gray')

plt.yscale("log")
plt.ylim(1e-8, 5e0)
plt.ylabel("Photon count rate", fontsize=14, fontweight='bold')
plt.xlabel("Wavelength [um]", fontsize=14, fontweight='bold')
plt.tick_params(axis='both', which='major', labelsize=12, width=1.5, length=6)
plt.tick_params(axis='both', which='minor', width=1, length=3)
for spine in ax.spines.values():
    spine.set_visible(True)
    spine.set_linewidth(1.5)
plt.grid(True, alpha=0.3, linestyle='--', linewidth=0.8)
plt.legend(bbox_to_anchor=(0.5, -0.2), loc='upper center', fontsize=11, frameon=True, shadow=True, fancybox=True,
           framealpha=0.95, edgecolor='gray', ncol=2)
plt.title("Photon Count Rates", fontsize=14, fontweight='bold')
plt.tight_layout()

[pyEDITH] INFO [2026-05-19 11:18:20,992] Logging level set to: DEBUG
[yippy] INFO [2026-05-19 11:18:20,992] Logging level set to: DEBUG
[pyEDITH] DEBUG [2026-05-19 11:18:22,277] Printing all relevant variables in pyedith_validation.txt and pyedith_full_info.txt.
_images/spectroscopy_tutorial_40_1.png