.. _section-slms: Spatial Light Modulators ============================== Spatial Light Modulators are the heart of any wavefront shaping experiment. Currently, OpenWFS supports the use of phase-only spatial light modulators through the following simple interface: .. code-block:: python class PhaseSLM(ABC): def set_phases(self, values: ArrayLike, update: bool = True) def update(self) The :meth:`~.PhaseSLM.set_phases()` method takes a scalar or a 2-D array of phase values in radians, which is wrapped to the range [0, 2π) and displayed on the SLM. This function calls :meth:`~.PhaseSLM.update()` by default to send the image to the SLM hardware. In more advanced scenarios, like texture blending (see below), it can be useful to postpone the update by passing ``update=False`` and manually cal :meth:`~.PhaseSLM.update()` later. The algorithms in OpenWFS only access SLMs through this simple interface. As a result, the details of the SLM hardware are decoupled from the wavefront shaping algorithm itself. Currently, there are two implementations of the :class:`~.PhaseSLM` interface. The :class:`simulation.SLM` is used for simulating experiments and for testing algorithms (see :numref:`section-simulations`). The :class:`hardware.SLM` is an OpenGL-accelerated controller for using a phase-only SLM that is connected to the video output of a computer. The SLM can be created in windowed mode (useful for debugging), or full screen. It is possible to have multiple windowed SLMs on the same monitor, but only one full-screen SLM per monitor. In addition, the SLM implements some advanced features that are discussed below. At the time of writing, SLMs that are controlled through other interfaces than the video output are not supported. However, the interface of the :class:`~.PhaseSLM` class is designed to accommodate these devices in the future. Through this interface, support for intensity-only light modulators (e.g. Digital Mirror Devices) operating in phase-modulation mode (e.g. :cite:`conkey2012high`) may also be added. Texture mapping and blending ----------------------------------- .. _slmdemo: .. figure:: slm_disk.png :align: center :scale: 50% Sample output of the SLM object, generated by the script ``examples/slm_disk.py``. Here, two patches were used: a circular one with large segments in concentric rings, and a second one showing a superposed phase gradient. On top of the basic functionality, the :class:`hardware.SLM` object provides advanced functionality for controlling how the pixels in the phase array are mapped to the screen. This functionality uses the texture mapping capabilities of the graphics card (see, e.g. :cite:`neider1993opengl`) to allow for arbitrary transformations of phase maps to the screen. Texture mapping involves two components: a texture and a geometry, which are stored together in a :class:`~.hardware.SLM.Patch` object. The *texture* is a 2-D array holding phase values in radians. Values in the texture are referenced by texture coordinates ranging from 0 to 1. The *geometry* describes a set of triangles that is drawn to the screen, with each triangle holding a 2-D screen coordinate and a 2-D texture coordinate. The screen coordinate determines where the vertex is drawn on the screen, and the texture coordinate determines which pixel in the texture is used to color the vertex. When drawing the triangles, OpenGL automatically interpolates the texture coordinates between the vertices, and looks up the nearest value in the phase texture. In the simplest form, a square texture is mapped to a square region on the screen. This region is comprised of two triangles, with the screen coordinates corresponding to the vertices of the square. The vertices hold texture coordinates ranging from (0,0) to (1,1). This way, the graphics hardware automatically scales the texture to fit the region, regardless of how many elements the phase map has. A more advanced example is shown in :numref:`slmdemo`, where the texture is mapped to a disk. The disk is drawn as a set of triangles, with the screen coordinates corresponding to points on the concentric rings that form the disk. In this example, the texture was a 1 × 18 element array with random values. The texture coordinates were defined such that the elements of this array are mapped to three concentric rings, consisting of 4, 6, and 8 segments, respectively (see :numref:`slmcode`). Such an approach can be useful for equalizing the contribution of different segments on the SLM :cite:`mastiani2021noise`. The example in :numref:`slmdemo` also demonstrates a second capability of the SLM object, namely using multiple patches simultaneously on a single SLM. The patches are drawn in the order they are present in the :attr:`~.slm.SLM.patches` list. At the pixels where the patches overlap, the phase values for the two patches are added and wrapped to the interval [0, 2π). In the example, the first patch describes the disk, and a second square patch was drawn on top of it. This second patch holds a linear gradient, which may be used to steer the light coming from the SLM.LM, while the disk texture determines the shape of the wavefront. This blending behavior can be disabled by setting :attr:`~.Patch.additive_blend` ``= False``, in which case each patch just overwrites the pixels drawn by previous patches. .. _slmcode: .. literalinclude:: ../../examples/slm_disk.py :language: python :caption: ``slm_disk``. Illustration of texture warping and blending functionality of the :class:`hardware.SLM` object. The combination of texture mapping and blending allows for a wide range of use cases, including: - Aligning the size and position of a square phase map with the illuminating beam. - Correcting phase maps for distortions in the optical system, such as barrel distortion. - Using two parts of the same SLM independently. This feature is possible because each Patch object can independently be used as a :class:`~.PhaseSLM` object. - Blocking part of a wavefront by drawing a different patch on top of it, with :attr:`~.Patch.additive_blend` ``= False``. - Modifying an existing wavefront by adding a gradient or defocus pattern. - Compensating for curvature in the SLM and other system aberrations by adding an offset layer with :attr:`~.Patch.additive_blend` ``= True`` to compensate for these aberrations. All of these corrections can be done in real time using OpenGL acceleration, making the SLM object a versatile tool for wavefront shaping experiments. A final aspect of the SLM that is demonstrated in the example is the use of the :attr:`~.slm.SLM.pixels` attribute. This attribute holds a virtual camera that reads the gray values of the pixels currently displayed on the SLM. This virtual camera implements the :class:`~.Detector` interface, meaning that it can be used just like an actual camera. This feature is useful, e.g., for storing or checking the images displayed on the SLM. For debugging or demonstration purposes, it is often useful to receive feedback on the image displayed on the SLM. In Windows, this image can be see by hovering over the program icon in the task bar. Alternatively, the combination Ctrl + PrtScn can be used to grab the image on all active monitors. For demonstration purposes, the :func:`~.slm.SLM.clone` function can be used to create a second SLM window (typically placed in a corner of the primary screen), which shows the same image as the original SLM. This technique is demonstrated in the ``wfs_demonstration_experimental.py`` code available in the online example gallery :cite:`readthedocsOpenWFS`. Lookup table --------------------------------------- Even though the SLM hardware itself often includes a hardware lookup table, there usually is no standard way to set it from Python, making switching between lookup tables cumbersome. The OpenGL-accelerated lookup table in the SLM object provides a solution to this problem, which is especially useful when working with tunable lasers, for which the lookup table needs to be adjusted often. The SLM object has a :attr:`~.slm.SLM.lookup_table` property, which holds a table that is used to convert phase values from radians to gray values on the screen. By default, this table is set to ``range(2 ** bit_depth)``, meaning that if an 8-bit video mode is selected, a phase of 0 produces a gray value of 0, and a phase of 255/256·2π produces a gray value of 255. A phase of 2π again produces a gray value of 0. .. _slm-synchronization: Synchronization ------------------------------------ When working with an SLM that is connected to a video output, it is essential to synchronize with the vertical retrace of the graphics card. 'Vertical retrace' is the historical name for the start of a new frame. The software should update the image on the screen only during this vertical retrace. If the image is changed between vertical retraces, *tearing* will occur, meaning that the SLM will show part of the old frame and part of the new frame simultaneously. The :class:`~.hardware.SLM` object uses OpenGL to synchronize to the vertical retrace of the graphics port. It uses the standard technique to avoid tearing: when updating the SLM, the image is first written to an invisible back buffer, which is swapped with the visible front buffer during the vertical retrace event.