Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 34 additions & 70 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,99 +1,63 @@
pymodaq_plugins_template
pymodaq_plugins_slmsuite
########################

.. the following must be adapted to your developed package, links to pypi, github description...

.. image:: https://img.shields.io/pypi/v/pymodaq_plugins_template.svg
:target: https://pypi.org/project/pymodaq_plugins_template/
.. image:: https://img.shields.io/pypi/v/pymodaq_plugins_slmsuite.svg
:target: https://pypi.org/project/pymodaq_plugins_slmsuite/
:alt: Latest Version

.. image:: https://readthedocs.org/projects/pymodaq/badge/?version=latest
:target: https://pymodaq.readthedocs.io/en/stable/?badge=latest
:alt: Documentation Status

.. image:: https://github.com/PyMoDAQ/pymodaq_plugins_template/workflows/Upload%20Python%20Package/badge.svg
:target: https://github.com/PyMoDAQ/pymodaq_plugins_template
.. image:: https://github.com/PyMoDAQ/pymodaq_plugins_slmsuite/workflows/Upload%20Python%20Package/badge.svg
:target: https://github.com/PyMoDAQ/pymodaq_plugins_slmsuite
:alt: Publication Status

.. image:: https://github.com/PyMoDAQ/pymodaq_plugins_template/actions/workflows/Test.yml/badge.svg
:target: https://github.com/PyMoDAQ/pymodaq_plugins_template/actions/workflows/Test.yml


Use this template to create a repository on your account and start the development of your own PyMoDAQ plugin!

This branch targets **PyMoDAQ 5.3.x** (Python 3.10 to 3.13). Use the branch matching your PyMoDAQ version.

Development
===========

* Install your plugin in editable mode: ``pip install -e .``
* Check it, without any hardware: ``check_plugin`` (and ``pytest``)
* If you use an AI coding assistant, point it to ``AGENTS.md``: it describes the patterns to follow for instrument
plugins, Dashboard extensions (``CustomExt``, whenever instruments are involved) and standalone apps
(``CustomApp``, only if no instruments are needed).
PyMoDAQ plugin giving access to the spatial light modulators (SLM) supported by
`slmsuite <https://github.com/slmsuite/slmsuite>`_ (Holoeye, Meadowlark, Santec, Hamamatsu, mirrored screens, ...).
It is meant to be used with the BeamShaping extension of
`pymodaq_plugins_beam_shaping <https://github.com/PyMoDAQ/pymodaq_plugins_beam_shaping>`_, but any Dashboard can use
the actuators.

This branch targets **PyMoDAQ 5.3.x** (Python 3.10 to 3.13).

Authors
=======

* First Author (myemail@xxx.org)
* Other author (myotheremail@xxx.org)

.. if needed use this field

Contributors
============

* First Contributor
* Other Contributors

.. if needed use this field

Depending on the plugin type, delete/complete the fields below
* Sebastien J. Weber (sebastien.weber@cemes.fr)


Instruments
===========

Below is the list of instruments included in this plugin

Actuators
+++++++++

* **yyy**: control of yyy actuators
* **xxx**: control of xxx actuators

Viewer0D
++++++++

* **yyy**: control of yyy 0D detector
* **xxx**: control of xxx 0D detector

Viewer1D
++++++++

* **yyy**: control of yyy 1D detector
* **xxx**: control of xxx 1D detector


Viewer2D
++++++++
All the actuators have the same interface: the target is a 2D array of the shape of the SLM (or a single value spread
over the SLM). It is displayed as a **phase in radians**, converted to grey levels by slmsuite (wrapping, look-up table,
wavefront correction if one was loaded), or as **raw grey levels** if the ``Input as grey levels`` setting is checked or
if the data carries the attribute ``as_grey_levels=True`` (as ``DataShaper`` of BeamShaping does). This is also the
interface of the Holoeye plugin and of the mock *Shaper* of BeamShaping.

* **yyy**: control of yyy 2D detector
* **xxx**: control of xxx 2D detector
* **Slmsuite**: generic, choose the slmsuite class (``SLM type``) and give its constructor arguments as a Python
dictionary (``Arguments``)
* **SlmsuiteSimulated**: simulated SLM of slmsuite, no hardware needed
* **SlmsuiteMeadowlark**: Meadowlark SLM (Windows only, Blink SDK)
* **SlmsuiteHoloeye**: Holoeye SLM (SLM Display SDK)

Another SLM supported by slmsuite with specific settings can be added by subclassing
``pymodaq_plugins_slmsuite.resources.daq_move_SlmsuiteBase.DAQ_Move_SlmsuiteBase``.

PID Models
==========
The working wavelength (``Wavelength (nm)`` setting, default in the configuration file of the plugin) is given to
slmsuite to convert phases into grey levels.

Status
++++++

Extensions
==========
Tested against the simulated SLM of slmsuite only (``pytest``). The Meadowlark and Holoeye actuators have not been
validated on real devices yet.


Installation instructions
=========================

* PyMoDAQ’s version.
* Operating system’s version.
* What manufacturer’s drivers should be installed to make this plugin run?
* PyMoDAQ 5.3.x, slmsuite >= 0.5.0 (installed with this plugin)
* The vendor SDK of your SLM has to be installed separately, see the
`slmsuite documentation <https://slmsuite.readthedocs.io/>`_ for each manufacturer.
* Development: ``pip install -e .``, then ``check_plugin`` and ``pytest`` (needs ``pytest-qt``)
14 changes: 6 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,24 @@ h5exporters = false # true if plugin contains custom h5 file exporters
scanners = false # true if plugin contains custom scan layout (daq_scan extensions)

[urls]
package-url = 'https://github.com/PyMoDAQ/pymodaq_plugins_template' #todo modify url by your plugin url
package-url = 'https://github.com/PyMoDAQ/pymodaq_plugins_slmsuite'

[project]
name = "pymodaq_plugins_template" #todo modify template by your plugin short name
description = 'some word about your plugin'
name = "pymodaq_plugins_slmsuite"
description = "PyMoDAQ plugin giving access to the spatial light modulators (SLM) supported by the slmsuite package"
dependencies = [
"pymodaq>=5.3.2",
"pymodaq_utils>=5.3.1",
"pymodaq_gui>=5.3.0",
"pymodaq_data>=5.3.0",
#todo: list here all dependencies your package may have
"slmsuite>=0.5.0",
]

authors = [
{name = "Name Surname", email = "myname@test.fr"},
#todo: list here all authors of your plugin
{name = "Weber Sébastien", email = "sebastien.weber@cemes.fr"},
]
maintainers = [
{name = "Name Surname", email = "myname@test.fr"},
#todo: list here all maintainers of your plugin
{name = "Weber Sébastien", email = "sebastien.weber@cemes.fr"},
]

# nottodo: leave everything below as is!
Expand Down
36 changes: 36 additions & 0 deletions src/pymodaq_plugins_slmsuite/daq_move_plugins/daq_move_Slmsuite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from pymodaq.control_modules.move_utility_classes import main, comon_parameters_fun

from pymodaq_plugins_slmsuite.hardware.slm_controller import SLM_CLASSES
from pymodaq_plugins_slmsuite.resources.daq_move_SlmsuiteBase import (DAQ_Move_SlmsuiteBase, parse_kwargs)


class DAQ_Move_Slmsuite(DAQ_Move_SlmsuiteBase):
"""Actuator displaying a phase (or grey levels) on any SLM supported by slmsuite

Choose the slmsuite class in ``SLM type`` and give its constructor arguments as a Python dictionary in
``Arguments``, for instance ``{'slm_number': 1}`` for a Meadowlark or ``{'preselect': 'index:0'}`` for a Holoeye. See
the documentation of slmsuite.hardware.slms for the arguments of each class. The vendor SDKs have to be installed
separately (see the slmsuite documentation).

Tested only against the simulated SLM of slmsuite, not against real devices.
"""
params = ([
{'title': 'SLM type:', 'name': 'slm_type', 'type': 'list', 'limits': list(SLM_CLASSES),
'value': 'SimulatedSLM'},
{'title': 'Arguments:', 'name': 'arguments', 'type': 'str', 'value': "{'resolution': (1920, 1080)}",
'tip': 'Python dictionary of keyword arguments for the constructor of the slmsuite class'},
] + DAQ_Move_SlmsuiteBase.common_slm_params +
comon_parameters_fun(DAQ_Move_SlmsuiteBase.is_multiaxes, DAQ_Move_SlmsuiteBase._axis_names,
epsilon=DAQ_Move_SlmsuiteBase._epsilons))

def get_slm_type(self) -> str:
return self.settings['slm_type']

def get_slm_kwargs(self) -> dict:
kwargs = super().get_slm_kwargs()
kwargs.update(parse_kwargs(self.settings['arguments']))
return kwargs


if __name__ == '__main__':
main(__file__, init=False)
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from pymodaq.control_modules.move_utility_classes import main, comon_parameters_fun

from pymodaq_plugins_slmsuite.resources.daq_move_SlmsuiteBase import DAQ_Move_SlmsuiteBase


class DAQ_Move_SlmsuiteHoloeye(DAQ_Move_SlmsuiteBase):
"""Holoeye SLM (PLUTO, LUNA, GAEA...) driven through slmsuite.hardware.slms.holoeye.Holoeye

The Holoeye SLM Display SDK has to be installed (see the slmsuite documentation). The plugin pymodaq_plugins_holoeye
does the same job with the SDK directly.

Not tested yet on a real device.
"""
slm_type = 'Holoeye'
params = ([
{'title': 'Preselect:', 'name': 'preselect', 'type': 'str', 'value': 'index:0',
'tip': 'Holoeye selection string, for instance index:0, name:pluto;serial:0001 or serial:2220-0011'},
] + DAQ_Move_SlmsuiteBase.common_slm_params +
comon_parameters_fun(DAQ_Move_SlmsuiteBase.is_multiaxes, DAQ_Move_SlmsuiteBase._axis_names,
epsilon=DAQ_Move_SlmsuiteBase._epsilons))

def get_slm_kwargs(self) -> dict:
kwargs = super().get_slm_kwargs()
kwargs['preselect'] = self.settings['preselect']
return kwargs


if __name__ == '__main__':
main(__file__, init=False)
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from pymodaq.control_modules.move_utility_classes import main, comon_parameters_fun

from pymodaq_plugins_slmsuite.resources.daq_move_SlmsuiteBase import DAQ_Move_SlmsuiteBase


class DAQ_Move_SlmsuiteMeadowlark(DAQ_Move_SlmsuiteBase):
"""Meadowlark Optics SLM driven through slmsuite.hardware.slms.meadowlark.Meadowlark

The Blink SDK of Meadowlark has to be installed (see the slmsuite documentation, the SDK is Windows only).

The connection (HDMI or PCIe) is detected by slmsuite from the installed SDK, set the SDK path if several
SDKs are installed (by default the most recent one is used).

Not tested yet on a real device.
"""
slm_type = 'Meadowlark'
supported_platforms = ('win32',) # the Blink SDK driven by slmsuite is a Windows DLL
params = ([
{'title': 'Board number:', 'name': 'slm_number', 'type': 'int', 'value': 1, 'min': 1,
'tip': 'Board number of the SLM, in case of several PCIe SLMs. Ignored for HDMI SLMs'},
{'title': 'SDK path:', 'name': 'sdk_path', 'type': 'browsepath', 'value': '', 'filetype': False,
'tip': 'Folder of the Blink SDK installation. Leave empty to use the default of slmsuite'},
{'title': 'LUT file:', 'name': 'lut_path', 'type': 'browsepath', 'value': '', 'filetype': True,
'tip': 'Look-up table file of the SLM. Leave empty to use the default of slmsuite'},
] + DAQ_Move_SlmsuiteBase.common_slm_params +
comon_parameters_fun(DAQ_Move_SlmsuiteBase.is_multiaxes, DAQ_Move_SlmsuiteBase._axis_names,
epsilon=DAQ_Move_SlmsuiteBase._epsilons))

def get_slm_kwargs(self) -> dict:
kwargs = super().get_slm_kwargs()
kwargs['slm_number'] = self.settings['slm_number']
for name in ('sdk_path', 'lut_path'):
if self.settings[name]:
kwargs[name] = self.settings[name]
return kwargs


if __name__ == '__main__':
main(__file__, init=False)
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from pymodaq.control_modules.move_utility_classes import main, comon_parameters_fun

from pymodaq_plugins_slmsuite.resources.daq_move_SlmsuiteBase import DAQ_Move_SlmsuiteBase


class DAQ_Move_SlmsuiteSimulated(DAQ_Move_SlmsuiteBase):
"""Simulated SLM (slmsuite.hardware.slms.simulated.SimulatedSLM): no hardware needed

Useful to test a Dashboard, or an extension such as BeamShaping, without a real SLM.
"""
slm_type = 'SimulatedSLM'
params = ([
{'title': 'Width (pixels):', 'name': 'sim_width', 'type': 'int', 'value': 1920, 'min': 1},
{'title': 'Height (pixels):', 'name': 'sim_height', 'type': 'int', 'value': 1080, 'min': 1},
{'title': 'Pixel pitch (um):', 'name': 'pitch_um', 'type': 'float', 'value': 8., 'min': 0.1},
] + DAQ_Move_SlmsuiteBase.common_slm_params +
comon_parameters_fun(DAQ_Move_SlmsuiteBase.is_multiaxes, DAQ_Move_SlmsuiteBase._axis_names,
epsilon=DAQ_Move_SlmsuiteBase._epsilons))

def get_slm_kwargs(self) -> dict:
kwargs = super().get_slm_kwargs()
kwargs.update(resolution=(self.settings['sim_width'], self.settings['sim_height']),
pitch_um=(self.settings['pitch_um'], self.settings['pitch_um']))
return kwargs


if __name__ == '__main__':
main(__file__, init=False)
72 changes: 72 additions & 0 deletions src/pymodaq_plugins_slmsuite/hardware/slm_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Thin wrapper around the SLM classes of slmsuite.

All the plugins of this package talk to an :class:`SlmController` instead of the slmsuite objects, so that they can be
tested with a fake controller and so that the vendor specific imports (SDKs that are often available on one platform
only) stay lazy.
"""
from importlib import import_module
from typing import Any

import numpy as np

# name shown in the plugin -> (module, class) in slmsuite.hardware.slms
SLM_CLASSES: dict[str, tuple[str, str]] = {
'SimulatedSLM': ('slmsuite.hardware.slms.simulated', 'SimulatedSLM'),
'Holoeye': ('slmsuite.hardware.slms.holoeye', 'Holoeye'),
'Meadowlark': ('slmsuite.hardware.slms.meadowlark', 'Meadowlark'),
'Santec': ('slmsuite.hardware.slms.santec', 'Santec'),
'Hamamatsu': ('slmsuite.hardware.slms.hamamatsu', 'Hamamatsu'),
'ScreenMirrored': ('slmsuite.hardware.slms.screenmirrored', 'ScreenMirrored'),
}


def get_slm_class(slm_type: str) -> type:
"""Import (lazily) and return the slmsuite class corresponding to a name of ``SLM_CLASSES``"""
try:
module_name, class_name = SLM_CLASSES[slm_type]
except KeyError:
raise ValueError(f'Unknown SLM type {slm_type}, should be one of {list(SLM_CLASSES)}') from None
return getattr(import_module(module_name), class_name)


class SlmController:
"""Open one slmsuite SLM and send phase or grey level images to it

Parameters
----------
slm_type: str
One of the keys of ``SLM_CLASSES``
**kwargs:
Passed to the constructor of the slmsuite class (``wav_um``, ``pitch_um``, ``resolution``, ...)
"""

def __init__(self, slm_type: str, **kwargs: Any):
self.slm_type = slm_type
self.slm = get_slm_class(slm_type)(**kwargs)

@property
def shape(self) -> tuple[int, int]:
"""The shape (height, width) of the SLM in pixels"""
return tuple(int(s) for s in self.slm.shape)

@property
def bitresolution(self) -> int:
"""The number of grey levels of the SLM"""
return int(self.slm.bitresolution)

def show_phase(self, phase: np.ndarray, phase_correct: bool = True):
"""Display a phase in radians, converted to grey levels by slmsuite (wrapping, LUT, phase correction...)"""
self.slm.set_phase(np.asarray(phase, dtype=float), phase_correct=phase_correct, settle=False)

def show_grey_levels(self, grey: np.ndarray):
"""Display grey levels directly, bypassing the phase conversion (values are rounded and clipped)"""
grey = np.clip(np.rint(np.asarray(grey, dtype=float)), 0, self.bitresolution - 1)
self.slm.set_phase(grey.astype(self.slm.dtype), settle=False)

def get_phase(self) -> np.ndarray:
"""The phase (radians) really displayed by the SLM, as a numpy array"""
phase = self.slm.phase
return np.asarray(phase.get() if hasattr(phase, 'get') else phase)

def close(self):
self.slm.close()
4 changes: 4 additions & 0 deletions src/pymodaq_plugins_slmsuite/resources/config_template.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
title = 'configuration file of the pymodaq_plugins_slmsuite plugin'

[slm]
wavelength_nm = 515 # working wavelength, given to slmsuite (wav_um) to convert phases (rad) into grey levels
Loading
Loading