From 757ac2a3ba8a0991bc2930bab6b4def3dfd83264 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 21:05:54 +0000 Subject: [PATCH 1/4] Add generic slmsuite SLM actuator with simulated, Meadowlark and Holoeye variants Rename the plugin template to pymodaq_plugins_slmsuite and add a DAQ_Move family driving any slmsuite SLM with the same phase / grey level interface as the Holoeye plugin and the BeamShaping mock shaper. Tested against the slmsuite simulated SLM only. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01CAhZwmbyCSpSgfTyicUxuS --- README.rst | 104 +++----- pyproject.toml | 14 +- .../__init__.py | 0 .../daq_move_plugins/__init__.py | 0 .../daq_move_plugins/daq_move_Slmsuite.py | 36 +++ .../daq_move_SlmsuiteHoloeye.py | 29 +++ .../daq_move_SlmsuiteMeadowlark.py | 36 +++ .../daq_move_SlmsuiteSimulated.py | 28 +++ .../exporters/__init__.py | 0 .../hardware}/__init__.py | 0 .../hardware/slm_controller.py | 72 ++++++ .../models/__init__.py | 0 .../resources}/__init__.py | 0 .../resources/config_template.toml | 4 + .../resources/daq_move_SlmsuiteBase.py | 144 +++++++++++ .../scanners}/__init__.py | 0 .../utils.py | 0 .../app/custom_app_template.py | 130 ---------- .../daq_move_plugins/daq_move_Template.py | 231 ------------------ .../daq_viewer_plugins/__init__.py | 3 - .../daq_viewer_plugins/plugins_0D/__init__.py | 13 - .../plugins_0D/daq_0Dviewer_Template.py | 160 ------------ .../daq_viewer_plugins/plugins_1D/__init__.py | 13 - .../plugins_1D/daq_1Dviewer_Template.py | 167 ------------- .../daq_viewer_plugins/plugins_2D/__init__.py | 14 -- .../plugins_2D/daq_2Dviewer_Template.py | 170 ------------- .../daq_viewer_plugins/plugins_ND/__init__.py | 13 - .../extensions/custom_extension_template.py | 163 ------------ .../resources/__init__.py | 0 .../resources/config_template.toml | 2 - .../scanners/__init__.py | 6 - tests/example_mock_plugins.py | 115 --------- tests/test_plugin_behaviour.py | 94 ------- tests/test_slmsuite_plugins.py | 158 ++++++++++++ 34 files changed, 547 insertions(+), 1372 deletions(-) rename src/{pymodaq_plugins_template => pymodaq_plugins_slmsuite}/__init__.py (100%) rename src/{pymodaq_plugins_template => pymodaq_plugins_slmsuite}/daq_move_plugins/__init__.py (100%) create mode 100644 src/pymodaq_plugins_slmsuite/daq_move_plugins/daq_move_Slmsuite.py create mode 100644 src/pymodaq_plugins_slmsuite/daq_move_plugins/daq_move_SlmsuiteHoloeye.py create mode 100644 src/pymodaq_plugins_slmsuite/daq_move_plugins/daq_move_SlmsuiteMeadowlark.py create mode 100644 src/pymodaq_plugins_slmsuite/daq_move_plugins/daq_move_SlmsuiteSimulated.py rename src/{pymodaq_plugins_template => pymodaq_plugins_slmsuite}/exporters/__init__.py (100%) rename src/{pymodaq_plugins_template/app => pymodaq_plugins_slmsuite/hardware}/__init__.py (100%) create mode 100644 src/pymodaq_plugins_slmsuite/hardware/slm_controller.py rename src/{pymodaq_plugins_template => pymodaq_plugins_slmsuite}/models/__init__.py (100%) rename src/{pymodaq_plugins_template/hardware => pymodaq_plugins_slmsuite/resources}/__init__.py (100%) create mode 100644 src/pymodaq_plugins_slmsuite/resources/config_template.toml create mode 100644 src/pymodaq_plugins_slmsuite/resources/daq_move_SlmsuiteBase.py rename src/{pymodaq_plugins_template/extensions => pymodaq_plugins_slmsuite/scanners}/__init__.py (100%) rename src/{pymodaq_plugins_template => pymodaq_plugins_slmsuite}/utils.py (100%) delete mode 100644 src/pymodaq_plugins_template/app/custom_app_template.py delete mode 100644 src/pymodaq_plugins_template/daq_move_plugins/daq_move_Template.py delete mode 100644 src/pymodaq_plugins_template/daq_viewer_plugins/__init__.py delete mode 100644 src/pymodaq_plugins_template/daq_viewer_plugins/plugins_0D/__init__.py delete mode 100644 src/pymodaq_plugins_template/daq_viewer_plugins/plugins_0D/daq_0Dviewer_Template.py delete mode 100644 src/pymodaq_plugins_template/daq_viewer_plugins/plugins_1D/__init__.py delete mode 100644 src/pymodaq_plugins_template/daq_viewer_plugins/plugins_1D/daq_1Dviewer_Template.py delete mode 100644 src/pymodaq_plugins_template/daq_viewer_plugins/plugins_2D/__init__.py delete mode 100644 src/pymodaq_plugins_template/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Template.py delete mode 100644 src/pymodaq_plugins_template/daq_viewer_plugins/plugins_ND/__init__.py delete mode 100644 src/pymodaq_plugins_template/extensions/custom_extension_template.py delete mode 100644 src/pymodaq_plugins_template/resources/__init__.py delete mode 100644 src/pymodaq_plugins_template/resources/config_template.toml delete mode 100644 src/pymodaq_plugins_template/scanners/__init__.py delete mode 100644 tests/example_mock_plugins.py delete mode 100644 tests/test_plugin_behaviour.py create mode 100644 tests/test_slmsuite_plugins.py diff --git a/README.rst b/README.rst index b369419..5ff552f 100644 --- a/README.rst +++ b/README.rst @@ -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 `_ (Holoeye, Meadowlark, Santec, Hamamatsu, mirrored screens, ...). +It is meant to be used with the BeamShaping extension of +`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 `_ for each manufacturer. +* Development: ``pip install -e .``, then ``check_plugin`` and ``pytest`` (needs ``pytest-qt``) diff --git a/pyproject.toml b/pyproject.toml index c8de085..ddb9bee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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! diff --git a/src/pymodaq_plugins_template/__init__.py b/src/pymodaq_plugins_slmsuite/__init__.py similarity index 100% rename from src/pymodaq_plugins_template/__init__.py rename to src/pymodaq_plugins_slmsuite/__init__.py diff --git a/src/pymodaq_plugins_template/daq_move_plugins/__init__.py b/src/pymodaq_plugins_slmsuite/daq_move_plugins/__init__.py similarity index 100% rename from src/pymodaq_plugins_template/daq_move_plugins/__init__.py rename to src/pymodaq_plugins_slmsuite/daq_move_plugins/__init__.py diff --git a/src/pymodaq_plugins_slmsuite/daq_move_plugins/daq_move_Slmsuite.py b/src/pymodaq_plugins_slmsuite/daq_move_plugins/daq_move_Slmsuite.py new file mode 100644 index 0000000..edef135 --- /dev/null +++ b/src/pymodaq_plugins_slmsuite/daq_move_plugins/daq_move_Slmsuite.py @@ -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) diff --git a/src/pymodaq_plugins_slmsuite/daq_move_plugins/daq_move_SlmsuiteHoloeye.py b/src/pymodaq_plugins_slmsuite/daq_move_plugins/daq_move_SlmsuiteHoloeye.py new file mode 100644 index 0000000..c241b3b --- /dev/null +++ b/src/pymodaq_plugins_slmsuite/daq_move_plugins/daq_move_SlmsuiteHoloeye.py @@ -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) diff --git a/src/pymodaq_plugins_slmsuite/daq_move_plugins/daq_move_SlmsuiteMeadowlark.py b/src/pymodaq_plugins_slmsuite/daq_move_plugins/daq_move_SlmsuiteMeadowlark.py new file mode 100644 index 0000000..eaf0506 --- /dev/null +++ b/src/pymodaq_plugins_slmsuite/daq_move_plugins/daq_move_SlmsuiteMeadowlark.py @@ -0,0 +1,36 @@ +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). + + 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'}, + {'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) diff --git a/src/pymodaq_plugins_slmsuite/daq_move_plugins/daq_move_SlmsuiteSimulated.py b/src/pymodaq_plugins_slmsuite/daq_move_plugins/daq_move_SlmsuiteSimulated.py new file mode 100644 index 0000000..631de93 --- /dev/null +++ b/src/pymodaq_plugins_slmsuite/daq_move_plugins/daq_move_SlmsuiteSimulated.py @@ -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) diff --git a/src/pymodaq_plugins_template/exporters/__init__.py b/src/pymodaq_plugins_slmsuite/exporters/__init__.py similarity index 100% rename from src/pymodaq_plugins_template/exporters/__init__.py rename to src/pymodaq_plugins_slmsuite/exporters/__init__.py diff --git a/src/pymodaq_plugins_template/app/__init__.py b/src/pymodaq_plugins_slmsuite/hardware/__init__.py similarity index 100% rename from src/pymodaq_plugins_template/app/__init__.py rename to src/pymodaq_plugins_slmsuite/hardware/__init__.py diff --git a/src/pymodaq_plugins_slmsuite/hardware/slm_controller.py b/src/pymodaq_plugins_slmsuite/hardware/slm_controller.py new file mode 100644 index 0000000..1b43b92 --- /dev/null +++ b/src/pymodaq_plugins_slmsuite/hardware/slm_controller.py @@ -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() diff --git a/src/pymodaq_plugins_template/models/__init__.py b/src/pymodaq_plugins_slmsuite/models/__init__.py similarity index 100% rename from src/pymodaq_plugins_template/models/__init__.py rename to src/pymodaq_plugins_slmsuite/models/__init__.py diff --git a/src/pymodaq_plugins_template/hardware/__init__.py b/src/pymodaq_plugins_slmsuite/resources/__init__.py similarity index 100% rename from src/pymodaq_plugins_template/hardware/__init__.py rename to src/pymodaq_plugins_slmsuite/resources/__init__.py diff --git a/src/pymodaq_plugins_slmsuite/resources/config_template.toml b/src/pymodaq_plugins_slmsuite/resources/config_template.toml new file mode 100644 index 0000000..f2e9cc2 --- /dev/null +++ b/src/pymodaq_plugins_slmsuite/resources/config_template.toml @@ -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 diff --git a/src/pymodaq_plugins_slmsuite/resources/daq_move_SlmsuiteBase.py b/src/pymodaq_plugins_slmsuite/resources/daq_move_SlmsuiteBase.py new file mode 100644 index 0000000..6fd8ee7 --- /dev/null +++ b/src/pymodaq_plugins_slmsuite/resources/daq_move_SlmsuiteBase.py @@ -0,0 +1,144 @@ +import ast +from typing import Any + +import numpy as np + +from pymodaq.control_modules.move_utility_classes import (DAQ_Move_base, comon_parameters_fun, DataActuatorType, + DataActuator) +from pymodaq_gui.parameter import Parameter +from pymodaq_utils.utils import ThreadCommand +from pymodaq_utils.logger import set_logger, get_module_name + +from pymodaq_plugins_slmsuite import config +from pymodaq_plugins_slmsuite.hardware.slm_controller import SlmController + +logger = set_logger(get_module_name(__file__)) + + +class DAQ_Move_SlmsuiteBase(DAQ_Move_base): + """Base class of the actuators driving a spatial light modulator (SLM) through slmsuite + + The target of a move is a 2D array (``DataActuator`` with one 2D array, or one value that is spread over the whole + SLM). It is displayed as a phase in radians, converted to grey levels by slmsuite (wrapping, lookup table, phase + correction of the wavefront if one was loaded), or, if the ``as_grey_levels`` attribute of the data is True (as + ``DataShaper`` of pymodaq_plugins_beam_shaping does) or the ``Input as grey levels`` setting is checked, as raw grey + levels. This is the same interface as the one of the Holoeye plugin and of the mock *Shaper* of BeamShaping. + + Subclasses define ``slm_type`` (a key of ``hardware.slm_controller.SLM_CLASSES``) and ``slm_params`` (the settings + specific to their SLM) and override ``get_slm_kwargs`` to turn these settings into constructor arguments. + """ + slm_type: str = None + slm_params: list = [] + + is_multiaxes = False + _axis_names = [''] + _controller_units = '' + _epsilons = 0.1 + data_actuator_type = DataActuatorType.DataActuator + has_encoder = False # the SLM cannot be read back: PyMoDAQ takes the target as the current value + + common_slm_params = [ + {'title': 'SLM Infos:', 'name': 'info', 'type': 'group', 'children': [ + {'title': 'Width:', 'name': 'width', 'type': 'int', 'value': 0, 'readonly': True}, + {'title': 'Height:', 'name': 'height', 'type': 'int', 'value': 0, 'readonly': True}, + {'title': 'Grey levels:', 'name': 'levels', 'type': 'int', 'value': 0, 'readonly': True}, + ]}, + {'title': 'Wavelength (nm):', 'name': 'wavelength_nm', 'type': 'float', + 'value': config('slm', 'wavelength_nm'), 'min': 1., + 'tip': 'Working wavelength, used by slmsuite to convert phases into grey levels. Needs a re-initialization.'}, + {'title': 'Apply phase correction:', 'name': 'phase_correct', 'type': 'bool', 'value': True, + 'tip': 'Add the wavefront correction loaded in slmsuite (if any) to the displayed phases'}, + {'title': 'Input as grey levels:', 'name': 'input_as_grey_levels', 'type': 'bool', 'value': False, + 'tip': 'Interpret the received arrays as grey levels instead of phases in radians'}, + ] + params = common_slm_params + comon_parameters_fun(is_multiaxes, _axis_names, epsilon=_epsilons) + + def ini_attributes(self): + self.controller: SlmController = None + + def get_slm_type(self) -> str: + """Name of the slmsuite SLM class to open (a key of SLM_CLASSES)""" + return self.slm_type + + def get_slm_kwargs(self) -> dict[str, Any]: + """Arguments of the constructor of the slmsuite class. To be extended by the subclasses.""" + return dict(wav_um=self.settings['wavelength_nm'] / 1000) + + def ini_stage(self, controller=None): + """Open the SLM (master) or use the shared controller (slave)""" + if self.is_master: + self.controller = SlmController(self.get_slm_type(), **self.get_slm_kwargs()) + else: + self.controller = controller + + height, width = self.controller.shape + self.settings.child('info', 'width').setValue(width) + self.settings.child('info', 'height').setValue(height) + self.settings.child('info', 'levels').setValue(self.controller.bitresolution) + + self.current_value = DataActuator(self._title, data=[np.zeros(self.controller.shape)]) + return f'{self.get_slm_type()} SLM {width}x{height} pixels', True + + def close(self): + if self.is_master and self.controller is not None: + self.controller.close() + + def commit_settings(self, param: Parameter): + pass + + def get_actuator_value(self) -> DataActuator: + """The phase (radians) that the SLM displays""" + return DataActuator(self._title, data=[self.controller.get_phase()]) + + def move_abs(self, value: DataActuator): + """Display the 2D array of ``value`` on the SLM + + Parameters + ---------- + value: DataActuator + One 2D array of the shape of the SLM (height, width), or one value spread over the whole SLM. + """ + array = np.asarray(value[0]) + if array.size == 1: + array = np.full(self.controller.shape, array.item()) + if array.shape != self.controller.shape: + raise ValueError(f'The array to display has shape {array.shape}, the SLM has {self.controller.shape}') + + self.target_value = value + as_grey = (self.settings['input_as_grey_levels'] or + ('as_grey_levels' in value.extra_attributes and bool(value.as_grey_levels))) + if as_grey: + self.controller.show_grey_levels(array) + else: + self.controller.show_phase(array, phase_correct=self.settings['phase_correct']) + self.emit_status(ThreadCommand('Update_Status', ['SLM updated'])) + + def absolute_difference_condition_to_reach_target(self) -> bool: + # the array sent is the target, once displayed it is reached: comparing it with the realized phase makes no + # sense (grey levels, wrapped or quantized phase) + return True + + def move_rel(self, value: DataActuator): + self.move_abs(self.current_value + value) + + def move_home(self): + """Display a null phase""" + self.controller.show_phase(np.zeros(self.controller.shape)) + self.move_done() + + def stop_motion(self): + self.move_done() + + +def parse_kwargs(text: str) -> dict[str, Any]: + """Parse the content of a free ``Arguments`` setting such as ``{'resolution': (1920, 1080)}`` + + Only Python literals are accepted (no code is evaluated) + """ + text = text.strip() + if not text: + return {} + kwargs = ast.literal_eval(text) + if not isinstance(kwargs, dict): + raise ValueError('The arguments should be a Python dictionary, for instance {"slm_number": 1}') + return kwargs diff --git a/src/pymodaq_plugins_template/extensions/__init__.py b/src/pymodaq_plugins_slmsuite/scanners/__init__.py similarity index 100% rename from src/pymodaq_plugins_template/extensions/__init__.py rename to src/pymodaq_plugins_slmsuite/scanners/__init__.py diff --git a/src/pymodaq_plugins_template/utils.py b/src/pymodaq_plugins_slmsuite/utils.py similarity index 100% rename from src/pymodaq_plugins_template/utils.py rename to src/pymodaq_plugins_slmsuite/utils.py diff --git a/src/pymodaq_plugins_template/app/custom_app_template.py b/src/pymodaq_plugins_template/app/custom_app_template.py deleted file mode 100644 index e29b851..0000000 --- a/src/pymodaq_plugins_template/app/custom_app_template.py +++ /dev/null @@ -1,130 +0,0 @@ -"""Template of a standalone application, for applications that need NO instruments. - -If your application drives instruments (actuators, detectors), write a Dashboard extension (CustomExt, see the -extensions folder) instead: it relies on the Dashboard's stable mechanisms to handle instruments (experiment and -state managers, modules manager...). -""" -from qtpy import QtWidgets - -from pymodaq_gui import utils as gutils -from pymodaq_utils.config import GlobalConfig -from pymodaq_utils.logger import set_logger, get_module_name - -logger = set_logger(get_module_name(__file__)) - -# all the configurations (PyMoDAQ's packages and the one of your plugin package) are read from GlobalConfig, for -# instance: config('gui', 'style', 'theme') -config = GlobalConfig() - - -# todo: modify the name of this class to reflect its application and change the name in the main -# method at the end of the script -class CustomAppTemplate(gutils.CustomApp): - - # todo: if you wish to create custom Parameter and corresponding widgets. These will be - # automatically added as children of self.settings. Morevover, the self.settings_tree will - # render the widgets in a Qtree. If you wish to see it in your app, add is into a Dock - params = [] - - def __init__(self, parent: gutils.DockArea): - super().__init__(parent) - - self.setup_ui() # calls, in this order: setup_docks_and_widgets, setup_menus_and_toolbars, - # setup_actions, connect_things and do_things_after_ui_setup - - def setup_docks_and_widgets(self): - """Method to be subclassed to setup the docks layout - - Examples - -------- - >>>self.docks['ADock'] = gutils.Dock('ADock name') - >>>self.dockarea.addDock(self.docks['ADock']) - >>>self.docks['AnotherDock'] = gutils.Dock('AnotherDock name') - >>>self.dockarea.addDock(self.docks['AnotherDock'''], 'bottom', self.docks['ADock']) - - See Also - -------- - pyqtgraph.dockarea.Dock - """ - # todo: create docks and add them here to hold your widgets. Here the settings tree in a dock - self.docks['settings'] = gutils.Dock('Settings') - self.dockarea.addDock(self.docks['settings']) - self.docks['settings'].addWidget(self.settings_tree) - - def setup_menus_and_toolbars(self, menubar: QtWidgets.QMenuBar = None): - """Non mandatory method to be subclassed in order to create menus and toolbars - - It is called before setup_actions: only create the (empty) menus and toolbars here, the actions are created - afterwards in setup_actions where they can be put in them, for instance: - - Examples - -------- - >>> self.add_menu('file', 'File', parent_menu=menubar) # here - >>> self.add_toolbar('main', 'Main toolbar', self.mainwindow) # here - >>> self.add_action('save', 'Save', 'SaveAs', "Save current data", menu='file', toolbar='main') # in setup_actions - - See Also - -------- - pymodaq_gui.managers.action_manager.ActionManager - """ - # todo create the menus and toolbars of your app here (they will be populated by the actions of setup_actions) - pass - - def setup_actions(self): - """Method where to create actions to be subclassed - - Examples - -------- - >>> self.add_action('quit', 'Quit', 'close2', "Quit program") - >>> self.add_action('grab', 'Grab', 'camera', "Grab from camera", checkable=True) - >>> self.add_action('load', 'Load', 'Open', "Load target file (.h5, .png, .jpg) or data from camera" - , checkable=False) - >>> self.add_action('save', 'Save', 'SaveAs', "Save current data", checkable=False) - - See Also - -------- - ActionManager.add_action - """ - # todo: replace this example action by yours - self.add_action('say_hello', 'Say hello', 'add_circle', tip='Show a message in the status bar') - - def connect_things(self): - """Connect actions and/or other widgets signal to methods""" - # todo: replace this example by your connections - self.connect_action('say_hello', lambda: self.update_status('Hello')) - - def value_changed(self, param): - """ Actions to perform when one of the param's value in self.settings is changed from the - user interface - - For instance: - if param.name() == 'do_something': - if param.value(): - print('Do something') - self.settings.child('main_settings', 'something_done').setValue(False) - - Parameters - ---------- - param: (Parameter) the parameter whose value just changed - """ - pass - - -def main(): - from pymodaq_gui.qt_utils import mkQApp - app = mkQApp('CustomApp') - - mainwindow = QtWidgets.QMainWindow() - dockarea = gutils.DockArea() - mainwindow.setCentralWidget(dockarea) - - # todo: change the name here to be the same as your app class - prog = CustomAppTemplate(dockarea) - - mainwindow.show() - - app.exec() - - -if __name__ == '__main__': - main() diff --git a/src/pymodaq_plugins_template/daq_move_plugins/daq_move_Template.py b/src/pymodaq_plugins_template/daq_move_plugins/daq_move_Template.py deleted file mode 100644 index 34777b5..0000000 --- a/src/pymodaq_plugins_template/daq_move_plugins/daq_move_Template.py +++ /dev/null @@ -1,231 +0,0 @@ - -from typing import Union, List, Dict -from pymodaq.control_modules.move_utility_classes import (DAQ_Move_base, comon_parameters_fun, - main, DataActuatorType, DataActuator) -from pymodaq.control_modules.daq_move_ui.utils import UiType - -from pymodaq_utils.utils import ThreadCommand # object used to send info back to the main thread -from pymodaq_gui.parameter import Parameter - -# TODO: -# Replace the following fake import with the import of the real Python wrapper of your instrument. Here we suppose that -# the wrapper is in the hardware directory, but it could come from an external librairy like pylablib or pymeasure. -from pymodaq_plugins_template.hardware.python_wrapper_file_of_your_instrument import PythonWrapperObjectOfYourInstrument - -# TODO: -# (1) change the name of the following class to DAQ_Move_TheNameOfYourChoice -# (2) change the name of this file to daq_move_TheNameOfYourChoice ("TheNameOfYourChoice" should be the SAME -# for the class name and the file name.) -# (3) this file should then be put into the right folder, namely IN THE FOLDER OF THE PLUGIN YOU ARE DEVELOPING: -# pymodaq_plugins_my_plugin/daq_move_plugins - - -class DAQ_Move_Template(DAQ_Move_base): - """ Instrument plugin class for an actuator. - - This object inherits all functionalities to communicate with PyMoDAQ’s DAQ_Move module through inheritance via - DAQ_Move_base. It makes a bridge between the DAQ_Move module and the Python wrapper of a particular instrument. - - TODO Complete the docstring of your plugin with: - * The set of controllers and actuators that should be compatible with this instrument plugin. - * With which instrument and controller it has been tested. - * The version of PyMoDAQ during the test. - * The version of the operating system. - * Installation instructions: what manufacturer’s drivers should be installed to make it run? - - Attributes: - ----------- - controller: object - The particular object that allow the communication with the hardware, in general a python wrapper around the - hardware library. - - # TODO add your particular attributes here if any - - """ - is_multiaxes = False # TODO for your plugin set to True if this plugin is controlled for a multiaxis controller - _axis_names: Union[List[str], Dict[str, int]] = ['Axis1', 'Axis2'] # TODO for your plugin: complete the list - _controller_units: Union[str, List[str]] = 'mm' # TODO for your plugin: put the correct unit here, it could be - # TODO a single str (the same one is applied to all axes) or a list of str (as much as the number of axes) - _epsilons: Union[float, List[float]] = 0.1 # TODO replace this by a value that is correct depending on your controller - # TODO it could be a single float of a list of float (as much as the number of axes) - # _epsilons is the initial default value for the epsilon parameter allowing pymodaq to know if the controller reached - # the target value. It is the developer responsibility to put here a meaningful value - - - data_actuator_type = DataActuatorType.DataActuator # keep this line: move_abs / move_rel receive a DataActuator (with - # units) and get_actuator_value returns one. The default of the PyMoDAQ base class is DataActuatorType.float, only - # kept for old plugins (a plain float in the axis unit is exchanged instead): do not remove this line, and do not - # use DataActuatorType.float in a new plugin. - - #todo: set the correct values for these two variables (pymodaq>5.3.0 only, if developing using lower version, just - # remove the two lines) - ui_type = UiType.NONE # should precise (force if possible) what should be the ui type to be used with this - # actuator. If NONE, PyMoDAQ will use the default ui type (see preferences). - has_encoder = True # tell PyMoDAQ if this actuator is able to set an absolute position and read the controller - # value. If False, you should consider having ui_type = UiType.RELATIVE - - - params = [ # TODO for your custom plugin: elements to be added here as dicts in order to control your custom stage - ] + comon_parameters_fun(axis_names=_axis_names) - - def ini_attributes(self): - # TODO declare the type of the wrapper (and assign it to self.controller) you're going to use for easy - # autocompletion - self.controller: PythonWrapperObjectOfYourInstrument = None - - #TODO declare here attributes you want/need to init with a default value - pass - - def ini_stage(self, controller=None): - """Actuator communication initialization - - Parameters - ---------- - controller: (object) - custom object of a PyMoDAQ plugin (Slave case). None if only one actuator by controller (Master case) - - Returns - ------- - info: str - initialized: bool - False if initialization failed otherwise True - """ - raise NotImplementedError # TODO when writing your own plugin remove this line and modify the ones below - if self.is_master: # is needed when controller is master - self.controller = PythonWrapperObjectOfYourInstrument() # TODO add the arguments for the instantiation - initialized = self.controller.a_method_or_atttribute_to_check_if_init() # todo - # todo: enter here whatever is needed for your controller initialization and eventual - # opening of the communication channel - else: - self.controller = controller - initialized = True - - info = "Whatever info you want to log" - return info, initialized - - def close(self): - """Terminate the communication protocol""" - ## TODO for your custom plugin - raise NotImplementedError # when writing your own plugin remove this line - # only the master owns the controller (a slave must not close the shared one) and ini_stage/ini_detector may - # have failed before creating it, so close can be called while self.controller is still None - if self.is_master and self.controller is not None: - # self.controller.your_method_to_terminate_the_communication() # when writing your own plugin replace this line - ... - - def get_actuator_value(self) -> DataActuator: - """Get the current value from the hardware with scaling conversion. - - Returns - ------- - float: The position obtained after scaling conversion. - """ - ## TODO for your custom plugin - raise NotImplementedError # when writing your own plugin remove this line - pos = DataActuator(data=self.controller.your_method_to_get_the_actuator_value(), # when writing your own plugin replace this line - units=self.axis_unit) - pos = self.get_position_with_scaling(pos) - return pos - - def user_condition_to_reach_target(self) -> bool: - """ Implement a condition for exiting the polling mechanism and specifying that the - target value has been reached - - Returns - ------- - bool: if True, PyMoDAQ considers the target value has been reached - """ - # TODO either delete this method if the usual polling is fine with you, but if need you can - # add here some other condition to be fullfilled either a completely new one or - # using or/and operations between the epsilon_bool and some other custom booleans - # for a usage example see DAQ_Move_brushlessMotor from the Thorlabs plugin - return True - - def move_abs(self, value: DataActuator): - """ Move the actuator to the absolute target defined by value - - Parameters - ---------- - value: DataActuator - Absolute target, with units. (With the backcompatibility DataActuatorType.float it would be a plain float, - already expressed in the axis unit, but new plugins should not use it.) - """ - - value = self.check_bound(value) #if user checked bounds, the defined bounds are applied here - self.target_value = value - value = self.set_position_with_scaling(value) # apply scaling if the user specified one - ## TODO for your custom plugin - # IMPORTANT: the controller only understands its own units. Always send value.value(self.axis_unit), it returns - # the magnitude expressed in the unit of this axis (the one of _controller_units, 'mm' here). PyMoDAQ already - # converts the target to the axis unit before calling move_abs, but value.value() alone is just the magnitude - # in whatever unit `value` carries (1 cm gives 1.0, not 10.0): value.value(self.axis_unit) guards against a - # mistake, and keeps the plugin correct when move_abs is called from elsewhere with another unit. - # (axis_unit is the unit of the current axis, axis_units the list/dict of the units of all the axes.) - raise NotImplementedError # when writing your own plugin remove this line - self.controller.your_method_to_set_an_absolute_value(value.value(self.axis_unit)) # when writing your own plugin replace this line - self.emit_status(ThreadCommand('Update_Status', ['Some info you want to log'])) - - def move_rel(self, value: DataActuator): - """ Move the actuator to the relative target actuator value defined by value - - Parameters - ---------- - value: DataActuator - Relative displacement, with units (convert it with value.value(self.axis_unit) before sending it to the - controller, see move_abs). - """ - value = self.check_bound(self.current_value + value) - self.current_value - self.target_value = value + self.current_value - value = self.set_position_relative_with_scaling(value) - - ## TODO for your custom plugin - # IMPORTANT: send value.value(self.axis_unit), not value.value(): the controller only understands its own units - raise NotImplementedError # when writing your own plugin remove this line - self.controller.your_method_to_set_a_relative_value(value.value(self.axis_unit)) # when writing your own plugin replace this line - self.emit_status(ThreadCommand('Update_Status', ['Some info you want to log'])) - - def move_home(self): - """Call the reference method of the controller - - Contrary to move_abs and move_rel, PyMoDAQ does not poll the position to know when the homing is finished. - Whether this method blocks until the actuator is at home or only starts the homing (asynchronous), the plugin - has to call self.move_done() when the homing process ends, otherwise PyMoDAQ keeps waiting for it. - """ - - ## TODO for your custom plugin - raise NotImplementedError # when writing your own plugin remove this line - self.controller.your_method_to_get_to_a_known_reference() # when writing your own plugin replace this line - self.emit_status(ThreadCommand('Update_Status', ['Some info you want to log'])) - self.move_done() # blocking homing: call it here. Asynchronous homing: call it once the controller reports the end - - def stop_motion(self): - """Stop the actuator and emits move_done signal""" - - ## TODO for your custom plugin - raise NotImplementedError # when writing your own plugin remove this line - self.controller.your_method_to_stop_positioning() # when writing your own plugin replace this line - self.emit_status(ThreadCommand('Update_Status', ['Some info you want to log'])) - - def commit_settings(self, param: Parameter): - """Apply the consequences of a change of value in the detector settings - - Parameters - ---------- - param: Parameter - A given parameter (within detector_settings) whose value has been changed by the user - """ - ## TODO for your custom plugin - if param.name() == 'axis': - self.axis_unit = self.controller.your_method_to_get_correct_axis_unit() - # do this only if you can and if the units are not known beforehand, for instance - # if the motors connected to the controller are of different type (mm, µm, nm, , etc...) - # see BrushlessDCMotor from the thorlabs plugin for an exemple - - elif param.name() == "a_parameter_you've_added_in_self.params": - self.controller.your_method_to_apply_this_param_change() - else: - pass - - -if __name__ == '__main__': - main(__file__) diff --git a/src/pymodaq_plugins_template/daq_viewer_plugins/__init__.py b/src/pymodaq_plugins_template/daq_viewer_plugins/__init__.py deleted file mode 100644 index b28b04f..0000000 --- a/src/pymodaq_plugins_template/daq_viewer_plugins/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/pymodaq_plugins_template/daq_viewer_plugins/plugins_0D/__init__.py b/src/pymodaq_plugins_template/daq_viewer_plugins/plugins_0D/__init__.py deleted file mode 100644 index f54d596..0000000 --- a/src/pymodaq_plugins_template/daq_viewer_plugins/plugins_0D/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -import importlib -from pathlib import Path -from ... import set_logger -logger = set_logger('viewer0D_plugins', add_to_console=False) - -for path in Path(__file__).parent.iterdir(): - try: - if '__init__' not in str(path): - importlib.import_module('.' + path.stem, __package__) - except Exception as e: - logger.warning("{:} plugin couldn't be loaded due to some missing packages or errors: {:}".format(path.stem, str(e))) - pass - diff --git a/src/pymodaq_plugins_template/daq_viewer_plugins/plugins_0D/daq_0Dviewer_Template.py b/src/pymodaq_plugins_template/daq_viewer_plugins/plugins_0D/daq_0Dviewer_Template.py deleted file mode 100644 index 3a75b1f..0000000 --- a/src/pymodaq_plugins_template/daq_viewer_plugins/plugins_0D/daq_0Dviewer_Template.py +++ /dev/null @@ -1,160 +0,0 @@ -import numpy as np - -from pymodaq_utils.utils import ThreadCommand -from pymodaq_data.data import DataToExport -from pymodaq_gui.parameter import Parameter - -from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters, main -from pymodaq.utils.data import DataFromPlugins - -# TODO: -# Replace the following fake import with the import of the real Python wrapper of your instrument. Here we suppose that -# the wrapper is in the hardware directory, but it could come from an external librairy like pylablib or pymeasure. -from pymodaq_plugins_template.hardware.python_wrapper_file_of_your_instrument import PythonWrapperObjectOfYourInstrument - -# TODO: -# (1) change the name of the following class to DAQ_0DViewer_TheNameOfYourChoice -# (2) change the name of this file to daq_0Dviewer_TheNameOfYourChoice ("TheNameOfYourChoice" should be the SAME -# for the class name and the file name.) -# (3) this file should then be put into the right folder, namely IN THE FOLDER OF THE PLUGIN YOU ARE DEVELOPING: -# pymodaq_plugins_my_plugin/daq_viewer_plugins/plugins_0D - - -class DAQ_0DViewer_Template(DAQ_Viewer_base): - """ Instrument plugin class for a OD viewer. - - This object inherits all functionalities to communicate with PyMoDAQ’s DAQ_Viewer module through inheritance via - DAQ_Viewer_base. It makes a bridge between the DAQ_Viewer module and the Python wrapper of a particular instrument. - - TODO Complete the docstring of your plugin with: - * The set of instruments that should be compatible with this instrument plugin. - * With which instrument it has actually been tested. - * The version of PyMoDAQ during the test. - * The version of the operating system. - * Installation instructions: what manufacturer’s drivers should be installed to make it run? - - Attributes: - ----------- - controller: object - The particular object that allow the communication with the hardware, in general a python wrapper around the - hardware library. - - # TODO add your particular attributes here if any - - """ - params = comon_parameters+[ - ## TODO for your custom plugin: elements to be added here as dicts in order to control your custom stage - ] - - def ini_attributes(self): - # TODO declare the type of the wrapper (and assign it to self.controller) you're going to use for easy - # autocompletion - self.controller: PythonWrapperObjectOfYourInstrument = None - - #TODO declare here attributes you want/need to init with a default value - pass - - def ini_detector(self, controller=None): - """Detector communication initialization - - Parameters - ---------- - controller: (object) - custom object of a PyMoDAQ plugin (Slave case). None if only one actuator/detector by controller - (Master case) - - Returns - ------- - info: str - initialized: bool - False if initialization failed otherwise True - """ - - raise NotImplementedError # TODO when writing your own plugin remove this line and modify the one below - if self.is_master: - self.controller = PythonWrapperObjectOfYourInstrument() #instantiate you driver with whatever arguments are needed - self.controller.open_communication() # call eventual methods - initialized = self.controller.a_method_or_atttribute_to_check_if_init() # TODO - else: - self.controller = controller - initialized = True - - # TODO for your custom plugin (optional) initialize viewers panel with the future type of data - self.dte_signal_temp.emit(DataToExport(name='myplugin', - data=[DataFromPlugins(name='Mock1', - data=[np.array([0]), np.array([0])], - dim='Data0D', - labels=['Mock1', 'label2'])])) - - info = "Whatever info you want to log" - return info, initialized - - def close(self): - """Terminate the communication protocol""" - ## TODO for your custom plugin - raise NotImplementedError # when writing your own plugin remove this line - # only the master owns the controller (a slave must not close the shared one) and ini_stage/ini_detector may - # have failed before creating it, so close can be called while self.controller is still None - if self.is_master and self.controller is not None: - # self.controller.your_method_to_terminate_the_communication() # when writing your own plugin replace this line - ... - - def grab_data(self, Naverage=1, **kwargs): - """Start a grab from the detector - - Parameters - ---------- - Naverage: int - Number of hardware averaging (if hardware averaging is possible, self.hardware_averaging should be set to - True in class preamble and you should code this implementation) - kwargs: dict - others optionals arguments - """ - ## TODO for your custom plugin: you should choose EITHER the synchrone or the asynchrone version following - - # synchrone version (blocking function) - raise NotImplementedError # when writing your own plugin remove this line - data_tot = self.controller.your_method_to_start_a_grab_snap() - self.dte_signal.emit(DataToExport(name='myplugin', - data=[DataFromPlugins(name='Mock1', data=data_tot, - dim='Data0D', labels=['dat0', 'data1'])])) - ######################################################### - - # asynchrone version (non-blocking function with callback) - raise NotImplementedError # when writing your own plugin remove this line - self.controller.your_method_to_start_a_grab_snap(self.callback) # when writing your own plugin replace this line - ######################################################### - - def callback(self): - """optional asynchrone method called when the detector has finished its acquisition of data""" - data_tot = self.controller.your_method_to_get_data_from_buffer() - self.dte_signal.emit(DataToExport(name='myplugin', - data=[DataFromPlugins(name='Mock1', data=data_tot, - dim='Data0D', labels=['dat0', 'data1'])])) - - def stop(self): - """Stop the current grab hardware wise if necessary""" - ## TODO for your custom plugin - raise NotImplementedError # when writing your own plugin remove this line - self.controller.your_method_to_stop_acquisition() # when writing your own plugin replace this line - self.emit_status(ThreadCommand('Update_Status', ['Some info you want to log'])) - ############################## - return '' - - def commit_settings(self, param: Parameter): - """Apply the consequences of a change of value in the detector settings - - Parameters - ---------- - param: Parameter - A given parameter (within detector_settings) whose value has been changed by the user - """ - ## TODO for your custom plugin - if param.name() == "a_parameter_you've_added_in_self.params": - self.controller.your_method_to_apply_this_param_change() # when writing your own plugin replace this line -# elif ... - ## - - -if __name__ == '__main__': - main(__file__) diff --git a/src/pymodaq_plugins_template/daq_viewer_plugins/plugins_1D/__init__.py b/src/pymodaq_plugins_template/daq_viewer_plugins/plugins_1D/__init__.py deleted file mode 100644 index 3fafded..0000000 --- a/src/pymodaq_plugins_template/daq_viewer_plugins/plugins_1D/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -import importlib -from pathlib import Path -from ... import set_logger -logger = set_logger('viewer1D_plugins', add_to_console=False) - -for path in Path(__file__).parent.iterdir(): - try: - if '__init__' not in str(path): - importlib.import_module('.' + path.stem, __package__) - except Exception as e: - logger.warning("{:} plugin couldn't be loaded due to some missing packages or errors: {:}".format(path.stem, str(e))) - pass - diff --git a/src/pymodaq_plugins_template/daq_viewer_plugins/plugins_1D/daq_1Dviewer_Template.py b/src/pymodaq_plugins_template/daq_viewer_plugins/plugins_1D/daq_1Dviewer_Template.py deleted file mode 100644 index 0f874f3..0000000 --- a/src/pymodaq_plugins_template/daq_viewer_plugins/plugins_1D/daq_1Dviewer_Template.py +++ /dev/null @@ -1,167 +0,0 @@ -import numpy as np - -from pymodaq_utils.utils import ThreadCommand -from pymodaq_data.data import DataToExport, Axis -from pymodaq_gui.parameter import Parameter - -from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters, main -from pymodaq.utils.data import DataFromPlugins - -# TODO: -# Replace the following fake import with the import of the real Python wrapper of your instrument. Here we suppose that -# the wrapper is in the hardware directory, but it could come from an external librairy like pylablib or pymeasure. -from pymodaq_plugins_template.hardware.python_wrapper_file_of_your_instrument import PythonWrapperObjectOfYourInstrument - -# TODO: -# (1) change the name of the following class to DAQ_1DViewer_TheNameOfYourChoice -# (2) change the name of this file to daq_1Dviewer_TheNameOfYourChoice ("TheNameOfYourChoice" should be the SAME -# for the class name and the file name.) -# (3) this file should then be put into the right folder, namely IN THE FOLDER OF THE PLUGIN YOU ARE DEVELOPING: -# pymodaq_plugins_my_plugin/daq_viewer_plugins/plugins_1D - - -class DAQ_1DViewer_Template(DAQ_Viewer_base): - """ Instrument plugin class for a 1D viewer. - - This object inherits all functionalities to communicate with PyMoDAQ’s DAQ_Viewer module through inheritance via - DAQ_Viewer_base. It makes a bridge between the DAQ_Viewer module and the Python wrapper of a particular instrument. - - TODO Complete the docstring of your plugin with: - * The set of instruments that should be compatible with this instrument plugin. - * With which instrument it has actually been tested. - * The version of PyMoDAQ during the test. - * The version of the operating system. - * Installation instructions: what manufacturer’s drivers should be installed to make it run? - - Attributes: - ----------- - controller: object - The particular object that allow the communication with the hardware, in general a python wrapper around the - hardware library. - - # TODO add your particular attributes here if any - - """ - params = comon_parameters+[ - ## TODO for your custom plugin - # elements to be added here as dicts in order to control your custom stage - ############ - ] - - def ini_attributes(self): - # TODO declare the type of the wrapper (and assign it to self.controller) you're going to use for easy - # autocompletion - self.controller: PythonWrapperObjectOfYourInstrument = None - - # TODO declare here attributes you want/need to init with a default value - - self.x_axis = None - - def ini_detector(self, controller=None): - """Detector communication initialization - - Parameters - ---------- - controller: (object) - custom object of a PyMoDAQ plugin (Slave case). None if only one actuator/detector by controller - (Master case) - - Returns - ------- - info: str - initialized: bool - False if initialization failed otherwise True - """ - - raise NotImplementedError # TODO when writing your own plugin remove this line and modify the one below - if self.is_master: - self.controller = PythonWrapperObjectOfYourInstrument() #instantiate you driver with whatever arguments are needed - self.controller.open_communication() # call eventual methods - initialized = self.controller.a_method_or_atttribute_to_check_if_init() # TODO - else: - self.controller = controller - initialized = True - - ## TODO for your custom plugin - # get the x_axis (you may want to to this also in the commit settings if x_axis may have changed - data_x_axis = self.controller.your_method_to_get_the_x_axis() # if possible - self.x_axis = Axis(data=data_x_axis, label='', units='', index=0) - - # TODO for your custom plugin. Initialize viewers pannel with the future type of data - self.dte_signal_temp.emit(DataToExport(name='myplugin', - data=[DataFromPlugins(name='Mock1', - data=[np.array([0., 0., ...]), - np.array([0., 0., ...])], - dim='Data1D', labels=['Mock1', 'label2'], - axes=[self.x_axis])])) - - info = "Whatever info you want to log" - return info, initialized - - def close(self): - """Terminate the communication protocol""" - ## TODO for your custom plugin - raise NotImplementedError # when writing your own plugin remove this line - # only the master owns the controller (a slave must not close the shared one) and ini_stage/ini_detector may - # have failed before creating it, so close can be called while self.controller is still None - if self.is_master and self.controller is not None: - # self.controller.your_method_to_terminate_the_communication() # when writing your own plugin replace this line - ... - - def grab_data(self, Naverage=1, **kwargs): - """Start a grab from the detector - - Parameters - ---------- - Naverage: int - Number of hardware averaging (if hardware averaging is possible, self.hardware_averaging should be set to - True in class preamble and you should code this implementation) - kwargs: dict - others optionals arguments - """ - ## TODO for your custom plugin: you should choose EITHER the synchrone or the asynchrone version following - - ##synchrone version (blocking function) - data_tot = self.controller.your_method_to_start_a_grab_snap() - self.dte_signal.emit(DataToExport('myplugin', - data=[DataFromPlugins(name='Mock1', data=data_tot, - dim='Data1D', labels=['dat0', 'data1'], - axes=[self.x_axis])])) - - ##asynchrone version (non-blocking function with callback) - self.controller.your_method_to_start_a_grab_snap(self.callback) - ######################################################### - - def callback(self): - """optional asynchrone method called when the detector has finished its acquisition of data""" - data_tot = self.controller.your_method_to_get_data_from_buffer() - self.dte_signal.emit(DataToExport('myplugin', - data=[DataFromPlugins(name='Mock1', data=data_tot, - dim='Data1D', labels=['dat0', 'data1'])])) - - def stop(self): - """Stop the current grab hardware wise if necessary""" - ## TODO for your custom plugin - raise NotImplementedError # when writing your own plugin remove this line - self.controller.your_method_to_stop_acquisition() # when writing your own plugin replace this line - self.emit_status(ThreadCommand('Update_Status', ['Some info you want to log'])) - ############################## - return '' - - def commit_settings(self, param: Parameter): - """Apply the consequences of a change of value in the detector settings - - Parameters - ---------- - param: Parameter - A given parameter (within detector_settings) whose value has been changed by the user - """ - ## TODO for your custom plugin - if param.name() == "a_parameter_you've_added_in_self.params": - self.controller.your_method_to_apply_this_param_change() -# elif ... - ## - - -if __name__ == '__main__': - main(__file__) diff --git a/src/pymodaq_plugins_template/daq_viewer_plugins/plugins_2D/__init__.py b/src/pymodaq_plugins_template/daq_viewer_plugins/plugins_2D/__init__.py deleted file mode 100644 index cbcf921..0000000 --- a/src/pymodaq_plugins_template/daq_viewer_plugins/plugins_2D/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -import importlib -from pathlib import Path -from ... import set_logger -logger = set_logger('viewer2D_plugins', add_to_console=False) - -for path in Path(__file__).parent.iterdir(): - try: - if '__init__' not in str(path): - importlib.import_module('.' + path.stem, __package__) - except Exception as e: - logger.warning("{:} plugin couldn't be loaded due to some missing packages or errors: {:}".format(path.stem, str(e))) - pass - - diff --git a/src/pymodaq_plugins_template/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Template.py b/src/pymodaq_plugins_template/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Template.py deleted file mode 100644 index 23e4f7f..0000000 --- a/src/pymodaq_plugins_template/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Template.py +++ /dev/null @@ -1,170 +0,0 @@ -import numpy as np - -from pymodaq_utils.utils import ThreadCommand -from pymodaq_data.data import DataToExport, Axis -from pymodaq_gui.parameter import Parameter - -from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters, main -from pymodaq.utils.data import DataFromPlugins - -# TODO: -# Replace the following fake import with the import of the real Python wrapper of your instrument. Here we suppose that -# the wrapper is in the hardware directory, but it could come from an external librairy like pylablib or pymeasure. -from pymodaq_plugins_template.hardware.python_wrapper_file_of_your_instrument import PythonWrapperObjectOfYourInstrument - -# TODO: -# (1) change the name of the following class to DAQ_2DViewer_TheNameOfYourChoice -# (2) change the name of this file to daq_2Dviewer_TheNameOfYourChoice ("TheNameOfYourChoice" should be the SAME -# for the class name and the file name.) -# (3) this file should then be put into the right folder, namely IN THE FOLDER OF THE PLUGIN YOU ARE DEVELOPING: -# pymodaq_plugins_my_plugin/daq_viewer_plugins/plugins_2D - - -class DAQ_2DViewer_Template(DAQ_Viewer_base): - """ Instrument plugin class for a 2D viewer. - - This object inherits all functionalities to communicate with PyMoDAQ’s DAQ_Viewer module through inheritance via - DAQ_Viewer_base. It makes a bridge between the DAQ_Viewer module and the Python wrapper of a particular instrument. - - TODO Complete the docstring of your plugin with: - * The set of instruments that should be compatible with this instrument plugin. - * With which instrument it has actually been tested. - * The version of PyMoDAQ during the test. - * The version of the operating system. - * Installation instructions: what manufacturer’s drivers should be installed to make it run? - - Attributes: - ----------- - controller: object - The particular object that allow the communication with the hardware, in general a python wrapper around the - hardware library. - - # TODO add your particular attributes here if any - - """ - params = comon_parameters + [ - ## TODO for your custom plugin - # elements to be added here as dicts in order to control your custom stage - ############ - ] - - def ini_attributes(self): - # TODO declare the type of the wrapper (and assign it to self.controller) you're going to use for easy - # autocompletion - self.controller: PythonWrapperObjectOfYourInstrument = None - - # TODO declare here attributes you want/need to init with a default value - - self.x_axis = None - self.y_axis = None - - def ini_detector(self, controller=None): - """Detector communication initialization - - Parameters - ---------- - controller: (object) - custom object of a PyMoDAQ plugin (Slave case). None if only one actuator/detector by controller - (Master case) - - Returns - ------- - info: str - initialized: bool - False if initialization failed otherwise True - """ - raise NotImplementedError # TODO when writing your own plugin remove this line and modify the one below - if self.is_master: - self.controller = PythonWrapperObjectOfYourInstrument() #instantiate you driver with whatever arguments are needed - self.controller.open_communication() # call eventual methods - initialized = self.controller.a_method_or_atttribute_to_check_if_init() # TODO - else: - self.controller = controller - initialized = True - - ## TODO for your custom plugin - # get the x_axis (you may want to to this also in the commit settings if x_axis may have changed - data_x_axis = self.controller.your_method_to_get_the_x_axis() # if possible - self.x_axis = Axis(data=data_x_axis, label='', units='', index=1) - - # get the y_axis (you may want to to this also in the commit settings if y_axis may have changed - data_y_axis = self.controller.your_method_to_get_the_y_axis() # if possible - self.y_axis = Axis(data=data_y_axis, label='', units='', index=0) - - ## TODO for your custom plugin. Initialize viewers pannel with the future type of data - self.dte_signal_temp.emit(DataToExport('myplugin', - data=[DataFromPlugins(name='Mock1', data=["2D numpy array"], - dim='Data2D', labels=['dat0'], - axes=[self.x_axis, self.y_axis]), ])) - - info = "Whatever info you want to log" - return info, initialized - - def close(self): - """Terminate the communication protocol""" - ## TODO for your custom plugin - raise NotImplementedError # when writing your own plugin remove this line - # only the master owns the controller (a slave must not close the shared one) and ini_stage/ini_detector may - # have failed before creating it, so close can be called while self.controller is still None - if self.is_master and self.controller is not None: - # self.controller.your_method_to_terminate_the_communication() # when writing your own plugin replace this line - ... - - def grab_data(self, Naverage=1, **kwargs): - """Start a grab from the detector - - Parameters - ---------- - Naverage: int - Number of hardware averaging (if hardware averaging is possible, self.hardware_averaging should be set to - True in class preamble and you should code this implementation) - kwargs: dict - others optionals arguments - """ - ## TODO for your custom plugin: you should choose EITHER the synchrone or the asynchrone version following - - ##synchrone version (blocking function) - data_tot = self.controller.your_method_to_start_a_grab_snap() - self.dte_signal.emit(DataToExport('myplugin', - data=[DataFromPlugins(name='Mock1', data=data_tot, - dim='Data2D', labels=['label1'], - axes=[self.x_axis, self.y_axis]), ])) - - ##asynchrone version (non-blocking function with callback) - self.controller.your_method_to_start_a_grab_snap(self.callback) - ######################################################### - - def callback(self): - """optional asynchrone method called when the detector has finished its acquisition of data""" - data_tot = self.controller.your_method_to_get_data_from_buffer() - self.dte_signal.emit(DataToExport('myplugin', - data=[DataFromPlugins(name='Mock1', data=data_tot, - dim='Data2D', labels=['label1'], - x_axis=self.x_axis, - y_axis=self.y_axis), ])) - - def stop(self): - """Stop the current grab hardware wise if necessary""" - ## TODO for your custom plugin - raise NotImplementedError # when writing your own plugin remove this line - self.controller.your_method_to_stop_acquisition() # when writing your own plugin replace this line - self.emit_status(ThreadCommand('Update_Status', ['Some info you want to log'])) - ############################## - return '' - - def commit_settings(self, param: Parameter): - """Apply the consequences of a change of value in the detector settings - - Parameters - ---------- - param: Parameter - A given parameter (within detector_settings) whose value has been changed by the user - """ - # TODO for your custom plugin - if param.name() == "a_parameter_you've_added_in_self.params": - self.controller.your_method_to_apply_this_param_change() - #elif ... - - -if __name__ == '__main__': - main(__file__) diff --git a/src/pymodaq_plugins_template/daq_viewer_plugins/plugins_ND/__init__.py b/src/pymodaq_plugins_template/daq_viewer_plugins/plugins_ND/__init__.py deleted file mode 100644 index 527b2d8..0000000 --- a/src/pymodaq_plugins_template/daq_viewer_plugins/plugins_ND/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -import importlib -from pathlib import Path -from ... import set_logger -logger = set_logger('viewerND_plugins', add_to_console=False) - -for path in Path(__file__).parent.iterdir(): - try: - if '__init__' not in str(path): - importlib.import_module('.' + path.stem, __package__) - except Exception as e: - logger.warning("{:} plugin couldn't be loaded due to some missing packages or errors: {:}".format(path.stem, str(e))) - pass - diff --git a/src/pymodaq_plugins_template/extensions/custom_extension_template.py b/src/pymodaq_plugins_template/extensions/custom_extension_template.py deleted file mode 100644 index bd16dea..0000000 --- a/src/pymodaq_plugins_template/extensions/custom_extension_template.py +++ /dev/null @@ -1,163 +0,0 @@ -"""Template of a Dashboard extension. - -An extension is a ``CustomExt``: a small application living inside the Dashboard. Everything related to the -instruments (the experiment, i.e. the list of actuators and detectors, and its state) is handled by the Dashboard, -the extension only uses it through its ``dashboard`` argument: - -* ``self.modules_manager``: a ModulesManager created for this extension, giving access to the actuators and - detectors of the Dashboard's experiment (never create control modules yourself) -* ``self.experiment_manager`` and ``self.state_manager``: the Dashboard's managers, never create your own - -The only difference with the extensions of the PyMoDAQ core is the presence of the ``EXTENSION_NAME`` and -``CLASS_NAME`` variables below, needed by the Dashboard to recognize your extension (plus the -``pymodaq.extensions`` entry point in pyproject.toml). -""" -from qtpy import QtWidgets - -from pymodaq_gui import utils as gutils -from pymodaq_utils.config import GlobalConfig -from pymodaq_utils.logger import set_logger, get_module_name - -from pymodaq.utils.custom_ext import CustomExt - -logger = set_logger(get_module_name(__file__)) - -# all the configurations (PyMoDAQ's packages and the one of your plugin package) are read from GlobalConfig, for -# instance: config('gui', 'style', 'theme') -config = GlobalConfig() - -# todo: modify this as you wish -EXTENSION_NAME = 'MY_EXTENSION_NAME' # the name that will be displayed in the extension list in the -# dashboard -CLASS_NAME = 'CustomExtensionTemplate' # this should be the name of your class defined below - - -# todo: modify the name of this class to reflect its application and change the name in the main -# method at the end of the script -class CustomExtensionTemplate(CustomExt): - - # todo: if you wish to create custom Parameter and corresponding widgets. These will be - # automatically added as children of self.settings. Morevover, the self.settings_tree will - # render the widgets in a Qtree. If you wish to see it in your app, add is into a Dock - params = [] - - def __init__(self, parent: gutils.DockArea, dashboard): - # also creates self.modules_manager for this extension, and the extension's own toolbar (named after the - # class). Like the PyMoDAQ extensions (scan, sequencer...), no break is added before it - super().__init__(parent, dashboard, add_toolbar_break=False) - - self.setup_ui() # calls, in this order: setup_docks_and_widgets, setup_menus_and_toolbars, - # setup_actions, connect_things and do_things_after_ui_setup - - def setup_docks_and_widgets(self): - """Method to be subclassed to setup the docks layout - - Examples - -------- - >>>self.docks['ADock'] = gutils.Dock('ADock name') - >>>self.dockarea.addDock(self.docks['ADock']) - >>>self.docks['AnotherDock'] = gutils.Dock('AnotherDock name') - >>>self.dockarea.addDock(self.docks['AnotherDock'''], 'bottom', self.docks['ADock']) - - See Also - -------- - pyqtgraph.dockarea.Dock - """ - # todo: create docks and add them here to hold your widgets. Here the settings tree in a dock - self.docks['settings'] = gutils.Dock('Settings') - self.dockarea.addDock(self.docks['settings']) - self.docks['settings'].addWidget(self.settings_tree) - - def setup_menus_and_toolbars(self, menubar: QtWidgets.QMenuBar = None): - """Non mandatory method to be subclassed in order to create menus and toolbars - - Examples - -------- - >>>file_menu = self.add_menu('file', 'File', parent_menu=menubar) - - See Also - -------- - pymodaq.utils.managers.action_manager.ActionManager - """ - # adds a second toolbar, the Dashboard one: show/hide the Dashboard, load an experiment and a state - self.create_dashboard_toolbar(add_break=False) - - def setup_actions(self): - """Method where to create actions to be subclassed - - Examples - -------- - >>> self.add_action('grab', 'Grab', 'camera', "Grab from camera", checkable=True) - - See Also - -------- - ActionManager.add_action - """ - # todo: replace this example action by yours - self.add_action('list_modules', 'List modules', 'add_circle', - tip='Show the actuators and detectors of the current experiment') - - def connect_things(self): - """Connect actions and/or other widgets signal to methods""" - # todo: replace this example by your connections - self.connect_action('list_modules', self.list_modules) - - def list_modules(self): - """Example: use the modules_manager to access the instruments of the Dashboard's experiment""" - self.update_status(f'Actuators: {self.modules_manager.actuators_name}, ' - f'detectors: {self.modules_manager.detectors_name}') - - def do_things_after_experiment_set(self, experiment_name: str, show_dashboard: bool = None): - """Called each time an experiment (the list of instruments) has been set in the Dashboard - - The base class updates self.modules_manager with the new instruments. - """ - super().do_things_after_experiment_set(experiment_name, show_dashboard) - # todo: update your widgets with the new instruments if needed - - def value_changed(self, param): - """ Actions to perform when one of the param's value in self.settings is changed from the - user interface - - For instance: - if param.name() == 'do_something': - if param.value(): - print('Do something') - self.settings.child('main_settings', 'something_done').setValue(False) - - Parameters - ---------- - param: (Parameter) the parameter whose value just changed - """ - pass - - def _quit_fun(self) -> bool: - """Called when the extension is closed. Return True to let it quit, False to refuse (for instance while - running). The base class returns None, which would prevent the extension from closing. - """ - return True - - -def main(): - """Run the extension on its own: loads a Dashboard (hidden), then the extension - - The Dashboard command line options apply, for instance ``-x EXPERIMENT_NAME -s STATE_NAME`` - """ - import sys - from pymodaq_gui.qt_utils import mkQApp - from pymodaq.dashboard import load_dashboard_with_arguments - from pymodaq.utils.gui_utils.loader_utils import create_extension - - app = mkQApp('Custom Ext') - - win, dashboard, _ = load_dashboard_with_arguments(show_dashboard=False, - load_extension=False, - ) - win.mainwindow.setVisible(False) - - win_ext, ext = create_extension(dashboard, CustomExtensionTemplate, show_extension=True) - sys.exit(app.exec()) - - -if __name__ == '__main__': - main() diff --git a/src/pymodaq_plugins_template/resources/__init__.py b/src/pymodaq_plugins_template/resources/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/pymodaq_plugins_template/resources/config_template.toml b/src/pymodaq_plugins_template/resources/config_template.toml deleted file mode 100644 index a2604a3..0000000 --- a/src/pymodaq_plugins_template/resources/config_template.toml +++ /dev/null @@ -1,2 +0,0 @@ -title = 'this is the configuration file of the plugin XXX' - diff --git a/src/pymodaq_plugins_template/scanners/__init__.py b/src/pymodaq_plugins_template/scanners/__init__.py deleted file mode 100644 index 180d4dd..0000000 --- a/src/pymodaq_plugins_template/scanners/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created the 01/06/2023 - -@author: Sebastien Weber -""" diff --git a/tests/example_mock_plugins.py b/tests/example_mock_plugins.py deleted file mode 100644 index 0827d4f..0000000 --- a/tests/example_mock_plugins.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Minimal fake controller and plugins used to test the harness, and as a pattern to copy. - -When you write a real plugin, keep the vendor communication behind a small wrapper class in ``hardware/`` and write a -fake of that wrapper with the same public methods: tests then run the real plugin class against the fake. -""" -import numpy as np - -from pymodaq.control_modules.move_utility_classes import (DAQ_Move_base, comon_parameters_fun, DataActuatorType) -from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters -from pymodaq.utils.data import DataActuator, DataFromPlugins -from pymodaq_data.data import DataToExport - - -class FakeController: - """Stands for the python wrapper of a real instrument: instantaneous, in memory, records what it was asked""" - def __init__(self): - self.position = 0. - self.is_open = True - self.calls = [] - - def move_at(self, value: float): - self.calls.append(('move_at', value)) - self.position = value - - def get_position(self) -> float: - return self.position - - def acquire(self, npts: int = 10) -> np.ndarray: - self.calls.append(('acquire', npts)) - return np.linspace(0., 1., npts) - - def close(self): - self.is_open = False - - -class DAQ_Move_Fake(DAQ_Move_base): - """Smallest actuator following the template: one axis, positions in mm""" - _controller_units = 'mm' - is_multiaxes = False - _axis_names = ['axis'] - _epsilons = [0.01] - data_actuator_type = DataActuatorType.DataActuator - params = comon_parameters_fun(is_multiaxes, axis_names=_axis_names, epsilon=_epsilons) - - def ini_attributes(self): - self.controller: FakeController = None - - def ini_stage(self, controller=None): - self.controller = FakeController() if self.is_master else controller - return 'fake stage ready', True - - def close(self): - if self.is_master and self.controller is not None: - self.controller.close() - - def get_actuator_value(self) -> DataActuator: - pos = DataActuator(data=self.controller.get_position(), units=self.axis_unit) - return self.get_position_with_scaling(pos) - - def move_abs(self, value: DataActuator): - value = self.check_bound(value) - self.target_value = value - value = self.set_position_with_scaling(value) - self.controller.move_at(value.value(self.axis_unit)) # in the controller unit, see the template - - def move_rel(self, value: DataActuator): - value = self.check_bound(self.current_value + value) - self.current_value - self.target_value = value + self.current_value - value = self.set_position_with_scaling(self.target_value) - self.controller.move_at(value.value(self.axis_unit)) # in the controller unit, see the template - - def move_home(self): - self.move_abs(DataActuator(data=0., units=self.axis_unit)) - self.move_done() # homing is blocking here; an asynchronous one calls move_done when the controller says it ended - - def stop_motion(self): - self.controller.calls.append(('stop',)) - - def commit_settings(self, param): - pass - - -class DAQ_MultiAxes_Fake(DAQ_Move_Fake): - """Several axes sharing one controller: the master creates it, the slaves are given it by PyMoDAQ""" - is_multiaxes = True - _axis_names = ['x', 'y'] - _epsilons = [0.01, 0.01] - params = comon_parameters_fun(is_multiaxes, axis_names=_axis_names, epsilon=_epsilons) - - -class DAQ_1DViewer_Fake(DAQ_Viewer_base): - """Smallest 1D detector following the template""" - params = comon_parameters + [ - {'title': 'Npts:', 'name': 'npts', 'type': 'int', 'value': 10, 'min': 1}, - ] - - def ini_attributes(self): - self.controller: FakeController = None - - def ini_detector(self, controller=None): - self.controller = FakeController() if self.is_master else controller - return 'fake detector ready', True - - def close(self): - if self.is_master and self.controller is not None: - self.controller.close() - - def grab_data(self, Naverage=1, **kwargs): - y = self.controller.acquire(self.settings['npts']) - self.dte_signal.emit(DataToExport( - name='fake', - data=[DataFromPlugins(name='trace', data=[y], dim='Data1D', labels=['signal'], units='V')])) - - def commit_settings(self, param): - pass diff --git a/tests/test_plugin_behaviour.py b/tests/test_plugin_behaviour.py deleted file mode 100644 index 0d7bc85..0000000 --- a/tests/test_plugin_behaviour.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Behaviour tests of instrument plugins against a fake controller: no hardware, no visible GUI. - -Here the harness of ``pymodaq.utils.plugin_testing`` is applied to the example fake plugins of ``example_mock_plugins.py``. To test your own plugin, copy -a test and replace the plugin class and the fake controller, for instance:: - - from pymodaq_plugins_.daq_move_plugins.daq_move_ import DAQ_Move_ - actuator = make_actuator(DAQ_Move_) # then monkeypatch the controller class with your fake before ini_stage -""" -import pytest - -from pymodaq.utils.plugin_testing import (make_actuator, make_detector, move_abs_and_wait, move_rel_and_wait, - move_home_and_wait, grab_and_wait, assert_units, wait_for_signal, - SignalTimeout) -from example_mock_plugins import DAQ_Move_Fake, DAQ_1DViewer_Fake, DAQ_MultiAxes_Fake - - -@pytest.fixture -def actuator(qapp): - plugin = make_actuator(DAQ_Move_Fake) - assert plugin.initialized - yield plugin - plugin.close() - - -@pytest.fixture -def detector(qapp): - plugin = make_detector(DAQ_1DViewer_Fake, npts=20) - assert plugin.initialized - yield plugin - plugin.close() - - -class TestActuator: - def test_ini_returns_info_and_flag(self, actuator): - assert isinstance(actuator.init_info, str) - assert actuator.initialized is True - - def test_value_has_units(self, actuator): - pos = actuator.get_actuator_value() - assert pos.units == 'mm' - - def test_move_abs(self, actuator): - pos = move_abs_and_wait(actuator, 2.5) - assert pos.value('mm') == pytest.approx(2.5, abs=0.01) - assert actuator.controller.calls[-1] == ('move_at', 2.5) - - def test_move_rel(self, actuator): - move_abs_and_wait(actuator, 1.) - pos = move_rel_and_wait(actuator, 0.5) - assert pos.value('mm') == pytest.approx(1.5, abs=0.01) - - def test_move_home(self, actuator): - move_abs_and_wait(actuator, 3.) - pos = move_home_and_wait(actuator) - assert pos.value('mm') == pytest.approx(0., abs=0.01) - - def test_other_units_are_converted(self, actuator): - from pymodaq.utils.data import DataActuator - pos = move_abs_and_wait(actuator, DataActuator(data=1., units='cm')) # 1 cm = 10 mm - assert pos.value('mm') == pytest.approx(10., abs=0.01) - - def test_close_releases_the_controller(self, actuator): - controller = actuator.controller - actuator.close() - assert controller.is_open is False - - def test_slave_axis_uses_the_given_controller(self, qapp): - master = make_actuator(DAQ_MultiAxes_Fake) - slave = make_actuator(DAQ_MultiAxes_Fake, controller=master.controller) - assert slave.controller is master.controller - move_abs_and_wait(slave, 1.) - assert master.controller.calls[-1] == ('move_at', 1.) - slave.close() - assert master.controller.is_open # only the master releases the shared controller - master.close() - assert not master.controller.is_open - - -class TestDetector: - def test_grab_returns_data_with_units(self, detector): - dte = grab_and_wait(detector) - assert len(dte) == 1 - assert dte[0].shape == (20,) - assert_units(dte, 'V') - - def test_close_releases_the_controller(self, detector): - controller = detector.controller - detector.close() - assert controller.is_open is False - - -def test_harness_times_out_when_nothing_happens(qapp, actuator): - with pytest.raises(SignalTimeout): - wait_for_signal(actuator.move_done_signal, lambda: None, timeout_ms=100) diff --git a/tests/test_slmsuite_plugins.py b/tests/test_slmsuite_plugins.py new file mode 100644 index 0000000..be6bce4 --- /dev/null +++ b/tests/test_slmsuite_plugins.py @@ -0,0 +1,158 @@ +"""Behaviour tests of the SLM actuators, against the simulated SLM of slmsuite (no hardware, no visible GUI). + +Real devices (Meadowlark, Holoeye...) are not tested here: only the arguments built for their constructor are checked, +with a fake controller. +""" +import numpy as np +import pytest + +from pymodaq.utils.plugin_testing import make_actuator, move_abs_and_wait +from pymodaq.utils.data import DataActuator + +from pymodaq_plugins_slmsuite.hardware import slm_controller +from pymodaq_plugins_slmsuite.hardware.slm_controller import SlmController +from pymodaq_plugins_slmsuite.resources.daq_move_SlmsuiteBase import parse_kwargs +from pymodaq_plugins_slmsuite.daq_move_plugins.daq_move_Slmsuite import DAQ_Move_Slmsuite +from pymodaq_plugins_slmsuite.daq_move_plugins.daq_move_SlmsuiteSimulated import DAQ_Move_SlmsuiteSimulated +from pymodaq_plugins_slmsuite.daq_move_plugins.daq_move_SlmsuiteMeadowlark import DAQ_Move_SlmsuiteMeadowlark +from pymodaq_plugins_slmsuite.daq_move_plugins.daq_move_SlmsuiteHoloeye import DAQ_Move_SlmsuiteHoloeye + +WIDTH, HEIGHT = 64, 48 + + +@pytest.fixture +def slm(qapp): + plugin = make_actuator(DAQ_Move_SlmsuiteSimulated, sim_width=WIDTH, sim_height=HEIGHT) + assert plugin.initialized + yield plugin + plugin.close() + + +class TestController: + def test_unknown_type(self): + with pytest.raises(ValueError): + SlmController('NotAnSlm') + + def test_all_names_are_known(self): + assert set(slm_controller.SLM_CLASSES) >= {'SimulatedSLM', 'Holoeye', 'Meadowlark'} + + def test_simulated_shape_and_levels(self): + controller = SlmController('SimulatedSLM', resolution=(WIDTH, HEIGHT)) + assert controller.shape == (HEIGHT, WIDTH) + assert controller.bitresolution == 256 + controller.close() + + def test_grey_levels_are_clipped_and_rounded(self): + controller = SlmController('SimulatedSLM', resolution=(WIDTH, HEIGHT)) + controller.show_grey_levels(np.full((HEIGHT, WIDTH), 300.4)) + assert np.all(controller.slm.display == 255) + controller.close() + + +class TestParseKwargs: + def test_empty(self): + assert parse_kwargs(' ') == {} + + def test_literal(self): + assert parse_kwargs("{'slm_number': 2, 'resolution': (10, 20)}") == {'slm_number': 2, 'resolution': (10, 20)} + + def test_no_code_evaluated(self): + with pytest.raises(ValueError): + parse_kwargs("__import__('os').getcwd()") + + def test_not_a_dict(self): + with pytest.raises(ValueError): + parse_kwargs('[1, 2]') + + +class TestSimulatedActuator: + def test_infos(self, slm): + assert slm.settings['info', 'width'] == WIDTH + assert slm.settings['info', 'height'] == HEIGHT + assert slm.settings['info', 'levels'] == 256 + + def test_phase_is_displayed_wrapped(self, slm): + phase = np.linspace(-1, 7, WIDTH * HEIGHT).reshape(HEIGHT, WIDTH) + move_abs_and_wait(slm, DataActuator(data=[phase])) + displayed = slm.get_actuator_value()[0] + expected = np.mod(phase, 2 * np.pi) + error = np.abs(np.angle(np.exp(1j * (displayed - expected)))) + assert error.max() < 2 * np.pi / 256 * 1.5 # quantization to 256 levels + + def test_single_value_fills_the_slm(self, slm): + move_abs_and_wait(slm, 1.) + displayed = slm.get_actuator_value()[0] + assert displayed.shape == (HEIGHT, WIDTH) + assert np.ptp(displayed) < 2 * np.pi / 256 * 1.5 + + def test_grey_levels_with_setting(self, slm): + slm.settings.child('input_as_grey_levels').setValue(True) + grey = np.tile(np.arange(WIDTH) * 4, (HEIGHT, 1)) + move_abs_and_wait(slm, DataActuator(data=[grey.astype(float)])) + assert np.array_equal(slm.controller.slm.display, grey.astype(np.uint8)) + + def test_grey_levels_with_data_attribute(self, slm): + grey = np.full((HEIGHT, WIDTH), 42.) + move_abs_and_wait(slm, DataActuator(data=[grey], as_grey_levels=True)) + assert np.all(slm.controller.slm.display == 42) + + def test_wrong_shape_is_refused(self, slm): + with pytest.raises(ValueError): + slm.move_abs(DataActuator(data=[np.zeros((3, 3))])) + + def test_move_rel(self, slm): + move_abs_and_wait(slm, DataActuator(data=[np.full((HEIGHT, WIDTH), 1.)])) + slm.move_rel(DataActuator(data=[np.full((HEIGHT, WIDTH), 0.5)])) + assert np.abs(slm.get_actuator_value()[0] - 1.5).max() < 2 * np.pi / 256 * 1.5 + + def test_close_only_by_master(self, slm): + slave = make_actuator(DAQ_Move_SlmsuiteSimulated, controller=slm.controller) + assert slave.controller is slm.controller + slave.close() # must not close the shared controller + move_abs_and_wait(slm, 0.) + + +def test_generic_plugin_with_arguments(qapp): + plugin = make_actuator(DAQ_Move_Slmsuite, slm_type='SimulatedSLM', + arguments=f"{{'resolution': ({WIDTH}, {HEIGHT})}}") + assert plugin.initialized + assert plugin.controller.shape == (HEIGHT, WIDTH) + move_abs_and_wait(plugin, 0.5) + plugin.close() + + +class FakeController: + instances = [] + + def __init__(self, slm_type, **kwargs): + self.slm_type, self.kwargs = slm_type, kwargs + self.shape = (4, 6) + self.bitresolution = 256 + FakeController.instances.append(self) + + def close(self): + pass + + +@pytest.fixture +def fake_controller(monkeypatch): + FakeController.instances.clear() + monkeypatch.setattr('pymodaq_plugins_slmsuite.resources.daq_move_SlmsuiteBase.SlmController', FakeController) + return FakeController + + +def test_meadowlark_arguments(qapp, fake_controller): + plugin = make_actuator(DAQ_Move_SlmsuiteMeadowlark, slm_number=2, wavelength_nm=800.) + assert plugin.initialized + controller = fake_controller.instances[-1] + assert controller.slm_type == 'Meadowlark' + assert controller.kwargs == {'wav_um': pytest.approx(0.8), 'slm_number': 2} # empty paths are not passed + plugin.close() + + +def test_holoeye_arguments(qapp, fake_controller): + plugin = make_actuator(DAQ_Move_SlmsuiteHoloeye, preselect='name:pluto') + controller = fake_controller.instances[-1] + assert controller.slm_type == 'Holoeye' + assert controller.kwargs['preselect'] == 'name:pluto' + plugin.close() From 0277f6a66e0c746a39ee2c452d0233b4c5ab1ffb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 21:12:11 +0000 Subject: [PATCH 2/4] Fix tests aborting with PyQt5 after opencv has been imported by slmsuite opencv-python sets QT_QPA_PLATFORM_PLUGIN_PATH to its own bundled Qt plugins when imported; creating the QApplication afterwards aborted with PyQt5. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01CAhZwmbyCSpSgfTyicUxuS --- tests/conftest.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..5a529ed --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,13 @@ +"""slmsuite imports opencv, whose pip wheel points Qt to its own bundled platform plugins (QT_QPA_PLATFORM_PLUGIN_PATH). +With PyQt5 this makes the creation of the QApplication abort when slmsuite was imported before it, as the tests of +the controller do. Import cv2 here and drop these variables, so that Qt keeps using the plugins of the installed binding. +""" +import os + +try: + import cv2 # noqa: F401 +except ImportError: + pass + +for variable in ('QT_QPA_PLATFORM_PLUGIN_PATH', 'QT_QPA_FONTDIR'): + os.environ.pop(variable, None) From e51992fcadca91f616be014ac5d387272b0883b3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 21:12:57 +0000 Subject: [PATCH 3/4] Add manual tests for a real SLM, skipped unless SLMSUITE_REAL_SLM is set Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01CAhZwmbyCSpSgfTyicUxuS --- tests/test_real_slm.py | 112 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 tests/test_real_slm.py diff --git a/tests/test_real_slm.py b/tests/test_real_slm.py new file mode 100644 index 0000000..4b22a0f --- /dev/null +++ b/tests/test_real_slm.py @@ -0,0 +1,112 @@ +"""Manual tests on a REAL SLM, skipped unless the environment variable ``SLMSUITE_REAL_SLM`` is set. + +They drive the real plugin class (the same one the Dashboard uses) and display a few patterns that you check by eye on +the SLM. Examples (from the root of the repository):: + + # Meadowlark, board 1 + SLMSUITE_REAL_SLM=Meadowlark SLMSUITE_REAL_ARGS="{'slm_number': 1}" pytest tests/test_real_slm.py -s -v + + # Holoeye, first SLM found + SLMSUITE_REAL_SLM=Holoeye SLMSUITE_REAL_ARGS="{'preselect': 'index:0'}" pytest tests/test_real_slm.py -s -v + +On Windows PowerShell use ``$env:SLMSUITE_REAL_SLM="Meadowlark"`` before calling pytest. + +Environment variables: + +* ``SLMSUITE_REAL_SLM``: name of the slmsuite class, one of ``hardware.slm_controller.SLM_CLASSES`` +* ``SLMSUITE_REAL_ARGS``: constructor arguments as a Python dictionary (see the generic ``Slmsuite`` actuator) +* ``SLMSUITE_REAL_WAVELENGTH_NM``: working wavelength in nm (default 515) +* ``SLMSUITE_REAL_HOLD_S``: seconds each pattern stays displayed (default 3) + +What the assertions check is only that nothing raises and that the realized phase read back from slmsuite is coherent +with what was sent. Whether the SLM really shows the pattern, and the wavelength / LUT calibration, are for you to +check by eye (and with a camera) while the patterns are displayed. +""" +import os +import time + +import numpy as np +import pytest + +from pymodaq.utils.plugin_testing import make_actuator, move_abs_and_wait +from pymodaq.utils.data import DataActuator + +from pymodaq_plugins_slmsuite.daq_move_plugins.daq_move_Slmsuite import DAQ_Move_Slmsuite + +SLM_TYPE = os.environ.get('SLMSUITE_REAL_SLM') +pytestmark = pytest.mark.skipif(not SLM_TYPE, reason='set SLMSUITE_REAL_SLM to test a real SLM') + +HOLD_S = float(os.environ.get('SLMSUITE_REAL_HOLD_S', 3)) + + +def phase_error(displayed: np.ndarray, expected: np.ndarray) -> float: + """Largest difference modulo 2 pi between two phases""" + return float(np.abs(np.angle(np.exp(1j * (displayed - expected)))).max()) + + +@pytest.fixture(scope='module') +def slm(qapp): + plugin = make_actuator(DAQ_Move_Slmsuite, slm_type=SLM_TYPE, + arguments=os.environ.get('SLMSUITE_REAL_ARGS', '{}'), + wavelength_nm=float(os.environ.get('SLMSUITE_REAL_WAVELENGTH_NM', 515))) + assert plugin.initialized, 'The SLM could not be opened, see the log above' + print(f'\n{plugin.init_info}') + yield plugin + move_abs_and_wait(plugin, 0.) + plugin.close() + + +def show(slm, array: np.ndarray, description: str, **attributes): + print(f' displaying: {description} (for {HOLD_S} s)') + move_abs_and_wait(slm, DataActuator(data=[array], **attributes)) + time.sleep(HOLD_S) + + +def grid(slm): + height, width = slm.controller.shape + return np.meshgrid(np.arange(width), np.arange(height)) + + +def test_zero_phase(slm): + show(slm, np.zeros(slm.controller.shape), 'phase 0 everywhere (uniform)') + assert slm.get_actuator_value()[0].shape == slm.controller.shape + + +def test_ramp_0_to_2pi(slm): + """Horizontal phase ramp from 0 to 2 pi: a blazed grating with one period over the whole SLM, the zeroth order + should leave its place""" + xx, _ = grid(slm) + phase = 2 * np.pi * xx / xx.shape[1] + show(slm, phase, 'phase ramp 0 -> 2 pi over the width') + assert phase_error(slm.get_actuator_value()[0], phase) < 0.2 + + +def test_blazed_grating(slm): + """Blazed grating of 16 pixels period: a clean first diffraction order should appear""" + xx, _ = grid(slm) + phase = 2 * np.pi * xx / 16 + show(slm, phase, 'blazed grating, period 16 pixels') + assert phase_error(slm.get_actuator_value()[0], np.mod(phase, 2 * np.pi)) < 0.2 + + +def test_two_halves(slm): + """Left half 0, right half pi: the two halves interfere destructively on axis if the wavelength / LUT are right""" + xx, _ = grid(slm) + phase = np.where(xx < xx.shape[1] // 2, 0., np.pi) + show(slm, phase, 'left half 0, right half pi') + assert phase_error(slm.get_actuator_value()[0], phase) < 0.2 + + +def test_grey_levels_ramp(slm): + """Raw grey levels, bypassing slmsuite's phase conversion: horizontal ramp over all the levels""" + xx, _ = grid(slm) + levels = slm.controller.bitresolution + grey = np.floor(xx / xx.shape[1] * levels) + show(slm, grey, f'raw grey levels ramp 0 -> {levels - 1}', as_grey_levels=True) + assert np.array_equal(np.asarray(slm.controller.slm.display), grey.astype(slm.controller.slm.dtype)) + + +def test_move_home_blanks_the_slm(slm): + slm.move_home() + time.sleep(HOLD_S) + assert np.abs(slm.get_actuator_value()[0]).max() < 0.1 From 3e2c598b4ee93f8f91235abec471438081ee61e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 21:35:48 +0000 Subject: [PATCH 4/4] Document HDMI/PCIe handling of the Meadowlark actuator Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01CAhZwmbyCSpSgfTyicUxuS --- .../daq_move_plugins/daq_move_SlmsuiteMeadowlark.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pymodaq_plugins_slmsuite/daq_move_plugins/daq_move_SlmsuiteMeadowlark.py b/src/pymodaq_plugins_slmsuite/daq_move_plugins/daq_move_SlmsuiteMeadowlark.py index eaf0506..22d3359 100644 --- a/src/pymodaq_plugins_slmsuite/daq_move_plugins/daq_move_SlmsuiteMeadowlark.py +++ b/src/pymodaq_plugins_slmsuite/daq_move_plugins/daq_move_SlmsuiteMeadowlark.py @@ -8,13 +8,16 @@ class DAQ_Move_SlmsuiteMeadowlark(DAQ_Move_SlmsuiteBase): 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'}, + '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,