Synthetic SHWFS Example
This is the intended first end-to-end pyrtc example for a new user.
It uses synthetic hardware classes that subclass the normal WavefrontSensor and ScienceCamera base classes, publishes the standard shared-memory streams, and runs the real SlopesProcess, Loop, and WavefrontCorrector components.
What This Example Covers
how to subclass a wavefront sensor without touching the rest of the AO pipeline
how to subclass a science camera for a simple synthetic PSF path
what a minimal config layout looks like for a runnable system
how to start a soft-RTC chain that still works with the standard viewer tools
what update rates and residual metrics to expect from a small CPU-only quick-start setup
Files
The example assets live under examples/synthetic_shwfs/:
config.yaml: runnable soft-RTC configuration
run_soft_rtc.py: demo launcher that builds the control chain, applies an identity interaction matrix, and prints live status
The synthetic hardware implementations live in pyRTC/hardware/SyntheticSystems.py.
Why This Example Exists
The OOPAO notebook remains useful, but it is not the best first touchpoint for release-quality onboarding because it adds an extra simulator dependency and hides some of the component boundaries that matter when you are integrating real hardware.
This synthetic SHWFS path keeps the mental model simple:
SyntheticSHWFS.expose() generates a lenslet image from a deterministic disturbance and the current wfc correction.
SlopesProcess.computeSignal() turns that image into SHWFS slopes.
Loop.standardIntegrator() reads signal, computes a correction, and writes wfc.
WavefrontCorrector.sendToHardware() updates wfc2D for display.
SyntheticScienceCamera.expose() reads residual slopes and generates a synthetic PSF plus Strehl estimate.
Running It
From the repository root:
python examples/synthetic_shwfs/run_soft_rtc.py --duration 15
The launcher clears the standard pyRTC streams by default, starts all configured worker threads, and prints one status line per second. A typical line looks like:
t= 5.0s wfs= 199.6 Hz psf= 49.8 Hz residual_rms=0.0312 correction_rms=0.1098 strehl=0.914
The exact numbers will vary by host, but the important pattern is:
the synthetic WFS should stay close to its configured frame rate
the residual RMS should settle below the open-loop disturbance amplitude
the synthetic Strehl should rise when the loop is behaving sensibly
Viewer Commands
The preferred way to inspect the demo is a single composite viewer window:
pyrtc-view wfs signal2D wfc2D psfShort psfLong --geometry 2x3
If you want a smaller science-camera-only view, open:
pyrtc-view psfShort psfLong --geometry row
The composite viewer auto-sizes to the stream dimensions, so it is usually a better default than opening many separate windows.
Config Layout
The synthetic config deliberately uses the same top-level sections you will keep for a real system:
wfs:
name: SyntheticSHWFS
width: 32
height: 32
frameRateHz: 200
subApSpacing: 8
functions:
- expose
slopes:
type: SHWFS
signalType: slopes
subApSpacing: 8
subApOffsetX: 0
subApOffsetY: 0
functions:
- computeSignal
wfc:
name: SyntheticWFC
numActuators: 32
numModes: 32
functions:
- sendToHardware
loop:
gain: 0.35
functions:
- standardIntegrator
psf:
name: SyntheticScienceCamera
width: 64
height: 64
integration: 10
functions:
- expose
- integrate
The transition from synthetic hardware to real hardware should mostly leave slopes, loop, and high-level wiring alone. In a typical integration, the first file you replace is the wfs section and the subclass behind it.
Subclassing Pattern
The synthetic classes are intentionally short and can be used as templates.
For a wavefront sensor, the important rule is: populate self.data and then call the parent expose() so the normal dark subtraction and shared-memory publication still happen.
from pyRTC.WavefrontSensor import WavefrontSensor
class MyWavefrontSensor(WavefrontSensor):
def __init__(self, conf):
super().__init__(conf)
self.serial_number = conf["serialNumber"]
def expose(self):
self.data = self.read_frame_from_camera_driver()
super().expose()
For a science camera, the pattern is the same: generate or acquire a frame in self.data, then call the parent implementation.
from pyRTC.ScienceCamera import ScienceCamera
class MyScienceCamera(ScienceCamera):
def __init__(self, conf):
super().__init__(conf)
def expose(self):
self.data = self.read_frame_from_camera_driver()
super().expose()
In both cases, that keeps the base-class shared-memory contract intact, which is what lets the rest of the pipeline remain unchanged.
Speed Expectations
This quick-start example is deliberately small. On a normal development workstation, a 32x32 synthetic SHWFS at 200 Hz and a 64x64 synthetic science camera at 50 Hz should be comfortably attainable in soft-RTC mode.
Use this example for:
checking that your install works
understanding the component boundaries
validating viewer behavior
experimenting with loop gain and SHWFS geometry
Do not use it to infer final hardware performance. For kernel-level performance expectations, use the benchmark tables in the project README and the benchmarking tools under benchmarks/.
Next Steps
Once this example is working, the next sensible step is one of:
replace SyntheticSHWFS with your real camera subclass while keeping the same slopes, loop, and wfc sections
replace SyntheticScienceCamera with your real PSF camera subclass if you need science-image monitoring
move to the OOPAO example when you want a richer simulated optical path