Pandeia Tutorials

This article provides several examples on how to use Pandeia for Roman.




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_8with 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

SNR Calculation Example
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 DescriptionParameter in Code ExampleValue 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 

Code to Estimate the Limiting Magnitude
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 DescriptionParameter in Code ExampleValue 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_field1
    • hlwas-medium_field2
    • hlwas-wide_field1
    • hlwas-wide_field2
    • hlwas-wide_field3
    • hlwas-wide_field4
    • hltds
    • gbtds_mid_5stripe
  • calc['background_level']
    • low
    • medium (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.




Latest Update

 

Minor edits to improve accuracy.
Publication

 

Initial publication of the article.