SCAO Basic Tutorial: Complete Walkthrough
This comprehensive tutorial guides you through creating and running a basic Single Conjugate Adaptive Optics (SCAO) simulation using SPECULA. A more advanced SCAO simulation tutorial is available in the SCAO Tutorial: Complete Walkthrough.
What you’ll learn:
Setting up a minimal SCAO system configuration with a Pyramid WFS
Running calibration and closed-loop phases
Using Zernike modes and a single atmospheric layer
Setting the push-pull amplitude directly in the YAML file
Prerequisites:
SPECULA installed and working (see Installation)
Basic understanding of adaptive optics concepts
Python and YAML familiarity
Note
About atmospheric and source parameters: All atmospheric parameters (seeing, layer heights) and source heights are defined at zenith. See Simulation Parameters Reference for details on units, conventions, and the zenith angle treatment.
Tutorial Overview
We’ll simulate a simple SCAO system with:
Circular pupil (VLT-like, 8m class)
1 atmospheric layer
Pyramid wavefront sensor
Zernike modal DM
1 kHz control loop with integrator controller
R-band natural guide star (magnitude 8)
Part 1: System Configuration
In this part, we will define the system configuration in a YAML file. SPECULA uses a modular, object-oriented architecture where each component of the simulation is defined as an object in a YAML configuration file. In the YAML file all the parameters of the simulation are defined, including the objects and their connections.
Understanding the YAML Simulation File
The core of a SPECULA simulation is defined in a YAML file, which describes the system as a set of interconnected objects. Each section in the YAML file corresponds to a specific object (or “block”) in the simulation, such as the atmosphere, the wavefront sensor, the deformable mirror, or the control law.
How it works:
Object Instantiation: Each section’s
classparameter specifies which Python class will be instantiated for that object. All other parameters in the section are passed as arguments to the class constructor. This is handled automatically by the simulation engine (simul.py).Inputs and Outputs: The
inputsandoutputsfields define how objects are connected.The value of each input is a reference to the output of another object, using the syntax
object_name.output_name.This mechanism defines the data flow and the dependency graph of the simulation.
Simulation Graph: The entire simulation is built as a directed graph, where nodes are objects and edges are the connections defined by the
inputs. The simulation engine parses the YAML, instantiates all objects, and connects them according to this graph. See Simulation diagrams for visual examples of simulation graphs automatically generated by SPECULA.Special Notes:
In the propagation block (
prop), the inputcommon_layer_listoften includesdm.out_layer:-1. This is a SPECULA convention to handle feedback in the simulation loop. The-1index is used to resolve an ambiguity in closed-loop simulations: not all the elements of the loop can perform their operations at the same time, at least one must happen at the following time step. In an Adaptive Optics context the DM output is computed during the current time step, but it is applied in the next time step.
Example:
prop:
class: 'AtmoPropagation'
simul_params_ref: 'main'
source_dict_ref: ['on_axis_source']
inputs:
atmo_layer_list: ['atmo.layer_list']
common_layer_list: ['pupilstop', 'dm.out_layer:-1']
outputs: ['out_on_axis_source_ef']
In this example:
The
propobject is an instance of theAtmoPropagationclass.It receives as input the atmospheric layers from
atmo.layer_list, the pupil geometry frompupilstop, and the most recent DM surface fromdm.out_layer:-1.It produces an output
out_on_axis_source_ef, which can be used as input by other objects (e.g., the Pyramid WFS).
This modular and explicit approach makes it easy to customize, extend, and debug your simulation setup.
Create the main YAML configuration file
Create a YAML configuration file, for example params_scao_pyr_basic.yml:
# main section with simulation parameters used by most of the components
# Note: zenith angle is not specified, so it is assumed to be 0 (on-axis)
main:
class: 'SimulParams'
root_dir: './calib/' # Root directory for calibration manager
pixel_pupil: 160 # Linear dimension of pupil phase array
pixel_pitch: 0.05 # [m] Pitch of the pupil phase array
total_time: 0.050 # [s] Total simulation running time
time_step: 0.001 # [s] Simulation time step
# Atmospheric seeing (approx. inverse of Fried parameter r0). Here is a static value,
# but it can be a function of time. The full list of parameters can be found in the
# init method of the WaveGenerator class.
seeing:
class: 'WaveGenerator'
constant: 0.8 # ["] seeing value (500nm and at zenith)
# Wind speed and direction, also static in this example.
# These can be functions of time or vary per layer in more complex setups.
wind_speed:
class: 'WaveGenerator'
constant: [20.] # [m/s] Wind speed value
wind_direction:
class: 'WaveGenerator'
constant: [0.] # [degrees] Wind direction value
# Source definition. This defines the direction of the propagation the electromagnetic field
# the flux intensity and the wavelength. In this case, we have a single on-axis source.
# The source is defined in polar coordinates (arcsec, degrees). The full list of parameters
# can be found in the init method of the Source class.
on_axis_source:
class: 'Source'
polar_coordinates: [0.0, 0.0] # [arcsec, degrees] source polar coordinates
magnitude: 8 # source magnitude
wavelengthInNm: 750 # [nm] wavelength
# Pupil stop definition. This is the pupil geometry used in our simulation.
# It can be a circular aperture or a more complex shape.
# It can be stored in a file or defined directly in the YAML.
# When no parameters are specified, a circular pupil is assumed, with the size
# defined by the pixel_pupil parameter in the main section. The full list of parameters
# can be found in the init method of the Pupilstop class.
pupilstop:
class: 'Pupilstop'
simul_params_ref: 'main'
# Atmospheric layers generation and temporal evolution.
# Here we define a single atmospheric layer with a constant Cn2 value.
# Outer scale can be a scalar or a vector (for multiple layers).
# The layer heights are defined in meters, and the Cn2 values must sum to 1.
# The fov parameter defines the field-of-view in arcseconds.
# The inputs are the seeing, wind speed, and wind direction defined above.
atmo:
class: 'AtmoEvolution'
simul_params_ref: 'main'
L0: 40 # [m] Outer scale
heights: [0.] # [m] layer heights at 0 zenith angle
Cn2: [1.00] # Cn2 weights (total must be eq 1)
fov: 0.0 # [arcsec] Field of view, 0.0 means only on-axis,
# no off-axis sources
inputs:
seeing: 'seeing.output'
wind_speed: 'wind_speed.output'
wind_direction: 'wind_direction.output'
outputs: ['layer_list']
# The propagation block simulates the propagation of the electromagnetic field
# through the atmosphere, the pupil stop and the DM. It takes the source and the atmospheric layers
# as inputs and outputs the electric field at the pupil plane in all the directions corresponding
# to the source polar coordinates.
# The output is a list of electric fields, one for each source direction.
prop:
class: 'AtmoPropagation'
simul_params_ref: 'main'
source_dict_ref: ['on_axis_source']
inputs:
atmo_layer_list: ['atmo.layer_list']
common_layer_list: ['pupilstop', 'dm.out_layer:-1']
outputs: ['out_on_axis_source_ef']
# The Pyramid WFS block simulates the Pyramid wavefront sensor.
# It takes the electric field from the propagation block and computes the intensity on the detector.
# The full list of parameters can be found in the init method of the ModulatedPyramid class.
pyramid:
class: 'ModulatedPyramid'
simul_params_ref: 'main'
pup_diam: 30. # Pupil diameter in subaps.
pup_dist: 36. # Separation between pupil centers in subaps.
fov: 2.0 # Requested field-of-view [arcsec]
mod_amp: 3.0 # Modulation radius (in lambda/D units)
output_resolution: 80 # Output sampling [usually corresponding to CCD pixels]
wavelengthInNm: 750 # [nm] Pyramid wavelength
inputs:
in_ef: 'prop.out_on_axis_source_ef'
# The detector simulates the CCD sensor where the Pyramid WFS intensity is recorded.
# Its integration time can be a multiple of the simulation time step.
detector:
class: 'CCD'
simul_params_ref: 'main'
size: [80,80] # Detector size in pixels
dt: 0.001 # [s] Detector integration time
bandw: 300 # [nm] Sensor bandwidth
photon_noise: True # activate photon noise
readout_noise: True # activate readout noise
readout_level: 1.0 # readout noise in [e-/pix/frame]
quantum_eff: 0.32 # quantum efficiency * total transmission
inputs:
in_i: 'pyramid.out_i'
# The slope computer calculates the wavefront slopes from the detector frame.
# It requires a pupil data object with the list of the valid sub-apertures (this
# is computed from the pyramid WFS input during the calibration step).
# The full list of parameters can be found in the init method of the PyrSlopec class.
slopec:
class: 'PyrSlopec'
pupdata_object: 'scao_pupdata' # tag of the pyramid WFS pupils
inputs:
in_pixels: 'detector.out_pixels'
# The modal reconstruction block reconstructs the wavefront slopes into modal coefficients.
# It uses a reconstruction matrix that is computed during the calibration phase.
rec:
class: 'Modalrec'
recmat_object: 'scao_pyr_rec' # reconstruction matrix tag
inputs:
in_slopes: 'slopec.out_slopes'
outputs: ['out_modes']
# The control block computes the control commands based on the differential modal coefficients.
# The modal coefficients are differential because it operates in closed loop.
# The full list of parameters can be found in the init method of the Integrator class.
control:
class: 'Integrator'
simul_params_ref: 'main'
delay: 2 # Total temporal delay in time steps
int_gain: [0.5] # Integrator gain (for 'INT' control)
n_modes: [54] # This means we use 54 modes with the same gain
inputs:
delta_comm: 'rec.out_modes'
# The DM block simulates the deformable mirror.
# In this case, it uses Zernike modes directly without generating a modal basis.
# The Zernike modes are generated on the fly.
# It can also use influence functions and modes-to-command matrix stored in files.
# The DM height is set to 0, meaning it is at the pupil plane.
# The full list of parameters can be found in the init method of the DM class.
dm:
class: 'DM'
simul_params_ref: 'main'
type_str: 'zernike' # modes type
nmodes: 54 # number of modes
obsratio: 0.0 # obstruction dimension ratio w.r.t. diameter
height: 0 # DM height [m]
inputs:
in_command: 'control.out_comm'
# The PSF block computes the point spread function (PSF) based on the electric field
# at the pupil plane after the DM. It uses the wavelength and the padding coefficient
# to compute the PSF. The start_time is the time at which the PSF integration starts.
psf:
class: 'PSF'
simul_params_ref: 'main'
wavelengthInNm: 1650 # [nm] Imaging wavelength
nd: 8 # padding coefficient for PSF computation
start_time: 0.05 # PSF integration start time
inputs:
in_ef: 'prop.out_on_axis_source_ef'
outputs: ['out_psf', 'out_sr']
# Data store for saving the simulation results.
# The data will be stored in a directory named with a timestamp (TN) located in 'output'.
# In this case it saves the residual electric field on-axis source.
# Other results can be added as needed.
data_store:
class: 'DataStore'
store_dir: './output' # Data result directory: store_dir'/TN/'
inputs:
input_list: ['res_ef-prop.out_on_axis_source_ef']
If the full-rate data volume is too large, DataStore can downsample what is
written to disk for selected inputs:
data_store:
class: 'DataStore'
store_dir: './output'
downsample_factor_by_input:
res_ef: 50
sr: 5
inputs:
input_list: ['res_ef-prop.out_on_axis_source_ef',
'sr-psf.out_sr']
In this example, res_ef is stored every 50 received samples and sr
every 5. The keys in downsample_factor_by_input are the aliases before the dash
in input_list. If you want a single cadence for all inputs, use
downsample_factor instead. The two options cannot be combined.
Part 2: Calibration
Not all elements of an adaptive optics simulation are fully defined a priori. Some key components—such as the geometry of valid subapertures for the wavefront sensor or the reconstruction matrix for modal control—depend on the specific configuration and must be determined through a calibration process.
Calibration in SPECULA consists of running dedicated, simplified simulations whose purpose is to “probe” the system and extract the necessary information for later use in the main (closed-loop) simulation. These calibration runs typically:
Use a reduced or modified version of the full simulation (for example, with only the pupil mask and no atmospheric or DM layers).
Apply known inputs (such as push-pull commands or uniform illumination) to measure the response of specific components.
Save the results (such as the valid pupil geometry or the interaction/reconstruction matrices) to files.
These calibration files are then referenced in the main YAML configuration and used by the corresponding objects during the closed-loop simulation.
This approach ensures that the simulation accurately reflects the real system’s behavior. Note that if you change the pupil geometry, WFS parameters, or DM configuration, you simply repeat the relevant calibration steps before running the full simulation.
In this case we need to calibrate two components:
Pyramid WFS pupil geometry: This defines the valid subapertures for the Pyramid WFS based on the pupil geometry.
Reconstruction matrix: This defines how the wavefront slopes measured by the Pyramid WFS are converted into modal coefficients for control. The reconstruction matrix is the inverse of the interaction matrix, which is computed by applying a known push-pull signal to the DM and measuring the resulting slopes.
Step 1: Calibrate the Pyramid WFS Pupil Geometry
Create a YAML file, for example params_scao_pyr_test_pupdata.yml:
# this is the block that generates the pupil geometry file used by the Pyramid WFS
# as a function of a single frame of the Pyramid WFS
pyr_pupdata:
class: 'PyrPupdataCalibrator'
thr1: 0.1
thr2: 0.25
display_debug: True
output_tag: 'scao_pupdata'
inputs:
in_i: 'pyramid.out_i'
# Override propagation parameters leaving only the pupilstop layer and removing
# the atmo layers and the DM layer.
# This is necessary to ensure the Pyramid WFS pupil geometry is correctly computed
# considering the pupil mask.
prop_override:
inputs:
common_layer_list: ['pupilstop']
# Override main simulation parameters reducing the total time to 0.001s,
# which is sufficient for getting a single frame of the Pyramid WFS.
main_override:
total_time: 0.001
# Override Pyramid WFS parameters to set a very large modulation radius
# This is necessary to reduce the diffraction effects and get uniform illumination
pyramid_override:
mod_amp: 10.0 # Modulation radius (in lambda/D units)
# Remove unnecessary components to avoid issues and speed up the calibration
remove: ['atmo', 'slopec', 'rec','control','dm', 'data_store']
Run this calibration step with:
specula params_scao_pyr_basic.yml params_scao_pyr_test_pupdata.yml
This will generate the scao_pupdata.fits file, which contains the valid pupil geometry for the Pyramid WFS.
Step 2: Calibrate the Reconstruction Matrix
Create a YAML file, for example params_scao_pyr_test_rec.yml:
# this block generates a positive and negative push-pull signal
# to command the DM and compute the interaction matrix
pushpull:
class: 'PushPullGenerator'
nmodes: 54
vect_amplitude: [50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0,
50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0,
50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0,
50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0,
50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0,
50.0, 50.0, 50.0, 50.0] # Push-pull amplitude in nm (written by hand,
# it can be stored in a file)
outputs: ['output']
# This block computes the interaction matrix using the Pyramid WFS
# It takes the slopes from the Pyramid WFS and the push-pull commands as inputs
im_calibrator:
class: 'ImCalibrator'
nmodes: 54
im_tag: 'scao_pyr_im'
data_dir: './calib/im'
overwrite: True
inputs:
in_slopes: 'slopec.out_slopes'
in_commands: 'pushpull.output'
outputs: ['out_intmat']
# This block computes the reconstruction matrix from the interaction matrix
# doing a pseudo-inverse operation
rec_calibrator:
class: 'RecCalibrator'
nmodes: 54
rec_tag: 'scao_pyr_rec'
data_dir: './calib/rec'
overwrite: True
inputs:
in_intmat: 'im_calibrator.out_intmat'
# Override main simulation parameters to set the total time
# 54 times 2 (push+pull) times 0.001s
main_override:
total_time: 0.108 # 54 modes × 2 (push+pull) × 0.001s
# Override propagation parameters to include the pupilstop and DM layers
# removing the atmo layers
prop_override:
inputs:
common_layer_list: ['pupilstop', 'dm.out_layer']
# Override the DM parameters to set the sign (by default it is -1)
# and change the input command to the push-pull output
dm_override:
sign: 1
inputs:
in_command: 'pushpull.output'
# Override the detector parameters to disable photon and readout noise
detector_override:
photon_noise: False
readout_noise: False
# Remove unnecessary components to avoid issues and speed up the calibration
remove: ['atmo', 'rec','control']
Run this calibration step with:
specula params_scao_pyr_basic.yml params_scao_pyr_test_rec.yml
This will generate the interaction matrix (scao_pyr_im.fits) and the reconstruction matrix (scao_recmat.fits).
Summary of Calibration Steps
Pyramid Pupil Geometry: - Generates
scao_pupdata.fitsfor the Pyramid WFS geometry.Reconstruction Matrix: - Generates
scao_recmat.fitsfor modal reconstruction.
Both files are then referenced in your main YAML for closed-loop operation:
slopec:
class: 'PyrSlopec'
pupdata_object: 'scao_pupdata'
inputs:
in_pixels: 'detector.out_pixels'
rec:
class: 'Modalrec'
recmat_object: 'scao_recmat'
inputs:
in_slopes: 'slopec.out_slopes'
—
Now your SCAO simulation is properly calibrated and ready for closed-loop runs!
Part 3: Running the Simulation
To run the simulation, use:
specula params_scao_pyr_basic.yml
The simulation will run and save results in the output directory (./output/), including the residual electric field on-axis source as specified in the YAML file.
Congratulations! You have now configured and run a minimal SCAO simulation with a Pyramid WFS, Zernike DM, and a single atmospheric layer.
See also
SCAO Tutorial: Complete Walkthrough for a full-featured SCAO simulation, calibration and analysis workflow
Field Analyser Tutorial: Post-Processing PSF, Modal Analysis, and Phase Cubes for post-processing and analysis