Skip to main content

Set Up a Launch Script for TipTop and Display Results

πŸ“ Run a TipTop simulation and display PSFs​

Simplest way​

As explained in the Quickstart section, to run a simulation with TipTop, you need:

➑️ a launch script which:

  • Loads the simulation parameters from a .ini file (e.g., minimalPar.ini)

  • Initializes the necessary modules

  • Starts the simulation using the overallSimulation function

The simplest file to launch a simulation looks like this (TIPTOP-EXAMPLE.py, available in the examples/ folder of our GitHub repository):

from tiptop.tiptop import *

overallSimulation("./", "minimalPar", './', 'test')

where:

  • The first and second arguments of overallSimulation are the path to the folder containing the input .ini file and the name of that file (without the extension).
  • The third and fourth arguments specify where to save the output results (in .fits format) and the name of the resulting .fits file.

A detailed documentation on the overallSimulation function is available below.

More complete launch script​

Below is an example of a more advanced launch script (suitable for a single science source). It runs a simulation for the ERIS instrument and extracts key outputs, including PSFs and performance metrics (e.g., Strehl Ratio (SR), Full Width at Half Maximum (FWHM)) from the output FITS file (see Simulation Output below). It also generates log-scaled intensity plots of the AO PSF, diffraction limited PSF, and seeing limited PSF, as well as a log-scaled radial profile plot.
βœ… You can adapt this script by changing the input/output paths and filenames to match your configuration.
The full example script is available for download hereπŸ“₯.

Example run and display script for a single science source: TIPTOP_RUN_DISPLAY.py
"""
Created on Mon Jun 23 10:58:33 2025
Run a TIPTOP simulation and display PSFs

@author: astro-tiptop-services
"""

#%% Import necessary libraries
from tiptop.tiptop import overallSimulation
from astropy.io import fits
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
import numpy as np
import os

#%% πŸ’‘Set working directory to TIPTOP root (optional)
import tiptop
tiptop_path = os.path.dirname(tiptop.__file__)
print(f"Directory of 'tiptop' is: {tiptop_path}")
os.chdir(tiptop_path)

#%% Define input and output paths and filenames
#πŸ’‘ Adapt these paths to your setup
# path_in : Path to the folder containing your .ini parameter file
# path_out : Path where output files will be saved
path_in = "perfTest/" # relative to TIPTOP_ROOT (because of chdir)
path_out = "/my_path_to_SimOutputs/" # Use an absolute path if you want outputs to be saved outside the TIPTOP root

file_in = "ERIS_SCAO_NGS" # Name of the input parameter file (without extension)
file_out = "ERIS_SCAO_NGS" # Name to use for the output files

#%% Run the TIPTOP simulation
overallSimulation(path_in, file_in,
path_out, file_out)


#%% Open the resulting FITS file and extract data
hdul = fits.open(path_out + file_out + '.fits')
hdul.info()
psf_ao = hdul[1].data[0,...] # AO-corrected PSF
psf_turb = hdul[2].data # Seeing-limited PSF
psf_dl = hdul[3].data # Diffraction-limited PSF
# Load PSF profiles (can be in HDU 4 or 5 depending on save options)
header4 = hdul[4].header
profiles = hdul[4].data if header4.get('CONTENT') == 'Final PSFs profiles' else hdul[5].data

psf_header = hdul[1].header # FITS header for metadata

hdul.close()

# Extract useful parameters
wvl = float(psf_header.get("WL_NM")) # Wavelength in nm
pix_mas = float(psf_header.get("PIX_MAS")) # Pixel scale in milliarcseconds
sr = psf_header.get(f"{"SR"}{0:04d}") # Strehl ratio
fwhm = psf_header.get(f"{"FWHM"}{0:04d}") # FWHM in milliarcseconds

# Print key metrics
print("Pixel scale [mas]:", pix_mas)
print("Strehl Ratio:", sr)
print("FWHMs [mas]:", fwhm)

#%% Normalize the PSFs so total flux = 1
psf_ao /= np.sum(psf_ao)
psf_dl /= np.sum(psf_dl)
psf_turb /= np.sum(psf_turb)

#%% Create axis in arcseconds
nx = psf_ao.shape[0]
axis = np.linspace(-nx//2, nx//2, nx) * pix_mas * 1e-3

#%% Plot the PSFs
# Compute dynamic normalization based on the AO PSF
psf_max = psf_ao.max()
vmax = psf_max
vmin = psf_max * 1e-6 # Adjust dynamic range: show down to 1 millionth of max
norm = LogNorm(vmin=vmin, vmax=vmax) # Set log scale for display

plt.figure(1, figsize=(20,5))
plt.suptitle(r'$\lambda_{\mathrm{science}} = %d$ nm' % int(wvl), y=1)
plt.subplots_adjust(top=0.85)

def plot_psf(psf, title, position):
"""Plot a PSF in a given subplot position."""
plt.subplot(1, 3, position)
plt.imshow(psf, norm=norm, cmap='Spectral_r',
extent=[axis[0], axis[-1], axis[0], axis[-1]])
plt.title(title, pad=10)
plt.xlabel('[arcsec]')
if position == 1:
plt.ylabel('[arcsec]', labelpad=10)
else:
plt.ylabel('')
plt.colorbar(fraction=0.046)

plot_psf(psf_ao, f'AO (SR={sr*100:.1f}%, FWHM={fwhm:.1f}mas)', 1)
plot_psf(psf_dl, 'Diffraction', 2)
plot_psf(psf_turb, 'Open loop', 3)

#%% Plot the radial profile
radii = profiles[0,0,:]
p_norm = profiles[1,0,:] / np.max(profiles[1,0,:])

plt.figure(figsize=(8, 6))
plt.plot(radii, p_norm, label='AO profile')
plt.xlabel('Radial distance (mas)')
plt.ylabel('PSF profile norm. to max')
plt.xscale("log")
plt.yscale("log")
plt.title(f'Radial profile - AO corrected PSF - @{int(wvl)} nm')
plt.legend()
plt.grid(True, which='both', linestyle='--', linewidth=0.5, color='gray', alpha=0.7)

More complete display script​

Below is an example of a more complete display script (suitable for multiple science sources). It loads the generated FITS file obtained after running TipTop β€” in this case, for the MORFEO instrument. It extracts the PSFs and performance metrics such as Strehl Ratio (SR) and Full Width at Half Maximum (FWHM), for each science source. It also recomputes SR and FWHM from the PSF data to enable consistency checks with the stored header values.
This script then displays the AO PSFs in a log-scaled grid layout, annotated with zenith/azimuth coordinates and performance metrics, allowing for quick visual assessment of PSF quality across the field. In addition, it plots the normalized radial PSF profiles in log-log scale for all sources.
βœ… You can adapt this script to your own simulation results by modifying the file names and paths.
The full example script is available for download hereπŸ“₯.

Example display script for multiple science sources: TIPTOP_DISPLAY.py
"""
Created on Wed Jul 09 10:50:35 2025
Display multiple AO PSFs in a grid

@author: astro-tiptop-services
"""

#%% =============================================================================
# Import necessary libraries
from tiptop.tiptop import *
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import os

#%% πŸ’‘Set working directory to TIPTOP root (optional)
import tiptop
tiptop_path = os.path.dirname(tiptop.__file__)
print(f"Directory of 'tiptop' is: {tiptop_path}")
os.chdir(tiptop_path)

#%% =============================================================================
# Define input and output paths
#πŸ’‘ Adapt these paths to your environment
# path_in : Path to the folder containing your .ini parameter file
# path_out : Path where output files will be saved
path_in = "perfTest/" # relative to TIPTOP_ROOT (because of chdir)
path_out = "/my_path_to_SimOutputs/" # Use an absolute path if you want outputs to be saved outside the TIPTOP root

inTag = "MORFEO" # Name of the input parameter file (without extension)
outTag = "MORFEO" # Name of the output FITS file

inputFile = f"{path_in}{inTag}.ini"
outputFile = f"{path_out}{outTag}.fits"

#%% =============================================================================
# Load AO system and compute key parameters
ao = aoSystem(inputFile)
wvl = ao.src.wvl[0] # Science wavelength
rad2mas = 3600 * 180 * 1000 / np.pi # Convert radians to milliarcseconds
pixel_scale_mas = ao.cam.psInMas
sampRef = wvl * rad2mas / (pixel_scale_mas*2*ao.tel.R) # Sampling reference

#%% =============================================================================
# Load FITS file and extract PSFs and metadata
hdul = fits.open(path_out + outTag + '.fits')

# Extract header and map dictionary
header = hdul[0].header
map_dictionary = hdr2map(header)

# Extract SR and FWHM values from header
psf = hdul[1].data
psf_header = hdul[1].header
n_psf = len(psf)

# Extract SR and FWHM values from the header of the PSF HDU
sr_list = [psf_header[f"SR{str(i).zfill(4)}"] for i in range(n_psf)]
fwhm_list = [psf_header[f"FWHM{str(i).zfill(4)}"] for i in range(n_psf)]

# Load PSF profiles (can be in HDU 4 or 5 depending on save options)
header4 = hdul[4].header
profile = hdul[4].data if header4.get('CONTENT') == 'Final PSFs profiles' else hdul[5].data

#%% =============================================================================
# Compare header SR/FWHM values with computed values
print('Compare SR and FWHM from header with computed values:')
for i in range(n_psf):
zenith = map_dictionary['sources_science']['Zenith'][i]
azimuth= map_dictionary['sources_science']['Azimuth'][i]
print(f'Zenith: {zenith}, Azimuth: {azimuth}')
sr_comp = FourierUtils.getStrehl(psf[i], ao.tel.pupil, sampRef, method='otf')
fwhm_comp = FourierUtils.getFWHM(psf[i], ao.cam.psInMas, nargout=1)
print(f' SR (computed) : {sr_comp:.5f}')
print(f' SR (from header): {sr_list[i]:.5f}')
print(f' FWHM (computed) : {fwhm_comp:.5f}')
print(f' FWHM (from hdr) : {fwhm_list[i]:.5f}')

#%% =============================================================================
# Display PSFs in a square grid
# => grid is made by same number of rows and columns
max_display = int(np.floor(np.sqrt(n_psf))) ** 2
n_rows = int(np.sqrt(max_display))
n_cols = n_rows

crop_size = 4
nx = FourierUtils.cropSupport(psf[0], crop_size).shape[0]
axis = np.linspace(-nx//2, nx//2, nx) * pixel_scale_mas * 1e-3

fig, axs = plt.subplots(n_rows, n_cols, figsize=(12, 12), constrained_layout=True, squeeze=False)
for i in range(max_display):
ax = axs.flat[i]
zenith = float(map_dictionary['sources_science']['Zenith'][i])
azimuth = float(map_dictionary['sources_science']['Azimuth'][i])
img = ax.imshow(FourierUtils.cropSupport(psf[i], crop_size),
cmap='Spectral_r',
extent=[axis[0], axis[-1], axis[0], axis[-1]],
norm=mcolors.LogNorm(vmin=np.max(psf)*1e-4,
vmax=np.max(psf)))
ax.text(0.05, 0.05,
f'Distance:{zenith:.0f}", Angle:{azimuth:.0f}Β°\nSR:{sr_list[i]*100:.1f}%, FWHM:{fwhm_list[i]:.1f} mas',
color='white', fontsize=9, transform=ax.transAxes,
bbox=dict(facecolor='black', alpha=0.5, lw=0))
if i % n_cols == 0:
ax.set_ylabel('Arcsec', fontsize=10)
else:
ax.set_yticks([])
if i // n_cols == n_rows - 1:
ax.set_xlabel('Arcsec', fontsize=10)
else:
ax.set_xticks([])
# Set tick parameters
ax.tick_params(labelsize=10, color='white')
# Add a small colorbar to the right of each subplot
cbar = plt.colorbar(img, ax=ax, fraction=0.046, pad=0.01)
cbar.ax.tick_params(labelsize=6)

#%% =============================================================================
# Plot normalized PSF profiles in log-log scale
plt.figure(figsize=(10, 8))
colors = plt.cm.tab20(np.linspace(0, 1, n_psf))

for i in range(n_psf):
zenith = float(map_dictionary['sources_science']['Zenith'][i])
azimuth = float(map_dictionary['sources_science']['Azimuth'][i])
y_norm = profile[1,i,:] / np.max(profile[1,i,:])
x = profile[0,i,:]
plt.plot(x, y_norm, label=f'Zen: {zenith:.1f}", Azi: {azimuth:.1f}Β°',
color=colors[i], linewidth=1.5)
plt.xscale('log')
plt.yscale('log')
plt.xlabel('Distance [mas]', fontsize=12)
plt.ylabel('PSF profile norm. to max', fontsize=12)
plt.title('PSF Profiles', fontsize=14)
plt.grid(True, which='both', linestyle='--', linewidth=0.5, color='gray', alpha=0.7)
plt.legend(loc='lower left', fontsize=10, ncol=2)
plt.tight_layout()
plt.show()

✏️Note: Results for the different AO instruments presented here were obtained by running the previous two scripts with the corresponding provided .ini files.

πŸ“ Simulation Output​

The output of a TipTop simulation consists of Point Spread Functions (PSFs) computed using the parameters specified in your .ini file.

If the doPlot parameter of the overallSimulation function is set to True, the following PSFs will be displayed after the simulation runs:

  • The AO-corrected PSF(s)
  • The seeing-limited PSF
  • The diffraction-limited PSF

πŸ’Ύ Saving to a FITS file​

These PSFs are also saved in a .fits file for further analysis and post-processing.

FITS File Structure & Contents

The FITS file contains multiple HDUs (Header/Data Units), each storing different types of data related to the PSFs generated during the simulation. The content is organized as follows:

  • HDU 0 – PRIMARY
    Contains metadata about the simulation, the instrument, and observational parameters. It does not contain image data but provides essential contextual information.
  • HDU 1 – AO-Corrected PSF
    Stores the cube of AO-corrected PSFs as a multi-dimensional image array with dimensions (FieldOfView, FieldOfView, Nsrc, Nwvl), where FieldOfView corresponds to the camera's field of view in pixels as defined in the [sensor_science] section of your .ini file, Nsrc is the number of science sources, and Nwvl is the number of wavelengths specified in the Wavelength parameter of the [sources_science] section.
  • HDU 2 – Seeing-Limited PSF
    Contains the seeing-limited (open-loop) PSF in a 2D image array of size (FieldOfView, FieldOfView).
  • HDU 3 – Diffraction-Limited PSF
    Contains the diffraction-limited PSF, also stored as a 2D image array of size (FieldOfView, FieldOfView).
  • HDU 4 – PSDs (if savePSDs=True)
    Contains the Power Spectral Density (PSD), stored as a 3D array.
  • HDU 4 or 5 – Final PSFs Radial Profiles
    Contains the 1D radial profiles of the PSFs, stored as a 3D array.
    ✏️Note: The HDU number depends on whether the PSDs are saved.

Here is an example FITS structure produced with the minimalPar.ini configuration:

No.    Name      Ver    Type      Cards   Dimensions   Format
0 PRIMARY 1 PrimaryHDU 84 ()
1 1 ImageHDU 21 (256, 256, 1) float64 #AO-corrected PSFs
2 1 ImageHDU 10 (256, 256) float64 #Open-loop PSF
3 1 ImageHDU 10 (256, 256) float64 #Diffraction limited PSF
4 1 ImageHDU 11 (256, 256, 1) float64 #High Order PSD (if saved)
5 1 ImageHDU 11 (128, 1, 2) float64 #PSFs profiles

✏️Note: Results for the different AO instruments are presented here.
By default, the FITS file header includes the SR and FWHM values for each PSF (the addSrAndFwhm parameter of the overallSimulation function is set to True by default).
To retrieve and display the SR, FWHM and encircled energy metrics directly in your terminal, set the returnMetrics option to True (see the documentation on the overallSimulation function below).
⚠️ If returnMetrics=True or returnRes=True, the FITS file is not saved.

πŸ“ In-memory workflow β€” skip output FITS file​

If you want to access the output PSFs and metrics as Python objects (e.g., for easier integration in your software) and avoid writing and reading the result FITS file, you can use baseSimulation directly: instantiate β†’ doOverallSimulation() β†’ computeMetrics().
βœ”οΈ Use baseSimulation (see documentation below).
❌ Instead of calling overallSimulation.

Minimal example: baseSimulation β†’ doOverallSimulation() β†’ computeMetrics()
from tiptop.tiptop import baseSimulation
from tiptop.tiptopUtils import cpuArray

path_in = "/my_path_to_SimInputs/"
path_out = "/my_path_to_SimOutputs/"
file_in = "IniName"
file_out = file_in

# 1) Instantiate the simulation
sim = baseSimulation(path_in, file_in, path_out, file_out)

# 2) Run the full pipeline
sim.doOverallSimulation()

# 3) Compute metrics (fills sim.sr, sim.fwhm, sim.ee)
sim.computeMetrics()

# 4) Use results directly, in memory
psf_ao = sim.cubeResultsArray # AO-corrected PSF(s) β€” numpy array
psf_ol = sim.psf_ol_array # Seeing-limited PSF β€” numpy array
psf_dl = sim.psf_dl_array # Diffraction-limited PSF β€” numpy array
profiles = sim.psf1d_data # PSF radial profiles β€” numpy array
psd = cpuArray(sim.PSD) # PSD (may be on GPU if CuPy is used)

# 5) Extract useful parameters
wvl = sim.wvl # Wavelength(s) in m
pix_mas = sim.psInMas # Pixel scale in mas
sr = sim.sr # Strehl ratio
fwhm = sim.fwhm # FWHM in mas
ee = sim.ee # Encircled energy

print("SR:", sr)
print("FWHM [mas]:", fwhm)
print("EE:", ee)

# To also write a FITS file:
# sim.saveResults()

✏️Note: psf_ol_array, psf_dl_array, and psf1d_data are already NumPy arrays β€” no cpuArray conversion needed. Only sim.PSD may reside on the GPU when CuPy is enabled.

Parameter sweep without re-reading the .ini file​

Since v1.5.1, baseSimulation passes its internal config dictionary (my_data_map) directly to P3 and MASTSEL on each run. This means you can modify any parameter in memory between calls β€” no file I/O on each iteration. Before v1.5.1, fourierModel re-read the .ini file on every call, so a clean in-memory sweep was not possible.

Example: telescope zenith angle sweep using my_data_map
import copy
from tiptop.tiptop import baseSimulation

path_in = "/my_path_to_SimInputs/"
path_out = "/my_path_to_SimOutputs/"
file_in = "IniName"

# 1) Load config from disk once
sim = baseSimulation(path_in, file_in, path_out, file_in)
base_config = copy.deepcopy(sim.my_data_map)

# Sweep telescope ZenithAngle (affects airmass and atmospheric layer scaling)
zenith_angles = [0, 15, 30, 45, 60] # degrees from zenith
results = {}

for za in zenith_angles:
# 2) Modify config dict in memory β€” no file I/O
sim.my_data_map = copy.deepcopy(base_config)
sim.my_data_map['telescope']['ZenithAngle'] = float(za)

# 3) Run β€” _prepare_static_PSF_state always re-runs when astIndex=None (default)
sim.doOverallSimulation()
sim.computeMetrics()

results[za] = {
'sr': sim.sr[0],
'fwhm': sim.fwhm[0],
'psf': sim.cubeResultsArray.copy(),
}
print(f"ZenithAngle = {za:2d}Β° β†’ SR = {sim.sr[0]:.3f}, FWHM = {sim.fwhm[0]:.1f} mas")

The same pattern works for any parameter in my_data_map, for example sim.my_data_map['atmosphere']['Seeing'] or sim.my_data_map['sources_HO']['NumberPhotons'].

Class baseSimulation β€” constructor parameters​

sim = baseSimulation(path2param, parametersFile, outputDir, outputFile, doConvolve=True, doPlot=False, 
addSrAndFwhm=True, verbose=False, getHoErrorBreakDown=False,
savePSDs=False, ensquaredEnergy=False, eeRadiusInMas=50)
Parameters:
  • path2param (str, required) β€” Path to the folder containing the parameter file.
  • parametersFile (str, required) β€” Name of the parameter file without the extension.
  • outputDir (str, required) β€” Path to the folder where output files will be written (used when sim.saveResults() is called).
  • outputFile (str, required) β€” Prefix for output files (.fits, .json) when saving.
  • doConvolve (bool, optional, default: True) β€” If True, uses the natural convolution operation.
  • doPlot (bool, optional, default: False) β€” If True, displays the PSFs after the simulation.
  • addSrAndFwhm (bool, optional, default: True) β€” Adds SR, FWHM, and EE within eeRadiusInMas to the FITS header for each PSF.
  • verbose (bool, optional, default: False) β€” If True, prints all messages.
  • getHoErrorBreakDown (bool, optional, default: False) β€” If True, computes the HO error breakdown.
  • savePSDs (bool, optional, default: False) β€” If True, saves the PSD in the output FITS file.
  • ensquaredEnergy (bool, optional, default: False) β€” If True, computes ensquared energy instead of encircled energy.
  • eeRadiusInMas (float, optional, default: 50) β€” Radius in mas for the encircled energy computation (if ensquaredEnergy=True, this is half the side of the square).

πŸ“ OverallSimulation function documentation​

tiptop.overallSimulation runs a complete TipTop simulation based on an input parameter file. The function accepts several optional arguments to enable or disable specific features and select desired outputs.

Parameters:
  • path2param (str, required) β€” Path to the folder containing the parameter file.

  • paramFileName (str, required) β€” Name of the parameter file without the extension.

  • outputDir (str, required) β€” Path to the folder in which to write the output.

  • outputFile (str, required) β€” Prefix for output files (.fits, .json) when saving.

  • doConvolve (bool, optional, default: True) β€” If True, uses the natural convolution operation.

  • doPlot (bool, optional, default: False) β€” If True, displays the result in Python after the simulation.

  • verbose (bool, optional, default: False) β€” If True, prints all messages.

  • returnRes (bool, optional, default: False) β€” If True, returns the HO residual (and LO residual if applicable) in nm RMS. No FITS file is written.

  • returnMetrics (bool, optional, default: False) β€” If True, returns Strehl Ratio, FWHM, and encircled energy within eeRadiusInMas. No FITS file is written.

  • addSrAndFwhm (bool, optional, default: True) β€” Adds SR, FWHM, and EE within eeRadiusInMas to the FITS header for each PSF.

  • getHoErrorBreakDown (bool, optional, default: False) β€” If True, computes HO error breakdown (verbose must also be True).

  • ensquaredEnergy (bool, optional, default: False) β€” If True, computes ensquared energy instead of encircled energy.

  • eeRadiusInMas (float, optional, default: 50) β€” Radius in mas for the encircled energy computation (if ensquaredEnergy=True, this is half the side of the square).

  • savePSDs (bool, optional, default: False) β€” If True, saves the PSD in the output FITS file.

  • saveJson (bool, optional, default: False) β€” If True, saves the PSF radial profile in a JSON file.

  • gpuIndex (int, optional, default: 0) β€” Target GPU device index.

⚠️ Note: If returnMetrics=True or returnRes=True, no FITS file is written.