Skip to content

Reference

This page contains a detailed description of all exported methods.

SLM

SLMDisplay

A class to control a Spatial Light Modulator (SLM). This class uses multiprocessing to manage the display in a separate process. When using this class in Python scripts (not imported modules), you must protect the instantiation with an if __name__ == '__main__': guard to prevent errors on macOS and Windows:

Example
import slmcontrol

if __name__ == '__main__':
    slm = slmcontrol.SLMDisplay()
    # ... use the SLM
    slm.close()
This guard is NOT required when:
  • Using in Jupyter notebooks or IPython
  • Importing and using in modules (not the main script)
  • Running via test frameworks (pytest, unittest)

Attributes:

Name Type Description
monitor_id int

The ID of the monitor to use.

width int

The width of the SLM.

height int

The height of the SLM.

Source code in src/slmcontrol/slm.py
class SLMDisplay:
    """
    A class to control a Spatial Light Modulator (SLM).
    This class uses multiprocessing to manage the display in a separate process.
    When using this class in Python scripts (not imported modules), you must protect
    the instantiation with an `if __name__ == '__main__':` guard to prevent errors
    on macOS and Windows:

    Example:
        ```python
        import slmcontrol

        if __name__ == '__main__':
            slm = slmcontrol.SLMDisplay()
            # ... use the SLM
            slm.close()
        ```

    Note: This guard is NOT required when:
        - Using in Jupyter notebooks or IPython
        - Importing and using in modules (not the main script)
        - Running via test frameworks (pytest, unittest)

    Attributes:
        monitor_id (int): The ID of the monitor to use.
        width (int): The width of the SLM.
        height (int): The height of the SLM.
    """

    def __init__(self, monitor_id: int = -1) -> None:
        """
        Initialize the SLM instance.

        Args:
            monitor_id (int): The ID of the monitor to use. Defaults to the last monitor.
        """
        assert monitor_id not in used_ids, (
            "SLMDisplay instance already exists for this monitor."
        )
        used_ids.append(monitor_id)
        self.monitor_id = monitor_id
        self.window_name = f"SLM Display - Monitor {monitor_id}"
        self.monitor = screeninfo.get_monitors()[monitor_id]
        self.height = self.monitor.height
        self.width = self.monitor.width

        # Create two shared memory buffers for double buffering
        buffer_size = self.height * self.width
        self.buffer_0 = SharedMemory(create=True, size=buffer_size)
        self.buffer_1 = SharedMemory(create=True, size=buffer_size)

        # Create numpy array views for both buffers
        self._array_0 = np.ndarray(
            (self.height, self.width), dtype=np.uint8, buffer=self.buffer_0.buf
        )
        self._array_1 = np.ndarray(
            (self.height, self.width), dtype=np.uint8, buffer=self.buffer_1.buf
        )

        # Initialize both buffers to black
        self._array_0.fill(0)
        self._array_1.fill(0)

        # Current write buffer index (0 or 1) - only used in the main process
        self._write_buffer_idx = 0

        # Queue to pass buffer indices to the display process (maxsize=1: latest frame wins)
        self._frame_queue: Queue = Queue(maxsize=1)

        # Event to signal shutdown
        self._shutdown = Event()

        self.process = Process(target=self.run)
        self.process.start()

    def run(self) -> None:
        # Attach to both shared memory buffers by name
        buffer_0 = SharedMemory(name=self.buffer_0.name)
        buffer_1 = SharedMemory(name=self.buffer_1.name)

        # Create numpy array views for both buffers
        array_0 = np.ndarray(
            (self.height, self.width), dtype=np.uint8, buffer=buffer_0.buf
        )
        array_1 = np.ndarray(
            (self.height, self.width), dtype=np.uint8, buffer=buffer_1.buf
        )

        cv.namedWindow(self.window_name, cv.WINDOW_NORMAL)
        cv.imshow(self.window_name, array_0)
        cv.moveWindow(self.window_name, self.monitor.x, self.monitor.y)
        cv.setWindowProperty(
            self.window_name, cv.WND_PROP_FULLSCREEN, cv.WINDOW_FULLSCREEN
        )
        cv.waitKey(1)

        while not self._shutdown.is_set():
            try:
                buffer_idx = self._frame_queue.get(timeout=0.05)
                cv.imshow(self.window_name, array_0 if buffer_idx == 0 else array_1)
            except Empty:
                pass
            cv.waitKey(1)

        # Clean up
        cv.destroyWindow(self.window_name)
        buffer_0.close()
        buffer_1.close()

    def updateArray(
        self, holo: NDArray[np.uint8], sleep_time: float | int = 0.15
    ) -> None:
        """
        Update the hologram displayed on the SLM.

        Args:
            holo: A 2D matrix of UInt8 values representing the hologram.
            sleep_time (float | int): Time to sleep after updating (in seconds) to allow display to refresh.
                       Set to 0 for maximum throughput (no waiting).
        """
        assert holo.shape == (self.height, self.width), "Invalid hologram shape."

        # Write to the back buffer (opposite of what was last sent to the display)
        next_idx = 1 - self._write_buffer_idx
        np.copyto(self._array_0 if next_idx == 0 else self._array_1, holo)
        self._write_buffer_idx = next_idx

        # Send the new buffer index to the display process; if the previous frame
        # hasn't been consumed yet, replace it so the display always shows the latest
        try:
            self._frame_queue.put_nowait(next_idx)
        except Full:
            try:
                self._frame_queue.get_nowait()
            except Empty:
                pass
            self._frame_queue.put_nowait(next_idx)

        # Optional sleep to allow the display to update
        if sleep_time > 0:
            sleep(sleep_time)

    def close(self) -> None:
        """
        Close the SLM window.
        """
        assert self.monitor_id in used_ids, (
            "SLMDisplay instance not found for this monitor."
        )

        # Signal the child process to shutdown
        self._shutdown.set()
        self.process.join(timeout=2.0)
        if self.process.is_alive():
            self.process.terminate()

        # Clean up both shared memory buffers
        self.buffer_0.close()
        self.buffer_0.unlink()
        self.buffer_1.close()
        self.buffer_1.unlink()

        # Remove monitor_id from used_ids
        used_ids.remove(self.monitor_id)

__init__(monitor_id=-1)

Initialize the SLM instance.

Parameters:

Name Type Description Default
monitor_id int

The ID of the monitor to use. Defaults to the last monitor.

-1
Source code in src/slmcontrol/slm.py
def __init__(self, monitor_id: int = -1) -> None:
    """
    Initialize the SLM instance.

    Args:
        monitor_id (int): The ID of the monitor to use. Defaults to the last monitor.
    """
    assert monitor_id not in used_ids, (
        "SLMDisplay instance already exists for this monitor."
    )
    used_ids.append(monitor_id)
    self.monitor_id = monitor_id
    self.window_name = f"SLM Display - Monitor {monitor_id}"
    self.monitor = screeninfo.get_monitors()[monitor_id]
    self.height = self.monitor.height
    self.width = self.monitor.width

    # Create two shared memory buffers for double buffering
    buffer_size = self.height * self.width
    self.buffer_0 = SharedMemory(create=True, size=buffer_size)
    self.buffer_1 = SharedMemory(create=True, size=buffer_size)

    # Create numpy array views for both buffers
    self._array_0 = np.ndarray(
        (self.height, self.width), dtype=np.uint8, buffer=self.buffer_0.buf
    )
    self._array_1 = np.ndarray(
        (self.height, self.width), dtype=np.uint8, buffer=self.buffer_1.buf
    )

    # Initialize both buffers to black
    self._array_0.fill(0)
    self._array_1.fill(0)

    # Current write buffer index (0 or 1) - only used in the main process
    self._write_buffer_idx = 0

    # Queue to pass buffer indices to the display process (maxsize=1: latest frame wins)
    self._frame_queue: Queue = Queue(maxsize=1)

    # Event to signal shutdown
    self._shutdown = Event()

    self.process = Process(target=self.run)
    self.process.start()

updateArray(holo, sleep_time=0.15)

Update the hologram displayed on the SLM.

Parameters:

Name Type Description Default
holo NDArray[uint8]

A 2D matrix of UInt8 values representing the hologram.

required
sleep_time float | int

Time to sleep after updating (in seconds) to allow display to refresh. Set to 0 for maximum throughput (no waiting).

0.15
Source code in src/slmcontrol/slm.py
def updateArray(
    self, holo: NDArray[np.uint8], sleep_time: float | int = 0.15
) -> None:
    """
    Update the hologram displayed on the SLM.

    Args:
        holo: A 2D matrix of UInt8 values representing the hologram.
        sleep_time (float | int): Time to sleep after updating (in seconds) to allow display to refresh.
                   Set to 0 for maximum throughput (no waiting).
    """
    assert holo.shape == (self.height, self.width), "Invalid hologram shape."

    # Write to the back buffer (opposite of what was last sent to the display)
    next_idx = 1 - self._write_buffer_idx
    np.copyto(self._array_0 if next_idx == 0 else self._array_1, holo)
    self._write_buffer_idx = next_idx

    # Send the new buffer index to the display process; if the previous frame
    # hasn't been consumed yet, replace it so the display always shows the latest
    try:
        self._frame_queue.put_nowait(next_idx)
    except Full:
        try:
            self._frame_queue.get_nowait()
        except Empty:
            pass
        self._frame_queue.put_nowait(next_idx)

    # Optional sleep to allow the display to update
    if sleep_time > 0:
        sleep(sleep_time)

close()

Close the SLM window.

Source code in src/slmcontrol/slm.py
def close(self) -> None:
    """
    Close the SLM window.
    """
    assert self.monitor_id in used_ids, (
        "SLMDisplay instance not found for this monitor."
    )

    # Signal the child process to shutdown
    self._shutdown.set()
    self.process.join(timeout=2.0)
    if self.process.is_alive():
        self.process.terminate()

    # Clean up both shared memory buffers
    self.buffer_0.close()
    self.buffer_0.unlink()
    self.buffer_1.close()
    self.buffer_1.unlink()

    # Remove monitor_id from used_ids
    used_ids.remove(self.monitor_id)

Hologram

generate_hologram(relative, two_pi_modulation, x_period, y_period, method='BesselJ1')

Generate a hologram used to produce the desired output.

Parameters:

Name Type Description Default
relative NDArray

The relative field. This is the desired output field divided by the input field. When the input field is a plane wave, this reduces to desired output field.

required
two_pi_modulation int

The greyscale value corresponding to a 2 pi phase shift.

required
x_period int

The period (in pixels) of the diffraction grating in the x direction.

required
y_period int

The period (in pixels) of the diffraction grating in the y direction.

required
method str

Hologram calculation method. Possible values are: 1. 'BesselJ1': Type 3 of reference [1] or method F of reference [2]

Defaults to 'BesselJ1'.
'BesselJ1'

Returns:

Type Description
NDArray[uint8]

NDArray[np.uint8]: The hologram.

References:

[1] Victor Arrizón, Ulises Ruiz, Rosibel Carrada, and Luis A. González,
    "Pixelated phase computer holograms for the accurate encoding of scalar complex fields,"
    J. Opt. Soc. Am. A 24, 3500-3507 (2007)

[2] Thomas W. Clark, Rachel F. Offer, Sonja Franke-Arnold, Aidan S. Arnold, and Neal Radwell,
    "Comparison of beam generation techniques using a phase only spatial light modulator,"
    Opt. Express 24, 6249-6264 (2016)
Source code in src/slmcontrol/hologram.py
def generate_hologram(
    relative: NDArray,
    two_pi_modulation: int,
    x_period: int,
    y_period: int,
    method: str = "BesselJ1",
) -> NDArray[np.uint8]:
    """
    Generate a hologram used to produce the desired output.

    Args:
        relative (NDArray): The relative field. This is the desired output field divided by the input field. When the input field is a plane wave, this reduces to desired output field.
        two_pi_modulation (int): The greyscale value corresponding to a 2 pi phase shift.
        x_period (int): The period (in pixels) of the diffraction grating in the x direction.
        y_period (int): The period (in pixels) of the diffraction grating in the y direction.
        method (str, optional): Hologram calculation method.
            Possible values are:
                1. 'BesselJ1': Type 3 of reference [1] or method F of reference [2]

                Defaults to 'BesselJ1'.

    Returns:
        NDArray[np.uint8]: The hologram.

     References:

        [1] Victor Arrizón, Ulises Ruiz, Rosibel Carrada, and Luis A. González,
            "Pixelated phase computer holograms for the accurate encoding of scalar complex fields,"
            J. Opt. Soc. Am. A 24, 3500-3507 (2007)

        [2] Thomas W. Clark, Rachel F. Offer, Sonja Franke-Arnold, Aidan S. Arnold, and Neal Radwell,
            "Comparison of beam generation techniques using a phase only spatial light modulator,"
            Opt. Express 24, 6249-6264 (2016)
    """
    abs_relative = np.abs(relative)
    phase_relative = np.angle(relative)
    M = np.max(abs_relative)
    x, y = np.meshgrid(
        np.arange(relative.shape[1]), np.arange(relative.shape[0]), sparse=True
    )

    if method == "BesselJ1":
        holo = inv_j1(x_max_besselj1 * abs_relative / M) * np.sin(
            2 * np.pi * (x / x_period + y / y_period) + phase_relative
        )

        return np.astype(
            np.round(two_pi_modulation * 0.586 * (holo / y_max_besselj1 + 1) / 2),
            np.uint8,
        )
    else:
        raise ValueError(f"Unknown hologram generation method: {method}")

Structures

lg(x, y, p=0, l=0, w=1)

Compute the Laguerre-Gaussian mode.

Parameters:

Name Type Description Default
x NDArray

x argument

required
y NDArray

y argument

required
p int

radial index

0
l int

azymutal index

0
w int | float

waist

1

Returns:

Type Description
NDArray

Laguerre-Gaussian mode.

Source code in src/slmcontrol/structures.py
def lg(x: NDArray, y: NDArray, p: int = 0, l: int = 0, w: int | float = 1) -> NDArray:  # noqa: E741
    """Compute the Laguerre-Gaussian mode.

    Args:
        x (NDArray): x argument
        y (NDArray): y argument
        p (int): radial index
        l (int): azymutal index
        w (int | float): waist

    Returns:
        (NDArray): Laguerre-Gaussian mode.
    """
    r2 = x**2 + y**2
    phi = np.arctan2(y, x)
    return (
        np.exp(-r2 / w**2 + 1j * l * phi)
        * genlaguerre(p, abs(l))(2 * r2 / w**2)
        * np.sqrt((2 * r2 / w**2) ** abs(l))
    )

hg(x, y, m=0, n=0, w=1)

Compute the Hermite-Gaussian mode.

Parameters:

Name Type Description Default
x NDArray

x argument

required
y NDArray

y argument

required
m int

vertical index

0
n int

horizontal index

0
w int | float

waist

1

Returns:

Type Description
NDArray

Hermite-Gaussian mode.

Source code in src/slmcontrol/structures.py
def hg(x: NDArray, y: NDArray, m: int = 0, n: int = 0, w: int | float = 1) -> NDArray:
    """Compute the Hermite-Gaussian mode.

    Args:
        x (NDArray): x argument
        y (NDArray): y argument
        m (int): vertical index
        n (int): horizontal index
        w (int | float): waist

    Returns:
        (NDArray): Hermite-Gaussian mode.
    """
    return (
        np.exp(-(x**2 + y**2) / w**2)
        * hermite(m)(np.sqrt(2) * x / w)
        * hermite(n)(np.sqrt(2) * y / w)
    )

diagonal_hg(x, y, m=0, n=0, w=1)

Compute the diagonal Hermite-Gaussian mode.

Parameters:

Name Type Description Default
x NDArray

x argument

required
y NDArray

y argument

required
m int

diagonal index

0
n int

anti-diagonal index

0
w int | float

waist

1

Returns:

Type Description
NDArray

diagonal Hermite-Gaussian mode.

Source code in src/slmcontrol/structures.py
def diagonal_hg(
    x: NDArray, y: NDArray, m: int = 0, n: int = 0, w: int | float = 1
) -> NDArray:
    """Compute the diagonal Hermite-Gaussian mode.

    Args:
        x (NDArray): x argument
        y (NDArray): y argument
        m (int): diagonal index
        n (int): anti-diagonal index
        w (int | float): waist

    Returns:
        (NDArray): diagonal Hermite-Gaussian mode.
    """
    return (
        np.exp(-(x**2 + y**2) / w**2)
        * hermite(m)((x + y) / w)
        * hermite(n)((x - y) / w)
    )

lens(x, y, fx, fy, k=1)

Compute the phase imposed by a lens.

Parameters:

Name Type Description Default
x NDArray

x argument

required
y NDArray

y argument

required
fx int | float

focal length in the x direction

required
fy int | float

focal length in the y direction

required
k int | float

wavenumber of incoming beam

1

Returns:

Type Description
NDArray

phase imposed by the lens.

Source code in src/slmcontrol/structures.py
def lens(
    x: NDArray,
    y: NDArray,
    fx: int | float,
    fy: int | float,
    k: int | float = 1,
) -> NDArray:
    """Compute the phase imposed by a lens.

    Args:
        x (NDArray): x argument
        y (NDArray): y argument
        fx (int | float): focal length in the x direction
        fy (int | float): focal length in the y direction
        k (int | float): wavenumber of incoming beam

    Returns:
        (NDArray): phase imposed by the lens.
    """
    return np.exp(-1j * k * (x**2 / (2 * fx) + y**2 / (2 * fy)))

Masks

rectangular_aperture(x, y, a, b)

Rectangular aperture centered at the origin.

Parameters:

Name Type Description Default
x NDArray

x argument

required
y NDArray

y argument

required
a int | float

lenght in the horizontal direction

required
b int | float

lenght in the vertical direction

required

Returns:

Type Description
NDArray

True if the point is inside the aperture. False otherwise.

Source code in src/slmcontrol/masks.py
def rectangular_aperture(
    x: NDArray, y: NDArray, a: int | float, b: int | float
) -> NDArray[np.bool]:
    """Rectangular aperture centered at the origin.

    Args:
        x (NDArray): x argument
        y (NDArray): y argument
        a (int | float): lenght in the horizontal direction
        b (int | float): lenght in the vertical direction

    Returns:
        (NDArray): True if the point is inside the aperture. False otherwise.
    """
    return (np.abs(x) <= a / 2) & (np.abs(y) <= b / 2)

square(x, y, L)

Square apperture centered at the origin.

Parameters:

Name Type Description Default
x NDArray

x argument

required
y NDArray

y argument

required
L int | float

side length

required

Returns:

Type Description
NDArray

True if the point is inside the apperture. False otherwise.

Source code in src/slmcontrol/masks.py
def square(x: NDArray, y: NDArray, L: int | float) -> NDArray[np.bool]:
    """Square apperture centered at the origin.

    Args:
        x (NDArray): x argument
        y (NDArray): y argument
        L (int | float): side length

    Returns:
        (NDArray): True if the point is inside the apperture. False otherwise.
    """
    return rectangular_aperture(x, y, L, L)

single_slit(x, y, a)

Single vertical slit.

Parameters:

Name Type Description Default
x NDArray

x argument

required
y NDArray

y argument

required
a int | float

slit width

required

Returns:

Type Description
NDArray

True if the point is inside the slit. False otherwise.

Source code in src/slmcontrol/masks.py
def single_slit(x: NDArray, y: NDArray, a: int | float) -> NDArray[np.bool]:
    """Single vertical slit.

    Args:
        x (NDArray): x argument
        y (NDArray): y argument
        a (int | float): slit width

    Returns:
        (NDArray): True if the point is inside the slit. False otherwise.
    """
    return rectangular_aperture(x, y, a, np.inf)

double_slit(x, y, a, d)

Double vertical slit.

Parameters:

Name Type Description Default
x NDArray

x argument

required
y NDArray

y argument

required
a int | float

slit width

required
d int | float

slit separation

required

Returns:

Type Description
NDArray

True if the point is inside the slits. False otherwise.

Source code in src/slmcontrol/masks.py
def double_slit(
    x: NDArray,
    y: NDArray,
    a: int | float,
    d: int | float,
) -> NDArray[np.bool]:
    """Double vertical slit.

    Args:
        x (NDArray): x argument
        y (NDArray): y argument
        a (int | float): slit width
        d (int | float): slit separation

    Returns:
        (NDArray): True if the point is inside the slits. False otherwise.
    """
    return single_slit(x - d / 2, y, a) | single_slit(x + d / 2, y, a)

pupil(x, y, radius)

Circular pupil centered at the origin.

Parameters:

Name Type Description Default
x NDArray

x argument

required
y NDArray

y argument

required
radius int | float

radius of the pupil

required

Returns:

Type Description
NDArray

True if the point is inside the pupil. False otherwise.

Source code in src/slmcontrol/masks.py
def pupil(x: NDArray, y: NDArray, radius: int | float) -> NDArray[np.bool]:
    """Circular pupil centered at the origin.

    Args:
        x (NDArray): x argument
        y (NDArray): y argument
        radius (int | float): radius of the pupil

    Returns:
        (NDArray): True if the point is inside the pupil. False otherwise.
    """
    return x**2 + y**2 <= radius**2

triangle(x, y, side_length)

Equilateral triangular apperture centered at the origin.

Parameters:

Name Type Description Default
x NDArray

x argument

required
y NDArray

y argument

required
side_length int | float

side length

required

Returns:

Type Description
NDArray

True if the point is inside the apperture. False otherwise.

Source code in src/slmcontrol/masks.py
def triangle(x: NDArray, y: NDArray, side_length: int | float) -> NDArray[np.bool]:
    """Equilateral triangular apperture centered at the origin.

    Args:
        x (NDArray): x argument
        y (NDArray): y argument
        side_length (int | float): side length

    Returns:
        (NDArray): True if the point is inside the apperture. False otherwise.
    """
    sqrt3 = np.sqrt(3)
    return (y > -side_length / 2 / sqrt3) & (np.abs(x) < -y / sqrt3 + side_length / 3)