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:

  1. SyntheticSHWFS.expose() generates a lenslet image from a deterministic disturbance and the current wfc correction.

  2. SlopesProcess.computeSignal() turns that image into SHWFS slopes.

  3. Loop.standardIntegrator() reads signal, computes a correction, and writes wfc.

  4. WavefrontCorrector.sendToHardware() updates wfc2D for display.

  5. 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