Pandeia Tutorials
How to Use this Article
In this article, several examples on how to use Pandeia for Roman are given. Prior to reading these examples, users should consult the article Overview of Pandeia for information on how to install Pandeia and the necessary supporting data files, as well as for configuration information for Pandeia simulations. All calculations in this article were performed using Pandeia and reference data. This documentation is written for Pandeia version 2025.9 (released on September 15, 2025).
Example 1: Computing the Signal-to-Noise Ratio
Running the code below will generate output in the form of a dictionary that contains all of the information from the Pandeia Engine Report. This is largely the standard way of running Pandeia where the properties of the instrumental set up and astronomical scene are specified.
Description of Code Snippet
The configuration below uses the default point source normalized to an AB magnitude of 25, the WFI multi-accumulation (MA) table "im_135_8" with no truncation (135 seconds of science exposure time), and the F129 imaging filter. The Appendix: WFI MultiAccum Tables article in the Roman APT User's Guide provides and overview of MA tables in Roman at this time. See the article Overview of Pandeia for more information.
Python Implementation
from pandeia.engine.perform_calculation import perform_calculation
from pandeia.engine.calc_utils import build_default_calc
# Get Default Parameters
calc = build_default_calc('roman', 'wfi', 'imaging')
# Set the global variable for the filter name (change to any valid filter)
FILTER = 'f129'
# Set the MA table to im_135_8
calc['configuration']['detector']['ma_table_name'] = 'im_135_8'
# Modify defaults to simulate a 25th AB magnitude source
MAG = 25
calc['scene'][0]['spectrum']['normalization']['norm_flux'] = MAG
calc['scene'][0]['spectrum']['normalization']['norm_fluxunit'] = 'abmag'
# Set number of exposures and filter
NEXP = 3
calc['configuration']['detector']['nexp'] = NEXP
calc['configuration']['instrument']['filter'] = FILTER
# Run calculation and return signal-to-noise ratio
report = perform_calculation(calc)
snr = report['scalar']['sn']
print(f'Estimated S/N: {snr:.2f}')
Warnings from Running the Code Block
This step may generate a WARNING from synphot that the spectrum is extrapolated, which can be ignored.
Running
Pandeia
for Roman will likely return a warning such as: if np.log(abs(val)) < -1*precision and val != 0.0. This is related to a JWST-specific test for float precision, and can be ignored in this case.
Result from the Example
This calculation should output an estimated signal-to-noise of 11.91 .
Example 2: Calculating the Corresponding Magnitude for a Given Setup
In this example, it is assumed that the user has an exposure configuration and is interested in understanding the corresponding magnitude for a given signal-to-noise ratio and for a specific observing setup. This application may be common for Roman users exploring the Roman science data archive.
The default observational setup for Roman will be used. As in the previous example, the MA table is set to "im_135_8". The Table of Code Inputs for Limiting Magnitude Calculation summarizes the parameters that can be adjusted in the Python implementation of this example and their presets. Starting with this example, users can change these parameters to better match their scientific use case.
Description of Code Snippet
The output from the code is the
limiting magnitude
achievable by an instrument setup with
MA_TABLE
= 'im_135_8',
NEXP
= 10, and
FILTER
= 'f129', assuming a minimum
SN
= 5 and a source with a flat SED. The calculation is determined by setting up a helper function
compute_mag
to compute the SNR for a given magnitude; and a function
mag2sn
that uses the results of many helper computations to find the limiting magnitude that produces the requested
SN
. The latter function sets up build_default_calc for Roman and performs the
Pandeia
simulations over a range of magnitudes iteratively to find the best match magnitude for the specified signal-to-noise. The parameters summarized in the Table of Code Inputs for Corresponding Magnitude Calculation are input near the end of the code-block and can be easily modified for the use case of interest. The result of this code is given at the end of the code block for a user to confirm their execution of the code.
Table of Code Inputs for Corresponding Magnitude Calculation
| Specified Input | Description | Parameter in Code Example | Value in Code Example |
|---|---|---|---|
signal-to-noise | the value that is useful for the science case being investigated | SN | 5 |
number of exposures | the number of individual exposures of a given Multi-Accumulation sequence | NEXP | 10 |
filter | the filter used in the observation | FILTER | 'f129' |
Python Implementation
from pandeia.engine.calc_utils import build_default_calc
from pandeia.engine.perform_calculation import perform_calculation
from scipy import interpolate
import numpy as np
def compute_mag(filt, nexp, bracket=(18, 30)):
"""
Method to compute the magnitude from S/N and number of exposures
Parameters
----------
filt : str
Name of Roman WFI filter
nexp : int
Number of exposures
bracket : tuple
Range of magnitudes to test. default: (18, 30)
Returns
-------
mag_range : float
An array of magnitudes used to compute the SNRs
computed_snrs: float
An array of computed SNRs from Pandeia calculations
"""
# Set up default Roman observation
calc = build_default_calc('roman', 'wfi', 'imaging')
# Modify defaults to place a source with an AB magnitude
calc['scene'][0]['spectrum']['normalization']['norm_fluxunit'] = 'abmag'
calc['scene'][0]['spectrum']['normalization']['norm_waveunit'] = 'um'
# Set number of exposures and filter
calc['configuration']['detector']['nexp'] = nexp
calc['configuration']['instrument']['filter'] = filt
# Create an array of magnitudes range of interest
mag_range = np.arange(bracket[0], bracket[1]+1, 1)
# Create empty lists to save the computations
computed_snrs = []
# Compute the SNRs for a given magnitude
for m in range(len(mag_range)):
mag = mag_range[m]
calc['scene'][0]['spectrum']['normalization']['norm_flux'] = mag
report = perform_calculation(calc)
computed_snrs.append(report['scalar']['sn'])
return mag_range, computed_snrs
def mag2sn(mag_range, computed_snrs, sntarget):
"""
Calculate a magnitude given a desired SNR by interpolating (computed_snrs, mag_range) from compute_mag
Parameters
----------
mag_range: float
An array of magnitudes used in calculating a range of SNRs in compute_mag
computed_snrs: float
An array of computed SNR given the mag_range using Pandeia calculation object
sntarget: float
Required S/N
"""
interpolator = interpolate.interp1d(computed_snrs, mag_range)
mag = interpolator(sntarget)
return mag
# Required S/N and number of exposures
SN = 5.
NEXP = 10
FILTER = 'f129'
# Run minimizer function to estimate the magnitude given sn and nexp
mag_range, computed_snrs = compute_mag(FILTER, NEXP)
mag = mag2sn(mag_range, computed_snrs, SN)
print(f'Estimated magnitude: {mag:.2f}')
Result from the Example
This calculation should output an estimated limiting magnitude of 26.76 mag at a signal-to-noise of 5 based on the inputs from the Table of Code Inputs for Corresponding Magnitude Calculation.
Example 3: Determining the Optimal Number of Exposures
In this example, we assume the user has a required signal-to-noise at a desired magnitude limit, and wishes to know the number of exposures required to achieve these observational results with the default MA table.
Description of Code Snippet
In this case, the inputs to the code are
SN
,
MAG
, and
FILTER
, which are described in the Table of Code Inputs for Determining Number of Exposures. The output will be
NEXP
. The code will assume a flat spectral energy distribution (SED). The calculation is determined by setting up a helper function to optimize the signal-to-noise at the input magnitude and a method that computes the number of exposures at a given signal-to-noise given the source magnitude. The latter function sets up build_default_calc for Roman and iteratively performs the
Pandeia
simulations over a range of exposures to find the best match. The parameters summarized in the Table of Code Inputs for Determining Number of Exposures are input near the end of the code-block and can be easily modified for the use case of interest. The result of this code is given at the end of the code block.
Table of Code Inputs for Determining Number of Exposures
| Specified Input | Description | Parameter in Code Example | Value in Code Example |
|---|---|---|---|
signal-to-noise | value that is useful for the science case being investigated | SN | 20 |
source magnitude | magnitude in ABMag for the source of interest | MAG | 26 |
filter | filter used in the observation | FILTER | 'f129' |
Python Implementation
from scipy.optimize import minimize_scalar
from pandeia.engine.calc_utils import build_default_calc
from pandeia.engine.perform_calculation import perform_calculation
def _nexp2sn_(nexp, calc, sntarget):
"""
Helper function to optimize the S/N given a number of exposures.
"""
calc['configuration']['detector']['nexp'] = int(nexp)
etc = perform_calculation(calc)['scalar']
return (sntarget - etc['sn'])**2
def compute_nexp(filt, sn, mag, bracket=(1, 1000), xtol=0.1):
"""
Method to compute the number of exposures from S/N and magnitude
Parameters
----------
filt : str
Name of Roman WFI filter
sn : float
Required S/N
mag : float
AB Magnitude of source
bracket : tuple, default (1, 1000)
Range of exposures to test
xtold: float, default 0.1
Target tolerance for minimizer
Returns
-------
nexp : float
Optimal number of exposures for specified S/N and magnitude
report: dict
Pandeia dictionary with optimal parameters
exptime: float
Exposure time for optimal observation
"""
# Setup default Roman observation
calc = build_default_calc('roman', 'wfi', 'imaging')
# Modify defaults to place a source with an AB magnitude
calc['scene'][0]['spectrum']['normalization']['norm_flux'] = mag
calc['scene'][0]['spectrum']['normalization']['norm_fluxunit'] = 'abmag'
calc['scene'][0]['spectrum']['normalization']['norm_waveunit'] = 'um'
# Set filter
calc['configuration']['instrument']['filter'] = filt
# Check that the minimum of 1 exposure has a S/N lower than requested,
# otherwise there is no sense in attempting to minimize nexp.
calc['configuration']['detector']['nexp'] = 1
report = perform_calculation(calc)
if report['scalar']['sn'] > sn:
nexp = 1
else:
res = minimize_scalar(_nexp2sn_,
bracket=bracket,
bounds=bracket,
args=(calc, sn),
method='bounded',
options={'xatol':xtol})
# Take the optimization result and set it to nexp
# 'x' is the solution array in the optimization result object
# For more details on the minimize_scalar function, refer to https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize_scalar.html
nexp = int(res['x'])
calc['configuration']['detector']['nexp'] = nexp
report = perform_calculation(calc)
# This generally returns a S/N less than the required amount.
# Let's ensure that we get *AT LEAST* the required S/N for two reasons:
# 1) Better to err on the side of caution
# 2) Make code consistent with the above if-clause
if report['scalar']['sn'] < sn:
nexp += 1
# Re-calculate
calc['configuration']['detector']['nexp'] = nexp
report = perform_calculation(calc)
exptime = report['scalar']['total_exposure_time']
return nexp, report, exptime
# Desired magnitude and S/N
MAG = 26.
SN = 20.
FILTER = 'f129'
# Run minimizer function
nexp, etc, exptime = compute_nexp(FILTER, SN, MAG)
# Print reported numbers
print(f'Number of exposures: {nexp}')
print(f'Actual S/N reached: {etc["scalar"]["sn"]:.2f}')
print(f'Exposure time: {exptime:.2f}')
Warnings Issued by Running this Code
This step may generate a WARNING from synphot that the spectrum is extrapolated, which may be ignored. There may be an additional WARNING that the signal-to-noise for a single exposure is larger than what was requested, which may also be ignored.
Result from the Example
This calculation should output
48
exposures, a signal-to-noise reached of
20.03
, and an exposure time of
6679.14
seconds.
Since nexp must be an integer, the signal-to-noise returned will be at least the required value, but could be a higher value. The value of the returned signal-to-noise can be significantly higher than the requested value when the inferred nexp is small.
Example 4: Modifying the Spectral Energy Distribution (SED)
A scientific goal may require specifying something more complex than a flat SED (as assumed in other examples). In this instance, we assume that the SED is determined by a star selected from a grid of Phoenix models.
Description of Code Snippet
The code will simulate a
mag
= 25 AB magnitude source in
FILTER
= 'f129' with
nexp
= 3 (using the default MA table). A step is added, however, to modify the SED shape from the default flat spectrum; the user sets the
sed_type
to 'phoenix' and then specifies the
key
as 'a0v', which is a star of type A0V (i.e., an A0 main-sequence star). A summary of these options is given in Pre-Configured Spectral Energy Distributions section of the article Overview of Pandeia.
Python Implementation
from pandeia.engine.perform_calculation import perform_calculation
from pandeia.engine.calc_utils import build_default_calc
# Get Default Parameters
calc = build_default_calc('roman', 'wfi', 'imaging')
# Set the global variable for the filter name (change to any valid filter)
FILTER = 'f129'
# Modify defaults to simulate a 25th AB magnitude source
MAG = 25
calc['scene'][0]['spectrum']['normalization']['norm_flux'] = MAG
calc['scene'][0]['spectrum']['normalization']['norm_fluxunit'] = 'abmag'
# Set number of exposures and filter
NEXP = 3
calc['configuration']['detector']['nexp'] = NEXP
calc['configuration']['instrument']['filter'] = FILTER
# Modify SED shape
calc['scene'][0]['spectrum']['sed']['sed_type'] = 'phoenix'
calc['scene'][0]['spectrum']['sed']['key'] = 'a0v'
# Run calculation and return signal-to-noise ratio
report = perform_calculation(calc)
snr = report['scalar']['sn']
print(f'Estimated S/N: {snr:.2f}')
Result from the Example
This calculation should output an estimated signal-to-noise ratio of
16.12
.
Example 5: Modifying the Background
Pandeia defines the background using 2 values in the configuration dictionary: background and background_level. Combination of these two values determines whether a pre-computed background data (the canned background) or a user-supplied background data (custom background) is used. As of
Pandeia
version 2025.9, the default values used by the engine are hlwas-medium_field1 for the background and medium for the background_level (see below).
Canned Backgrounds
The engine is shipped with a set of pre-computed backgrounds that are generated by the Roman Backgrounds Tool (RBT) at various locations within the three Core Community Surveys (CCS). Please refer to the "Canned backgrounds in the Pandeia engine and the Roman Interactive Sensitivity Tool (RIST)" section in Assessing Background Levels for WFI Observations for the details. The current valid values for the background configuration dictionaries are listed below:
calc['background']hlwas-medium_field1hlwas-medium_field2hlwas-wide_field1hlwas-wide_field2hlwas-wide_field3hlwas-wide_field4hltdsgbtds_mid_5stripe
calc['background_level']lowmedium (for hltds, only this background level is available)high
In the code below, we show how to set up the imaging observation and change the background and level to hltds and medium , respectively.
Python Implementation
from pandeia.engine.perform_calculation import perform_calculation
from pandeia.engine.calc_utils import build_default_calc
# Get Default Parameters
calc = build_default_calc('roman', 'wfi', 'imaging')
# Update the background model to a different set
calc['background'] = 'hltds'
calc['background_level'] = 'medium'
Custom Backgrounds
If you are interested in exploring the observing setup with background other than the pre-computed canned backgrounds, you can either use the RBT (available on the Roman Research Nexus) to generate the background or download a calculation from the web ETC (available as a tar file in the Downloads tab under the Reports pane) where you will find 'background.fits' file to import from the engine. The custom background needs to be defined as a list containing the wavelength and flux arrays, denoted by square brackets, and set to the background. The background_level key will be ignored by the engine. The code below shows how to define the custom background in the engine using the
background.fits
file from the web ETC.
from pandeia.engine.perform_calculation import perform_calculation
from pandeia.engine.calc_utils import build_default_calc
from astropy.io import fits
# Get Default Parameters
calc = build_default_calc('roman', 'wfi', 'imaging')
# Read the background.fits file from the web ETC
bg_data = fits.open('backgrounds.fits')
bg_wvl = bg_data[1].data['wavelength']
bg_flux = bg_data[1].data['background']
# Define the custom background
custom_bg = [bg_wvl, bg_flux]
calc['background'] = custom_bg
# Perform the calculation
report = perform_calculation(calc)
Here, we show how to generate a custom background using the RBT and supply it to the engine. The code will simulate a background spectrum at a specific coordinate (RA = 34.5656 and Dec = -52.6140, both in decimal degrees) and wavelength (0.6291 microns). Then you can choose the specific observable day to supply to the engine and save it if needed. Note that RBT considers 366 calendar days in a year with Day 0 corresponding to January 1. In this example, we will look at the first observable day in the year.
from pandeia.engine.perform_calculation import perform_calculation
from pandeia.engine.calc_utils import build_default_calc
from roman_backgrounds import rbt
from astropy import units as u
from astropy.coordinates import SkyCoord
# Define the coordinates
ra = 34.5656 # in degrees
dec = -52.6140 # in degrees
wavelength = 0.6291 # in microns
threshold = 1.5 # A cut off value above the minimum background, to calculate number of good days.
# Create the RBT instance
bg = rbt.background(ra, dec, wavelength, thresh=threshold)
# Look at the all available observable days
good_days = bg.bkg_data['calendar']
# Define the day to look at the background spectrum for the target
this_day = good_days[0]
# Define the background spectrum
bg_wvl = bg.bkg_data['wave_array']
bg_flux = bg.bkg_data['total_bg'][this_day]
# Get Default Parameters
calc = build_default_calc('roman', 'wfi', 'imaging')
# Set the background configuration dictionary to the RBT-supplied background spectrum
calc['background'] = [bg_wvl, bg_flux]
# Save the background spectrum to a file
bg.write_background(thisday=this_day, background_file = your_filename)
# Perform the calculation
report = perform_calculation(calc)
Example 6: Importing input.json from the Web ETC into the Pandeia engine
For calculations that completed successfully, the Web ETC provides a downloadable tar file in the Downloads tab in the Reports pane that contains useful products for additional offline analysis. One of the files contains the input configurations for the calculation in a json file (input.json). In this example, we show how to read in the input.json file in Pandeia and re-run the calculation again.
from pandeia.engine import perform_calculation
import json
from textwrap import wrap
calculation = "input.json"
# load the JSON file
with open(calculation,'r') as calcfile:
calc = json.load(calcfile)
# run the calculation
result = perform_calculation.perform_calculation(calc)
# Extract the pretty-printed web report
report = result['web_report']
# Formatted output
print("-"*30 + "\033[1m RESULTS \033[0m" + "-"*30)
for category in report:
# ANSI Pretty-print the report categories and apply word wrapping
print("\033[1m" + category["category"] + "\033[0m")
print('-'*len(category["category"]))
for item in category["items"]:
if "value" in item:
namelist = wrap(item["name"], width=36, subsequent_indent=" ")
if "indent" in item and item["indent"]:
namelist[-1] = " " + namelist[-1]
for i in range(len(namelist)-1):
print(f"{namelist[i]:<36}")
print(f"{namelist[-1]:<36} {item['value']:>16} {item['unit']:<10}")
else:
print(f"{item['name']}")
print()
print("-"*30 + "\033[1m WARNINGS \033[0m" + "-"*30)
for x in result['warnings']:
# ANSI Pretty-print the warnings and apply word wrapping
warning_display = wrap(result["warnings"][x], width=45, subsequent_indent=" ")
print(f"{x:<25}: {warning_display[0]}")
if len(warning_display) > 1:
for idx in range(len(warning_display) -1):
print(f"{" "*25} {warning_display[idx+1]}")
print("-"*70)
Result from the Example
The code should output the following texts in the terminal:
------------------------------ RESULTS ------------------------------
Results
-------
Extracted Signal-to-Noise Ratio 0.00
Extracted Flux 19845.28 e-/s
Standard Deviation of Extracted Flux nan e-/s
Brightest Pixel Rate 12187.61 e-/s
Maximum Fraction of Saturation 7.71
Maximum Number of Resultants Before
Saturation 2 resultants
Instrument and Detector
-----------------------
Instrument Detector/Filter/Disperser wfi01, f062, n/a
Integration Duration 63.25 s
Single Exposure Time 63.25 s
Fraction of Time Spent Collecting
Flux 1.00
Effective Exposure Time 60.09 s
Science Time 60.09 s
Total time collecting photons 63.25 s
Saturation Time 63.25 s
Extraction Strategy Settings
----------------------------
Radius of Extraction Aperture 0.40 arcsec
Area of Extraction Aperture 41.54 pixels
Effective Wavelength 0.65 microns
Extraction Aperture Position (0.00, 0.00) arcsec
Background
----------
Input Background Surface Brightness 0.18 MJy/sr
Area of Background Measurement 62.31 pixels
Total Sky Background Flux in
Background Aperture 23.91 e-/s
Total Flux in Background Aperture 432.61 e-/s
Fraction of Total Background due to
Signal from Scene 0.94
Average Number of Cosmic Ray Events
per Pixel per Ramp 1.55e-03 events/pixel/read
------------------------------ WARNINGS ------------------------------
full_saturated : Full saturation: There are 1 pixels
saturated before the third resultant. These
pixels cannot be recovered.
partial_saturated : Partial saturation: There are 1 pixels
saturated at the end of a ramp. Partial
ramps may still be used in some cases.
----------------------------------------------------------------------
More Information and Options to Explore
Further information about Pandeia is available on the Pandeia for JWST Documentation on JDox, including detailed breakdowns of all of the allowable keywords and pre-configured options.
For additional questions not answered in this article, please contact the Roman Help Desk.