13.3. openwfs.devices

class Axis(channel, v_min, v_max, scale, maximum_acceleration, terminal_configuration=TerminalConfiguration.DEFAULT)[source]

Bases: object

Specification of a NIDAQ output channel for controlling a scan axis.

Output voltages are clipped to the safety limit indicated by [v_min, v_max] Note that the actual field of view only covers part of this range (see reference_zoom in ScanningMicroscope.).

channel

The name of the channel, e.g. ‘Dev4/ao0’

v_min

The minimum voltage that can safely be sent to the output.

v_max

The maximum voltage that can safely be sent to the output.

scale

Conversion factor between voltage at the NI-DAQ card and displacement of the focus in the object plane. This may be different for different axes.

maximum_acceleration

The maximum acceleration of this axis in V per s². The output signal will be constructed to ensure that the mirror does not exceed this acceleration, except in special cases such as when turning on the system. This acceleration is also used to compute how far to overshoot the scan mirror to ensure that it reaches a linear speed over the scan range. See scan for details.

terminal_configuration

The terminal configuration of the channel, defaults to TerminalConfiguration.DEFAULT

channel: str
static compute_acceleration(*, optical_deflection, torque_constant, rotor_inertia, maximum_current)[source]

Computes the angular acceleration of the focus of the galvo mirror.

The result is returned in the unit V / second², where the voltage can be converted to displacement using the scale factor.

Parameters:
  • optical_deflection (Annotated[Quantity]) – The optical deflection (i.e. twice the mechanical angle) of the mirror as a function of applied voltage.

  • torque_constant (Annotated[Quantity]) – The torque constant of the galvo mirror driving coil. May also be given in the equivalent unit of dyne·cm/A.

  • rotor_inertia (Annotated[Quantity]) – The moment of inertia of the rotor. May also be given in the equivalent unit of g·cm².

  • maximum_current (Annotated[Quantity]) – The maximum current that can be applied to the galvo mirror.

Returns:

Quantity[u.V / u.s**2] – The maximum acceleration of the galvo mirror in voltage per second squared.

static compute_scale(*, optical_deflection, galvo_to_pupil_magnification, objective_magnification, reference_tube_lens)[source]

Computes the conversion factor between voltage and displacement in the object plane.

Parameters:
  • optical_deflection (Annotated[Quantity]) – The optical deflection (i.e. twice the mechanical angle) of the mirror as a function of applied voltage.

  • galvo_to_pupil_magnification (float) – The magnification of the relay system between the galvo mirrors and the pupil.

  • objective_magnification (float) – The magnification of the microscope objective.

  • reference_tube_lens (Annotated[Quantity]) – The tube lens focal length on which the objective magnification is based. This value is manufacturer-specific. Typical values are: - 200 mm for Thorlabs, Nikon, Leica, and Mitutoyo - 180 mm for Olympus/Evident - 165 mm for Zeiss

Returns:

Quantity[u.um/u.V] – The conversion factor between voltage and displacement in the object plane.

maximum_acceleration: Annotated[Quantity, Unit('V / s2')]
maximum_scan_speed(linear_range)[source]

Computes the maximum scan speed in V per sample.

It is assumed that the mirror accelerates and decelerates at the maximum acceleration, and scans with a constant velocity over the linear range. There are two limits to the scan speed:

  • A practical limit: if it takes longer to perform the acceleration + deceleration than it does to traverse the linear range, it does not make sense to set the scan speed so high. The speed at which acceleration + deceleration takes as long as the linear range is the maximum speed.

  • A hardware limit: when accelerating with the maximum acceleration over a distance 0.5 · (V_max-V_min) · (1-linear_range), the mirror will reach the maximum possible speed.

Parameters:

linear_range (float) – Fraction of the full range that is used for the linear part of the scan.

Returns:

Quantity[u.V / u.s] – Maximum scan speed.

scale: Annotated[Quantity, Unit('um / V')]
scan(start, stop, sample_count, sample_rate)[source]

Generate a voltage sequence to scan with a constant velocity from start to stop, including acceleration and deceleration.

Before starting this sequence, the mirror is assumed to be standing still at the launch point, which is some distance _before_ start. After the scan sequence, the mirror is stopped at the landing point, which is some distance _after_ stop. The launch point and landing point are returned along with the scan sequence.

This function also returns a slice object, which represents the part of the sequence that corresponds to a linear movement from start to stop. slice.stop - slice.start = sample_count.

The scan follows the coordinate convention used throughout OpenWFS and Astropy, where the coordinates correspond to the centers of the pixels. Therefore, while the linear part of the scan starts at start and ends at stop, the sample points in this range correspond to the centers of the pixels, so the sample at slice.start lies half a pixel _after_ start, and the sample at slice.stop - 1 lies half a pixel _before_ stop.

Parameters:
  • start (float) – Starting position in relative coordinates (0.0 to 1.0).

  • stop (float) – Ending position in relative coordinates (0.0 to 1.0).

  • sample_count (int) – Number of samples in the linear part of the scan.

  • sample_rate (Annotated[Quantity]) – The sampling rate of the voltage sequence.

Returns:

tuple

A tuple containing:
  • Quantity[u.V]: Voltage sequence for the entire scan.

  • float: Launch point in relative coordinates.

  • float: Landing point in relative coordinates.

  • slice: Slice object indicating the linear part of the scan.

step(start, stop, sample_rate)[source]

Generate a voltage sequence to move from start to stop in the fastest way possible.

This function assumes that the mirror is standing still at v_start, and generates a voltage ramp to move the mirror and stop it exactly at v_end, using the maximum acceleration allowed by the mirror.

The voltage curve is given by: v_start + 1/2 a·t² for t < t_total /2 v_end - 1/2 a·(t_total-t)² for t >= t_total

using continuity at t=t_total/2, we can solve this equation to get: t_total = 2·sqrt((v_end - v_start) / a)

Parameters:
  • start (float) – Starting position in relative coordinates (0.0 to 1.0).

  • stop (float) – Ending position in relative coordinates (0.0 to 1.0).

  • sample_rate (Annotated[Quantity]) – The sampling rate of the voltage sequence.

Returns:

Quantity[u.V] – Voltage sequence for the movement.

terminal_configuration: TerminalConfiguration = -1
to_pos(volt)[source]

Converts voltage [V_min .. V_max] to relative position [0.0 .. 1.0].

Parameters:

volt (Annotated[Quantity]) – Voltage value(s) to convert.

Returns:

np.ndarray – Relative position value(s) between 0.0 and 1.0.

to_volt(pos)[source]

Converts relative position [0.0 … 1.0] to voltage [V_min … V_max].

Currently, this is just a linear conversion, but a lookup table may be used in the future.

Parameters:

pos (Union[ndarray, float]) – Relative position value(s) between 0.0 and 1.0.

Returns:

Quantity[u.V] – Voltage value(s) between V_min and V_max.

v_max: Annotated[Quantity, Unit('V')]
v_min: Annotated[Quantity, Unit('V')]
class Camera(cti_file=None, serial_number=None, multi_threaded=True, **kwargs)[source]

Bases: Detector

Adapter for GenICam/GenTL cameras.

_nodes

The GenICam node map of the camera. This map can be used to access camera properties, see the GenICam/GenAPI documentation and the Standard Features Naming Convention for more details.

The node map should not be used to set properties that are available as properties in the Camera object, such as exposure, width, height, binning, etc.

Also, the node map should not be used to set properties while the camera is fetching a frame (i.e., between trigger() and calling result() on the returned concurrent.futures.Future object).

Note

This class is a thin wrapper around the Harvesters module, which is a generic adapter for GenICam/GenTL cameras.

Example

>>> camera = Camera(cti_file=R"C:\Program Files\Basler\pylon 7\Runtime\x64\ProducerU3V.cti")
>>> camera.exposure = 10 * u.ms
>>> frame = camera.read()
property binning: int

Pixel binning factor.

Note

setting horizontal and vertical binning separately is not supported.

cam_harvester
property data_shape

Shape (height, width) of the data array returned by the camera.

property duration: Annotated[Quantity, Unit('ms')]

The duration between the trigger and the end of the exposure.

Returns ∞ · ms if hardware triggering is used.

static enumerate_cameras(cti_file=None)[source]

Enumerates all cameras available through the specified GenTL producer.

Parameters:

cti_file – The path to the GenTL producer file. If None, the files on the GENICAM_GENTL64_PATH or GENICAM_GENTL32_PATH environment variable will be used.

Returns:

list – A list of device information dictionaries for all available cameras.

property exposure: Annotated[Quantity, Unit('ms')]

Exposure time of the camera

property height: int

Height of the camera frame, in pixels.

Note

The camera may round up this value to multiples of some power of 2.

property left: int

The horizontal start position of the region of interest (in pixels).

Note

The camera may round up this value to multiples of some power of 2.

paused()[source]

Returns a context manager for pausing the camera.

Usage ::
>>> with camera.paused():
>>>     camera.nodes.SomeNode.value = 10
property pixel_size: Annotated[Quantity, Unit('um')] | None

Physical pixel size of the camera sensor.

property top: int

The vertical start position of the region of interest (in pixels).

Note

The camera may round up this value to multiples of some power of 2.

property width: int

Width of the camera frame, in pixels.

Note

The camera may round up this value to multiples of some power of 2.

ModuleType

alias of ModuleType

class SLM(monitor_id=WINDOWED, shape=None, pos=(0, 0), refresh_rate=None, latency=2, duration=1, coordinate_system='short', transform=None)[source]

Bases: Actuator, PhaseSLM

An OpenGL object to control a spatial light modulator connected to a graphics card.

See Section 8 for more information.

WINDOWED = 0
clone(monitor_id=WINDOWED, shape=None, pos=(0, 0))[source]

Creates a new SLM window that mirrors the content of this SLM window.

This is useful for demonstration and debugging purposes. The image in the clone window is updated automatically when the SLM is updated.

Parameters:
  • monitor_id (int) – ID of the monitor to display the window on. Defaults to WINDOWED (0) mode

  • shape (Optional[tuple[int, int]]) – shape (height, width) of the window.

  • pos (tuple[int, int]) – position (y, x) of the window.

Returns:

Returns an object that should be stored in a variable to keep the SLM window open. When the variable is cleared or leaves the current scope, the SLM window is closed. See slm_demo.py for an example.

property coordinate_system: str

Specifies the base coordinate system that is used to map vertex coordinates to the SLM window.

Possible values are ‘full’, ‘short’ and ‘long’.

‘full’ means that the coordinate range (-1,-1) to (1,1) is mapped to the entire SLM window. If the window is not square, this means that the coordinates are anisotropic.

‘short’ and ‘long’ map the coordinate range (-1,-1) to (1,1) to a square. ‘short’ means that the square is scaled to fill the short side of the SLM (introducing zero-padding at the edges).

‘long’ means that the square is scaled to fill the long side of the SLM (causing part of the coordinate range to be cropped because these coordinates correspond to points outside the SLM window).

For a square SLM, ‘full’, ‘short’ and ‘long’ are all equivalent.

In all three cases, (-1,-1) corresponds to the top-left corner of the screen, and (1,-1) to the bottom-left corner. This convention is consistent with that used in numpy/matplotlib

To further modify the mapping system, use the transform property.

property duration: Annotated[Quantity, Unit('ms')]

Maximum amount of time it takes to perform the measurement or for the actuator to stabilize.

This value does not include the latency.

For a detector, this is the maximum amount of time that elapses between returning from trigger() and the end of the measurement. For an actuator, this is the maximum amount of time that elapses from returning from a command like update() and the stabilization of the device.

If the duration of an operation is not known in advance, (e.g., when waiting for a hardware trigger), this function should return np.inf * u.ms.

Note

A device may update the duration dynamically. For example, a stage may compute the required time to move to the target position and update the duration accordingly.

Note

If latency is a (lower) estimate, the duration should be high enough to guarantee that latency + duration is at least as large as the time between starting the operation and finishing it.

property field: Detector

Returns a ‘camera’ to monitor the current field coming from the SLM.

Returns:

a detector that returns self.amplitude * exp(1.0j * self.phases.read()

property latency: Annotated[Quantity, Unit('ms')]

Minimum amount of time between sending a command or trigger to the device and the moment the device starts responding.

The default value is 0.0 ms.

Note

The latency is used to compute when it is safe to switch the global state from ‘moving’ to ‘measuring’ or vice versa. Devices that report a non-zero latency promise not to do anything before the latency has passed. This allows making the state switch even before the devices of the other type have all finished.

This construct is used for spatial light modulators, which typically have a long latency (1-2 frames). Due to this latency, we may send an update command to the SLM even before the camera has finished reading the previous frame.

Note

A device is allowed to report a different latency every time latency is called. For example, for a spatial light modulator that only refreshes at a fixed rate, we can add the remaining time until the next refresh to the latency.

property lookup_table: Sequence[int]

Lookup table that is used to map the wrapped phase range of 0-2pi to gray values

The gray values are represented in the range from 0 to 2**bit_depth - 1). For an 8-bit video mode, this is 0-255. By default, a linear lookup table is set: range(2**bit_depth - 1).

Note: lookup table need not contain 2**bit_depth elements. A typical scenario is to use something like slm.lookup_table=range(142) to map the 0-2pi range to only the first 142 gray values.

property monitor_id: int

Number of the monitor (1 for primary screen, 2 for secondary screen, etc.) for the SLM.

Each monitor can only hold a single full-screen window. Use monitor_id=SLM.WINDOWED to show a windowed SLM on the primary screen (for debugging and monitoring purposes). There can be multiple windowed SLMs on the primary screen, but there cannot also be a fullscreen SLM on the primary screen at the same time.

monitor_id can be modified at run time, in which case the current SLM window is replaced by a new window on a different monitor. When moving the SLM to a different window, width, height and refresh_rate are set

patches: list[Patch]
property period: Annotated[Quantity, Unit('ms')]

The period of the refresh rate in milliseconds (read only).

property phases: Detector

Returns an object to monitor the phase pattern last sent to the SLM.

This is the pattern before applying the lookup table.

property pixels: Detector

Returns a ‘camera’ to monitor the current value of the pixels displayed on the SLM.

property position: tuple[int, int]

The position of the top-left corner of the SLM window as (y, x) screen coordinates.

Note

This property is ignored for full-screen SLMs.

primary_patch
property refresh_rate: Annotated[Quantity, Unit('Hz')]

Refresh rate of the SLM in Hz (read only).

Note

The refresh rate cannot be modified after the SLM is created. When moving the SLM to a different monitor (see monitor_id), the refresh rate is changed to the current video mode on that monitor. Note that OpenGL specifies this value as an integer, whereas some SLMs support non-integer refresh rates. It is always best to not specify the refresh rate and set the video mode in the operating system before creating the SLM object.

set_phases(values, update=True)[source]

Sets the phase pattern on the SLM.

Parameters:
  • values (ArrayLike) – phase pattern, in radians. The pattern is automatically stretched to fill the full SLM.

  • update – when True, calls update after setting the phase pattern. Set to False to suppress the call to update. This is useful in advanced scenarios where multiple parameters of the SLM need to be changed before updating the displayed image.

property shape: tuple[int, int]

Shape (height × width) of the window in pixels.

Limitations :
  • For windowed-mode SLMs, the size cannot be modified.

  • When moving the SLM to a different monitor (see monitor_id), the SLM is sized to match the current

    resolution on that monitor. Note that this value may differ from the value passed as input, because the input value is specified in screen coordinates, whereas the reported width is in pixels. In this case, the original value of shape will be lost.

  • The transform property is not updated automatically, so if the aspect ratio changes

    the transform needs to be set again.

property transform: Transform

Global transformation matrix

The transform determines how the vertex coordinates that make up the shape of a Patch (see Patch) are mapped to the standard coordinate system. In turn, the coordinate_system property determines how this coordinate system is mapped to the SLM window. By default, this value is just the identity transformation Transform().

update()[source]

Sends the new phase pattern to be displayed on the SLM.

This function waits for the vsync, and returns directly after it. Therefore, it can be used as software synchronization to the SLM.

Note

This function does not wait for the image to appear on the SLM. To wait for the image stabilization explicitly, use ‘wait()’. However, this should rarely be needed since all Detectors already call wait_finished before starting a measurement.

Note

At the moment, update() blocks until all OpenGL commands are processed, and a vertical retrace occurs (i.e., the hardware signals the start of a new frame). This behavior may change in the future and should not be relied on. Instead, use the automatic synchronization mechanism to synchronize detectors with the SLM hardware.

class ScanningMicroscope(input, y_axis, x_axis, sample_rate, resolution, reference_zoom, *, delay=<Quantity 0. us>, bidirectional=True, multi_threaded=True, preprocessor=None, test_pattern=TestPatternType.NONE, test_image=None)[source]

Bases: Detector

Laser scanning microscope with galvo mirrors controlled by a National Instruments data acquisition card (nidaq).

Effectively, a ScanningMicroscope works like a camera, which can be triggered and returns 2-D images. These images are obtained by raster-scanning a focus using two galvo mirrors, controlled by a nidaq card, and recording a detector signal (typically from a photon multiplier tube (PMT)) with the same card.

Region of Interest (ROI):

Upon construction, the maximum voltage range is set for both axes. To avoid possible damage to the hardware, this range cannot be exceeded during scanning, and the value cannot be changed after construction of the ScanningMicroscope. It is recommended to set this voltage range slightly larger than the maximum field of view of the microscope (but still within the limits that may cause damage to the hardware), so that the mirror has some room to accelerate and decelerate at the edges of the scan range.

The scan region is defined by the following equation:

V_start = V_min + (center - 0.5 / (reference_zoom * zoom)) * (V_max - V_min) V_stop = V_min + (center + 0.5 / (reference_zoom * zoom)) * (V_max - V_min)

or, in relative coordinates:

start_full = center - 0.5 / (reference_zoom * zoom) stop_full = center + 0.5 / (reference_zoom * zoom)

with * V_min, V_max: The voltage ranges set for both axes: * zoom, reference_zoom: The zoom factors * center: The center of the image relative to the full voltage range, default value is 0.5

The scan region is divided into resolution × resolution pixels. Within this scan region, a smaller region of interest (ROI) can be defined by setting top, left, height, and width properties. The ROI is defined in pixels, with (0,0,resolution, resolution) corresponding to the full field of view.

start = center + (left / resolution - 0.5) / (reference_zoom * zoom) stop = center + ((left + width) / resolution - 0.5) / (reference_zoom * zoom)

Scan pattern:

The scanner performs a raster scan, with y being the slow axis and x the fast axis. The pattern is computed such that the mirrors have a constant velocity during the scan. The start and end of each scan line, where the mirror accelerates or decelerates are discarded from the data. By default, the scanner uses bidirectional scanning along the fast axis, which reduces the time needed for a full scan. Especially for bidirectional scanning, the synchronization between output and input is crucial, otherwise the image will appear teared (even and odd scan lines not aligning). To fine-tune this synchronization, the delay parameter can be used.

property bidirectional: bool

Whether scanning is bidirectional along the fast axis.

When enabled, the scanner acquires data in both directions along the fast axis, which reduces the total scan time but may require more precise timing adjustments.

Returns:

bool – True if bidirectional scanning is enabled, False otherwise.

property binning: int

Undersampling factor.

Increasing the binning reduces the number of pixels in the image while keeping dwell time the same. As a result, the total duration of a scan decreases.

Note

This behavior is different from that of a real camera. No actual binning is performed, the scanner just takes fewer steps in x and y

Note: the ROI is kept the same as much as possible.

However, due to rounding, it may vary slightly.

close()[source]

Close connection to the NI-DAQ.

This method closes both the read and write tasks, releasing the hardware resources. It should be called when the scanning microscope is no longer needed.

property delay: Annotated[Quantity, Unit('us')]

Delay between the control signal to the mirrors and the start of data acquisition.

This property controls the synchronization between the mirror movement and data acquisition. Adjusting this value can help correct for timing mismatches that cause image tearing.

Returns:

Quantity[u.us] – The current delay setting.

property duration: Annotated[Quantity, Unit('ms')]

Total duration of scanning for one frame.

This property calculates the time required to complete a full scan based on the scan pattern length and the sample rate.

Returns:

Quantity[u.ms] – The total time required to complete one frame.

property dwell_time: Annotated[Quantity, Unit('us')]

The time spent on each pixel during scanning.

This property calculates the dwell time based on the oversampling factor and the sample rate of the scanner.

Returns:

Quantity[u.us] – The time spent on each pixel during scanning.

property exposure: Annotated[Quantity, Unit('ms')]

The required time to scan a frame.

This is an alias for the duration property, provided for compatibility with the camera interface.

Returns:

Quantity[u.ms] – The total time required to complete one frame.

property height: int

The number of pixels in the vertical dimension of the ROI.

Returns:

int – The height of the ROI in pixels.

property left: int

The leftmost pixel of the Region of Interest (ROI) in the scan range.

Returns:

int – The x-coordinate of the left edge of the ROI in pixels.

static list_devices()[source]

Returns a list of all NI-DAQ devices available on the system.

This method queries the system for all National Instruments data acquisition devices that are currently connected and available.

Returns:

list – A list of device names (strings) that can be used in the channel specifications.

property offset_x: float

The center of the full field of view in the horizontal direction.

The offset is relative to the full voltage range specified in the Axis objects, with 0.0 corresponding to the center of the voltage range, and -0.5 and +0.5 to the edges of the voltage range.

Note that changing the offset may cause the ROI to move outside the original field of view. Also, it may cause the scan speed to change, as the mirror has a shorter distance to accelerate or decelerate.

property offset_y: float

The center of the full field of view in the vertical direction.

The offset is relative to the full voltage range specified in the Axis objects, with 0.0 corresponding to the center of the voltage range, and -0.5 and +0.5 to the edges of the voltage range.

Note that changing the offset may cause the ROI to move outside the original field of view. Also, it may cause the scan speed to change, as the mirror has a shorter distance to accelerate or decelerate.

property pixel_size: Quantity

The size of a pixel in the object plane.

This property calculates the physical size of each pixel based on the voltage range, scale factors of the axes, zoom settings, and resolution.

Returns:

Quantity – The physical size of each pixel in the object plane, as a 2D quantity (y, x).

property preprocessor: callable | None

An optional function to preprocess raw data before cropping.

The function takes a linear array of raw data as required arguments, and a list of keyword arguments. Currently, the following arguments are passed:

  • data: The raw data array from the scanner.

  • sample_rate (Quantity[u.MHz]): The sample rate of the NI-DAQ input channel.

Returns:

Optional[callable] – The current preprocessor function or None if not set.

reset_roi()[source]

Reset the ROI to span the original left, top, width and height.

This method resets the Region of Interest to cover the full scan area, setting left and top to 0, and width and height to the full resolution.

property resolution: int
property scan_speed: Annotated[float, Ge(ge=0.05), Le(le=1.0)]

The scan speed relative to the maximum scan speed.

property test_pattern: TestPatternType

Get the current test pattern type.

Returns:

TestPatternType – The current test pattern setting.

property top: int

The topmost pixel of the ROI in the scan range.

Returns:

int – The y-coordinate of the top edge of the ROI in pixels.

property width: int

The number of pixels in the horizontal dimension of the ROI.

Depending on the scan speed and sample rate, the scanner may acquire multiple data points along a scan line, and return the averaged value. A value of 1 is treated as a special case, where the beam does not move horizontally at all (i.e. it does not scan back and forth over the size of this single pixel).

Returns:

int – The width of the ROI in pixels.

property zoom: float

Zoom factor for the scanning microscope.

The zoom factor determines the pixel size relative to the original pixel size. When zooming in or out, the center of the region of interest is kept constant. Note that this may cause the field of view to get extended to outside the original FOV.

Returns:

float – The current zoom factor.

class ZaberLinearStage(port, device_number=0, protocol='ascii')[source]

Bases: Actuator

Wrapper for a single Zaber linear stage connected via serial port.

todo: asynchroneous behavior (now setting the position blocks until the stage has moved completely) todo: add a settle_time parameter to tell openwfs how long to wait after a move before starting a measurement. todo: at the moment it is possible to construct two stage objects referring to the same device. This condition

should be checked and result in an error being raised.

Parameters:
  • port (str) – COM port string, e.g. “COM3”

  • device_number (int) – index of the device on the port (0-based)

  • protocol (str) – “ascii” or “binary”

Example

stage = ZaberLinearStage(port=”COM3”, device_number=0, protocol=”ascii”) stage.x = 5000 * u.um # move to 5000 micrometers print(stage.x) # get current position stage.home() # home the stage

home()[source]
static list_all_devices()[source]

List all COM ports and devices connected to them using both ASCII and Binary protocols. Works even if some ports are already open by objects.

Returns: dict { “COMx”: [{“protocol”: “ascii”|”binary”, “devices”: […] }], … }

property position: Annotated[Quantity, Unit('um')]
class ZaberXYStage(port_x, port_y=None, device_number_x=0, device_number_y=None, protocol='ascii')[source]

Bases: Actuator

Wrapper for a pair of Zaber linear stages connected via serial ports, controlling X and Y axes.

todo: if we add more stages, it makes sense to make a general XYStage class that combines two linear stages into one. todo: Currently both axis move after each other, need to implement parallel movement.

Parameters:
  • port_x (str) – COM port string for X axis, e.g. “COM3”

  • port_y (str | None) – COM port string for Y axis, e.g. “COM4”. If None, uses port_x.

  • device_number_x (int) – index of the device on the X axis port (0-based)

  • device_number_y (int | None) – index of the device on the Y axis port (0-based). If None and port_y==port_x, assumes second device (1), else 0.

  • protocol (str) – “ascii” or “binary”

Example

stage = ZaberXYStage(port_x=”COM3”, port_y=”COM4”, device_number_x=0, device_number_y=0, protocol=”ascii”) stage.home() # home both axes stage.x = 5000 * u.um # move X to 5000 micrometers stage.y = 2 * u.mm # move Y to 2 millimeters print(stage.x, stage.y) # get current positions

home()[source]
static list_all_devices()[source]

List all COM ports and devices connected to them using both ASCII and Binary protocols. Works even if some ports are already open by objects.

Returns: dict { “COMx”: [{“protocol”: “ascii”|”binary”, “devices”: […] }], … }

property x: Annotated[Quantity, Unit('um')]
x_home()[source]
property y: Annotated[Quantity, Unit('um')]
y_home()[source]
is_loaded(module)[source]

Helper function to check if a module is a real loaded module or a mock module.

safe_import(module_name, extra_name)[source]

Tries to import a module.

If the import files (usually because the module is not installed), this function registers a mock module so that importing can still continue without giving an error. This is essential for loading openwfs if optional dependencies are missing. This is also important for readthedocs, where not all dependenceis can be installed.

13.3.1. openwfs.devices.slm

class Geometry(vertices, indices)[source]

Bases: object

Class that represents the shape of a Patch object that can be drawn on the screen.

The geometry is defined by a set of vertices and a set of indices that define the order in which the vertices are drawn.

The vertices are stored in a numpy array of shape (N, 4) where N is the number of vertices. Each vertex is represented by an array of four values [x, y, tx, ty]. Here, (x, y) are the coordinates that determine where the vertex is drawn on the screen (after the applying the transform of the SLM that contains this patch).

The vertices and indices define a triangle strip (see OpenGL specification) that is drawn on the screen. A triangle strip is a sequence of connected triangles, where each triangle shares two vertices with the previous triangle. The indices are used to determine the order in which the vertices are connected to form the triangles. To start a new triangle strip, insert the special index 0xFFFF into the index array.

(tx, ty) are the texture coordinates that determine which pixel of the texture (e.g. the array passed to set_phases) is drawn at each vertex. For each triangle, the screen coordinates (x,y) define a triangle on the screen, whereas the texture coordinates (tx, ty) define a triangle in the texture. OpenGL maps the texture triangle onto the screen triangle, using linear interpolation of the coordinates between the vertex points.

The vertex data is uploaded to the GPU when the Geometry object is assigned to the geometry property of a Patch.

static compute_indices_for_grid(shape)[source]

Computes indices for a rectangular grid of vertices.

Assuming the vertices represent the corner points of a rectangular grid, this function computes the indices needed to connect these vertices into triangle strips.

Parameters:

shape (Sequence[int]) – The shape of the grid (nr, nc). nr is the number of grid rows, nc is the number of grid columns. Note that the number of vertices should equal (nr + 1) · (nc+1).

property indices

The indices of the geometry.

property vertices

The vertices of the geometry.

class Patch(slm, geometry=None, vertex_shader=default_vertex_shader, fragment_shader=default_fragment_shader)[source]

Bases: PhaseSLM

property geometry

Vertices that define the shape of the patch on the screen. Currently, this should be a NxMx4 numpy array of float32 values. Each 4 values define a vector: x,y position and tx, ty texture coordinate. See geometry.py for examples of geometry specifications. The vertices are drawn as a NxM ‘rectangular’ grid of quadrilaterals.

set_phases(values, update=True)[source]
Parameters:
  • values (ArrayLike) – 1-D or 2-D array holding phase values to display on the SLM. Phases are in radians, and stored as float32. There is no need to wrap the phase to a 0-2pi range.

  • update (bool) – when True, the SLM in which this patch is contained is updated immediately. When False, the SLM is not updated, and the caller is responsible for calling slm.update() to update the SLM.

update()[source]

Sends the new phase pattern to be displayed on the SLM.

Implementations should call _start() before triggering the SLM.

Note

This function does not wait for the image to appear on the SLM. To wait for the image stabilization explicitly, use ‘wait()’. However, this should rarely be needed since all Detectors already wait for the image to stabilize before starting a measurement.

class SLM(monitor_id=WINDOWED, shape=None, pos=(0, 0), refresh_rate=None, latency=2, duration=1, coordinate_system='short', transform=None)[source]

Bases: Actuator, PhaseSLM

An OpenGL object to control a spatial light modulator connected to a graphics card.

See Section 8 for more information.

WINDOWED = 0
clone(monitor_id=WINDOWED, shape=None, pos=(0, 0))[source]

Creates a new SLM window that mirrors the content of this SLM window.

This is useful for demonstration and debugging purposes. The image in the clone window is updated automatically when the SLM is updated.

Parameters:
  • monitor_id (int) – ID of the monitor to display the window on. Defaults to WINDOWED (0) mode

  • shape (Optional[tuple[int, int]]) – shape (height, width) of the window.

  • pos (tuple[int, int]) – position (y, x) of the window.

Returns:

Returns an object that should be stored in a variable to keep the SLM window open. When the variable is cleared or leaves the current scope, the SLM window is closed. See slm_demo.py for an example.

property coordinate_system: str

Specifies the base coordinate system that is used to map vertex coordinates to the SLM window.

Possible values are ‘full’, ‘short’ and ‘long’.

‘full’ means that the coordinate range (-1,-1) to (1,1) is mapped to the entire SLM window. If the window is not square, this means that the coordinates are anisotropic.

‘short’ and ‘long’ map the coordinate range (-1,-1) to (1,1) to a square. ‘short’ means that the square is scaled to fill the short side of the SLM (introducing zero-padding at the edges).

‘long’ means that the square is scaled to fill the long side of the SLM (causing part of the coordinate range to be cropped because these coordinates correspond to points outside the SLM window).

For a square SLM, ‘full’, ‘short’ and ‘long’ are all equivalent.

In all three cases, (-1,-1) corresponds to the top-left corner of the screen, and (1,-1) to the bottom-left corner. This convention is consistent with that used in numpy/matplotlib

To further modify the mapping system, use the transform property.

property duration: Annotated[Quantity, Unit('ms')]

Maximum amount of time it takes to perform the measurement or for the actuator to stabilize.

This value does not include the latency.

For a detector, this is the maximum amount of time that elapses between returning from trigger() and the end of the measurement. For an actuator, this is the maximum amount of time that elapses from returning from a command like update() and the stabilization of the device.

If the duration of an operation is not known in advance, (e.g., when waiting for a hardware trigger), this function should return np.inf * u.ms.

Note

A device may update the duration dynamically. For example, a stage may compute the required time to move to the target position and update the duration accordingly.

Note

If latency is a (lower) estimate, the duration should be high enough to guarantee that latency + duration is at least as large as the time between starting the operation and finishing it.

property field: Detector

Returns a ‘camera’ to monitor the current field coming from the SLM.

Returns:

a detector that returns self.amplitude * exp(1.0j * self.phases.read()

property latency: Annotated[Quantity, Unit('ms')]

Minimum amount of time between sending a command or trigger to the device and the moment the device starts responding.

The default value is 0.0 ms.

Note

The latency is used to compute when it is safe to switch the global state from ‘moving’ to ‘measuring’ or vice versa. Devices that report a non-zero latency promise not to do anything before the latency has passed. This allows making the state switch even before the devices of the other type have all finished.

This construct is used for spatial light modulators, which typically have a long latency (1-2 frames). Due to this latency, we may send an update command to the SLM even before the camera has finished reading the previous frame.

Note

A device is allowed to report a different latency every time latency is called. For example, for a spatial light modulator that only refreshes at a fixed rate, we can add the remaining time until the next refresh to the latency.

property lookup_table: Sequence[int]

Lookup table that is used to map the wrapped phase range of 0-2pi to gray values

The gray values are represented in the range from 0 to 2**bit_depth - 1). For an 8-bit video mode, this is 0-255. By default, a linear lookup table is set: range(2**bit_depth - 1).

Note: lookup table need not contain 2**bit_depth elements. A typical scenario is to use something like slm.lookup_table=range(142) to map the 0-2pi range to only the first 142 gray values.

property monitor_id: int

Number of the monitor (1 for primary screen, 2 for secondary screen, etc.) for the SLM.

Each monitor can only hold a single full-screen window. Use monitor_id=SLM.WINDOWED to show a windowed SLM on the primary screen (for debugging and monitoring purposes). There can be multiple windowed SLMs on the primary screen, but there cannot also be a fullscreen SLM on the primary screen at the same time.

monitor_id can be modified at run time, in which case the current SLM window is replaced by a new window on a different monitor. When moving the SLM to a different window, width, height and refresh_rate are set

patches: list[Patch]
property period: Annotated[Quantity, Unit('ms')]

The period of the refresh rate in milliseconds (read only).

property phases: Detector

Returns an object to monitor the phase pattern last sent to the SLM.

This is the pattern before applying the lookup table.

property pixels: Detector

Returns a ‘camera’ to monitor the current value of the pixels displayed on the SLM.

property position: tuple[int, int]

The position of the top-left corner of the SLM window as (y, x) screen coordinates.

Note

This property is ignored for full-screen SLMs.

primary_patch
property refresh_rate: Annotated[Quantity, Unit('Hz')]

Refresh rate of the SLM in Hz (read only).

Note

The refresh rate cannot be modified after the SLM is created. When moving the SLM to a different monitor (see monitor_id), the refresh rate is changed to the current video mode on that monitor. Note that OpenGL specifies this value as an integer, whereas some SLMs support non-integer refresh rates. It is always best to not specify the refresh rate and set the video mode in the operating system before creating the SLM object.

set_phases(values, update=True)[source]

Sets the phase pattern on the SLM.

Parameters:
  • values (ArrayLike) – phase pattern, in radians. The pattern is automatically stretched to fill the full SLM.

  • update – when True, calls update after setting the phase pattern. Set to False to suppress the call to update. This is useful in advanced scenarios where multiple parameters of the SLM need to be changed before updating the displayed image.

property shape: tuple[int, int]

Shape (height × width) of the window in pixels.

Limitations :
  • For windowed-mode SLMs, the size cannot be modified.

  • When moving the SLM to a different monitor (see monitor_id), the SLM is sized to match the current

    resolution on that monitor. Note that this value may differ from the value passed as input, because the input value is specified in screen coordinates, whereas the reported width is in pixels. In this case, the original value of shape will be lost.

  • The transform property is not updated automatically, so if the aspect ratio changes

    the transform needs to be set again.

property transform: Transform

Global transformation matrix

The transform determines how the vertex coordinates that make up the shape of a Patch (see Patch) are mapped to the standard coordinate system. In turn, the coordinate_system property determines how this coordinate system is mapped to the SLM window. By default, this value is just the identity transformation Transform().

update()[source]

Sends the new phase pattern to be displayed on the SLM.

This function waits for the vsync, and returns directly after it. Therefore, it can be used as software synchronization to the SLM.

Note

This function does not wait for the image to appear on the SLM. To wait for the image stabilization explicitly, use ‘wait()’. However, this should rarely be needed since all Detectors already call wait_finished before starting a measurement.

Note

At the moment, update() blocks until all OpenGL commands are processed, and a vertical retrace occurs (i.e., the hardware signals the start of a new frame). This behavior may change in the future and should not be relied on. Instead, use the automatic synchronization mechanism to synchronize detectors with the SLM hardware.

circular(radii, segments_per_ring, edge_count=256, center=(0, 0))[source]

Creates a circular geometry with the specified extent.

This geometry maps a texture to a disk or a ring. It can be used for feedback-based wavefront shaping to match the segment size to the illumination profile of the SLM. [1]

When used with a linear texture of shape (1, np.sum(segments_per_ring) (see set_phases), this geometry maps the pixels of the texture to concentric rings, starting with the innermost ring, and distributing the phase values counter-clockwise, starting at the positive x-axis. Each ring will hold the specified segments_per_ring.

Notes

When radii[0] == 0 , a disk is created. When radii[0] > 0, the center is left open, creating a ring

Parameters:
  • radii (Sequence[float]) – The radii of the rings in the circular geometry. The first value is the inner radius of the inner ring, the last value is the outer radius of the outer ring.

  • center (CoordinateType) – The center of the circle with respect to the origin, specified in (y, x) coordinates. Default value is (0, 0).

  • segments_per_ring (Sequence[int]) – The number of segments for each ring. The number of segments is one less than the number of rings.

  • edge_count (int) – The number of edge points to approximate the full circle. The more edges, the closer the geometry will approximate a circle. Default value is 256.

[1]: Mastiani, Bahareh, and Ivo M. Vellekoop. “Noise-tolerant wavefront shaping in a Hadamard basis.” Optics express 29.11 (2021): 17534-17541.

rectangle(extent, center=(0, 0))[source]

Creates a rectangle geometry with the specified extent.

Parameters:
  • extent (ExtentType) – The extent (height, width) of the rectangle. Default value is (2, 2). If a single value is specified, the same value is used for both axes.

  • center (CoordinateType) – The center of the rectangle with respect to the origin, specified in (y, x) coordinates. Default value is (0, 0).