From acdecaea60022a0bae4b781b15cb6cbfb4c0eee5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:39:34 +0000 Subject: [PATCH 1/5] Add ShaperInterface: pixel count and pixel size reported by the shaper plugins Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01CAhZwmbyCSpSgfTyicUxuS --- .../daq_move_plugins/daq_move_MockShaper.py | 9 ++++- .../utilities/shaper.py | 38 ++++++++++++++++++ tests/test_shaper_interface.py | 40 +++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 tests/test_shaper_interface.py diff --git a/src/pymodaq_plugins_beam_shaping/daq_move_plugins/daq_move_MockShaper.py b/src/pymodaq_plugins_beam_shaping/daq_move_plugins/daq_move_MockShaper.py index 6d38524..db28865 100644 --- a/src/pymodaq_plugins_beam_shaping/daq_move_plugins/daq_move_MockShaper.py +++ b/src/pymodaq_plugins_beam_shaping/daq_move_plugins/daq_move_MockShaper.py @@ -10,9 +10,11 @@ from pymodaq_gui.parameter import Parameter from pymodaq_plugins_beam_shaping.hardware.mock_shaper_camera import ShaperCamera +from pymodaq_plugins_beam_shaping.utilities.shaper import ShaperInterface, ShaperGeometry +from pymodaq_plugins_beam_shaping.utilities import sizing -class DAQ_Move_MockShaper(DAQ_Move_base): +class DAQ_Move_MockShaper(ShaperInterface, 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 @@ -38,6 +40,11 @@ class DAQ_Move_MockShaper(DAQ_Move_base): def ini_attributes(self): self.controller: ShaperCamera = None + def get_shaper_geometry(self) -> ShaperGeometry: + height, width = sizing.get_slm_size() + pixel_size = float(sizing.plugin_config('SLM', sizing.plugin_config('SLM', 'default_slm')[0], 'pixel_size')) + return ShaperGeometry(width, height, pixel_size, pixel_size) + def get_actuator_value(self): """Get the current value from the hardware with scaling conversion. diff --git a/src/pymodaq_plugins_beam_shaping/utilities/shaper.py b/src/pymodaq_plugins_beam_shaping/utilities/shaper.py index 0d16e0d..339fef3 100644 --- a/src/pymodaq_plugins_beam_shaping/utilities/shaper.py +++ b/src/pymodaq_plugins_beam_shaping/utilities/shaper.py @@ -23,6 +23,44 @@ class Shaper: modulator_type: ModulatorType +@dataclasses.dataclass(frozen=True) +class ShaperGeometry: + """Pixel layout of a shaper (SLM, DMD...), as reported by its instrument plugin""" + width: int # number of pixels along x (horizontal) + height: int # number of pixels along y (vertical) + pixel_size_x: float # in micrometer + pixel_size_y: float # in micrometer + + @property + def shape(self) -> tuple[int, int]: + """The shape (height, width) of the arrays to send to the shaper""" + return self.height, self.width + + @property + def pixel_size(self) -> tuple[float, float]: + """The pixel sizes (x, y) in micrometer""" + return self.pixel_size_x, self.pixel_size_y + + +class ShaperInterface: + """Base class (mixin) of the instrument plugins (DAQ_Move) that drive a shaper + + A plugin deriving from it must tell the BeamShaping extension the number of pixels and their size, whatever the + driver (the values come from the instrument, not from the configuration file). Put it first in the bases: + ``class DAQ_Move_X(ShaperInterface, DAQ_Move_base)``. + """ + + def get_shaper_geometry(self) -> ShaperGeometry: + """Number of pixels along x and y and size of the pixels in micrometer, to be implemented""" + raise NotImplementedError + + +def has_shaper_interface(obj) -> bool: + """True if the object (a plugin, or a controller) gives the geometry of the shaper, derived from + ShaperInterface or not (so that a plugin does not need beam_shaping installed to provide it)""" + return callable(getattr(obj, 'get_shaper_geometry', None)) + + def get_shaper() -> Shaper: default_slm = config('beam_shaping', 'SLM', 'default_slm')[0] return Shaper(name=default_slm, diff --git a/tests/test_shaper_interface.py b/tests/test_shaper_interface.py new file mode 100644 index 0000000..b688d30 --- /dev/null +++ b/tests/test_shaper_interface.py @@ -0,0 +1,40 @@ +import pytest + +from pymodaq_plugins_beam_shaping.utilities.shaper import ShaperGeometry, ShaperInterface, has_shaper_interface + + +def test_geometry(): + geometry = ShaperGeometry(width=1920, height=1080, pixel_size_x=8., pixel_size_y=9.) + assert geometry.shape == (1080, 1920) + assert geometry.pixel_size == (8., 9.) + + +def test_interface_has_to_be_implemented(): + class Plugin(ShaperInterface): + pass + + with pytest.raises(NotImplementedError): + Plugin().get_shaper_geometry() + + +def test_duck_typing(): + class Other: + def get_shaper_geometry(self): + return ShaperGeometry(2, 3, 4., 4.) + + assert has_shaper_interface(Other()) + assert has_shaper_interface(ShaperInterface()) + assert not has_shaper_interface(object()) + + +def test_mock_shaper_gives_the_configured_geometry(qapp): + from pymodaq.utils.plugin_testing import make_actuator + from pymodaq_plugins_beam_shaping.daq_move_plugins.daq_move_MockShaper import DAQ_Move_MockShaper + from pymodaq_plugins_beam_shaping.utilities import sizing + + plugin = make_actuator(DAQ_Move_MockShaper) + geometry = plugin.get_shaper_geometry() + assert isinstance(plugin, ShaperInterface) + assert geometry.shape == sizing.get_slm_size() + assert geometry.pixel_size_x > 0 + plugin.close() From 0cfc52bea921d1470c58472911f38e78d0131c3e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 10:46:04 +0000 Subject: [PATCH 2/5] Shaper geometry as a settings group read from the control module, instead of a mixin Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01CAhZwmbyCSpSgfTyicUxuS --- .../daq_move_plugins/daq_move_MockShaper.py | 15 ++++-- .../utilities/shaper.py | 54 ++++++++++++------- tests/test_shaper_interface.py | 36 +++++++------ 3 files changed, 67 insertions(+), 38 deletions(-) diff --git a/src/pymodaq_plugins_beam_shaping/daq_move_plugins/daq_move_MockShaper.py b/src/pymodaq_plugins_beam_shaping/daq_move_plugins/daq_move_MockShaper.py index db28865..187f8d4 100644 --- a/src/pymodaq_plugins_beam_shaping/daq_move_plugins/daq_move_MockShaper.py +++ b/src/pymodaq_plugins_beam_shaping/daq_move_plugins/daq_move_MockShaper.py @@ -10,11 +10,11 @@ from pymodaq_gui.parameter import Parameter from pymodaq_plugins_beam_shaping.hardware.mock_shaper_camera import ShaperCamera -from pymodaq_plugins_beam_shaping.utilities.shaper import ShaperInterface, ShaperGeometry +from pymodaq_plugins_beam_shaping.utilities.shaper import shaper_geometry_params from pymodaq_plugins_beam_shaping.utilities import sizing -class DAQ_Move_MockShaper(ShaperInterface, DAQ_Move_base): +class DAQ_Move_MockShaper(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 @@ -34,16 +34,20 @@ class DAQ_Move_MockShaper(ShaperInterface, DAQ_Move_base): _epsilons: Union[float, List[float]] = 0.1 data_actuator_type = DataActuatorType.DataActuator - params = [] + comon_parameters_fun(is_multiaxes, axis_names=_axis_names, epsilon=_epsilons) + params = [shaper_geometry_params()] + comon_parameters_fun(is_multiaxes, axis_names=_axis_names, epsilon=_epsilons) def ini_attributes(self): self.controller: ShaperCamera = None - def get_shaper_geometry(self) -> ShaperGeometry: + def set_shaper_geometry(self): + """Fill the shaper_geometry group read by the BeamShaping extension""" height, width = sizing.get_slm_size() pixel_size = float(sizing.plugin_config('SLM', sizing.plugin_config('SLM', 'default_slm')[0], 'pixel_size')) - return ShaperGeometry(width, height, pixel_size, pixel_size) + group = self.settings.child('shaper_geometry') + for name, value in (('width', width), ('height', height), ('pixel_size_x', pixel_size), + ('pixel_size_y', pixel_size)): + group.child(name).setValue(value) def get_actuator_value(self): """Get the current value from the hardware with scaling conversion. @@ -86,6 +90,7 @@ def ini_stage(self, controller=None): """ if self.is_master: # is needed when controller is master self.controller = ShaperCamera() # arguments for instantiation!) + self.set_shaper_geometry() initialized = True else: self.controller = controller diff --git a/src/pymodaq_plugins_beam_shaping/utilities/shaper.py b/src/pymodaq_plugins_beam_shaping/utilities/shaper.py index 339fef3..6b3eb09 100644 --- a/src/pymodaq_plugins_beam_shaping/utilities/shaper.py +++ b/src/pymodaq_plugins_beam_shaping/utilities/shaper.py @@ -1,4 +1,5 @@ import dataclasses +from typing import Optional from pymodaq_utils.enums import StrEnum from pymodaq_utils.config import GlobalConfig @@ -23,6 +24,25 @@ class Shaper: modulator_type: ModulatorType +SHAPER_GEOMETRY_NAME = 'shaper_geometry' + + +def shaper_geometry_params() -> dict: + """Settings group that an instrument plugin driving a shaper (SLM, DMD...) adds to its parameters + + The BeamShaping extension reads the number of pixels and their size from this group in the settings of the + control module (the plugin itself lives in another thread and fills the group when it is initialized). A plugin + does not need to import this module: the group is a contract, its name and the names of its children are what + matters. All the children are read only, sizes in micrometer. + """ + return {'title': 'Shaper geometry:', 'name': SHAPER_GEOMETRY_NAME, 'type': 'group', 'children': [ + {'title': 'Width (pixels):', 'name': 'width', 'type': 'int', 'value': 0, 'readonly': True}, + {'title': 'Height (pixels):', 'name': 'height', 'type': 'int', 'value': 0, 'readonly': True}, + {'title': 'Pixel size x (um):', 'name': 'pixel_size_x', 'type': 'float', 'value': 0., 'readonly': True}, + {'title': 'Pixel size y (um):', 'name': 'pixel_size_y', 'type': 'float', 'value': 0., 'readonly': True}, + ]} + + @dataclasses.dataclass(frozen=True) class ShaperGeometry: """Pixel layout of a shaper (SLM, DMD...), as reported by its instrument plugin""" @@ -41,24 +61,22 @@ def pixel_size(self) -> tuple[float, float]: """The pixel sizes (x, y) in micrometer""" return self.pixel_size_x, self.pixel_size_y - -class ShaperInterface: - """Base class (mixin) of the instrument plugins (DAQ_Move) that drive a shaper - - A plugin deriving from it must tell the BeamShaping extension the number of pixels and their size, whatever the - driver (the values come from the instrument, not from the configuration file). Put it first in the bases: - ``class DAQ_Move_X(ShaperInterface, DAQ_Move_base)``. - """ - - def get_shaper_geometry(self) -> ShaperGeometry: - """Number of pixels along x and y and size of the pixels in micrometer, to be implemented""" - raise NotImplementedError - - -def has_shaper_interface(obj) -> bool: - """True if the object (a plugin, or a controller) gives the geometry of the shaper, derived from - ShaperInterface or not (so that a plugin does not need beam_shaping installed to provide it)""" - return callable(getattr(obj, 'get_shaper_geometry', None)) + @classmethod + def from_parameter(cls, group) -> 'ShaperGeometry': + """Read the group created by :func:`shaper_geometry_params`""" + return cls(int(group['width']), int(group['height']), + float(group['pixel_size_x']), float(group['pixel_size_y'])) + + +def get_module_shaper_geometry(module) -> Optional[ShaperGeometry]: + """The geometry given by a DAQ_Move control module, None if its plugin has no shaper geometry group or did not + fill it yet (not initialized)""" + try: + group = module.settings.child(module._hw_settings_name, SHAPER_GEOMETRY_NAME) + except (KeyError, AttributeError): + return None + geometry = ShaperGeometry.from_parameter(group) + return geometry if geometry.width > 0 and geometry.height > 0 and geometry.pixel_size_x > 0 else None def get_shaper() -> Shaper: diff --git a/tests/test_shaper_interface.py b/tests/test_shaper_interface.py index b688d30..4edf533 100644 --- a/tests/test_shaper_interface.py +++ b/tests/test_shaper_interface.py @@ -1,6 +1,8 @@ import pytest +from pymodaq_gui.parameter import Parameter -from pymodaq_plugins_beam_shaping.utilities.shaper import ShaperGeometry, ShaperInterface, has_shaper_interface +from pymodaq_plugins_beam_shaping.utilities.shaper import (ShaperGeometry, shaper_geometry_params, + get_module_shaper_geometry) def test_geometry(): @@ -9,22 +11,27 @@ def test_geometry(): assert geometry.pixel_size == (8., 9.) -def test_interface_has_to_be_implemented(): - class Plugin(ShaperInterface): - pass +class FakeModule: + """What the extension has: the DAQ_Move control module, with the settings tree of its plugin""" + _hw_settings_name = 'actuator_settings' - with pytest.raises(NotImplementedError): - Plugin().get_shaper_geometry() + def __init__(self, children): + self.settings = Parameter.create(name='settings', type='group', children=[ + {'name': self._hw_settings_name, 'type': 'group', 'children': children}]) -def test_duck_typing(): - class Other: - def get_shaper_geometry(self): - return ShaperGeometry(2, 3, 4., 4.) +def test_read_from_module(): + module = FakeModule([shaper_geometry_params()]) + assert get_module_shaper_geometry(module) is None # not filled yet: plugin not initialized + group = module.settings.child('actuator_settings', 'shaper_geometry') + for name, value in (('width', 1920), ('height', 1080), ('pixel_size_x', 8.), ('pixel_size_y', 9.)): + group.child(name).setValue(value) + assert get_module_shaper_geometry(module) == ShaperGeometry(1920, 1080, 8., 9.) - assert has_shaper_interface(Other()) - assert has_shaper_interface(ShaperInterface()) - assert not has_shaper_interface(object()) + +def test_plugin_without_the_group(): + assert get_module_shaper_geometry(FakeModule([])) is None + assert get_module_shaper_geometry(object()) is None def test_mock_shaper_gives_the_configured_geometry(qapp): @@ -33,8 +40,7 @@ def test_mock_shaper_gives_the_configured_geometry(qapp): from pymodaq_plugins_beam_shaping.utilities import sizing plugin = make_actuator(DAQ_Move_MockShaper) - geometry = plugin.get_shaper_geometry() - assert isinstance(plugin, ShaperInterface) + geometry = ShaperGeometry.from_parameter(plugin.settings.child('shaper_geometry')) assert geometry.shape == sizing.get_slm_size() assert geometry.pixel_size_x > 0 plugin.close() From b47379c7aed7a7d642bcf18bd4aefc3f0073a0b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 14:17:42 +0000 Subject: [PATCH 3/5] Build the Shaper from the Shaper actuator using the pymodaq_plugins_utils contract, configuration as fallback Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01CAhZwmbyCSpSgfTyicUxuS --- pyproject.toml | 1 + .../daq_move_plugins/daq_move_MockShaper.py | 17 ++-- .../extensions/spatial_beam_shaping.py | 16 ++-- .../utilities/shaper.py | 83 ++----------------- tests/test_shaper_interface.py | 51 +++++++----- 5 files changed, 56 insertions(+), 112 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8c90127..32db6d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ dependencies = [ "pymodaq_utils>=5.3.1,<5.4", "pymodaq_gui>=5.3.0,<5.4", "pymodaq_data>=5.3.0,<5.4", + "pymodaq_plugins_utils>=5.3.4", 'pyfftw', 'scikit-image', 'opencv-python', diff --git a/src/pymodaq_plugins_beam_shaping/daq_move_plugins/daq_move_MockShaper.py b/src/pymodaq_plugins_beam_shaping/daq_move_plugins/daq_move_MockShaper.py index 187f8d4..a5e2947 100644 --- a/src/pymodaq_plugins_beam_shaping/daq_move_plugins/daq_move_MockShaper.py +++ b/src/pymodaq_plugins_beam_shaping/daq_move_plugins/daq_move_MockShaper.py @@ -10,7 +10,7 @@ from pymodaq_gui.parameter import Parameter from pymodaq_plugins_beam_shaping.hardware.mock_shaper_camera import ShaperCamera -from pymodaq_plugins_beam_shaping.utilities.shaper import shaper_geometry_params +from pymodaq_plugins_beam_shaping.utilities.shaper import shaper_params, fill_shaper_settings, ModulatorType from pymodaq_plugins_beam_shaping.utilities import sizing @@ -34,20 +34,21 @@ class DAQ_Move_MockShaper(DAQ_Move_base): _epsilons: Union[float, List[float]] = 0.1 data_actuator_type = DataActuatorType.DataActuator - params = [shaper_geometry_params()] + comon_parameters_fun(is_multiaxes, axis_names=_axis_names, epsilon=_epsilons) + params = shaper_params() + comon_parameters_fun(is_multiaxes, axis_names=_axis_names, epsilon=_epsilons) def ini_attributes(self): self.controller: ShaperCamera = None def set_shaper_geometry(self): - """Fill the shaper_geometry group read by the BeamShaping extension""" + """Fill the shaper settings groups read by the BeamShaping extension""" height, width = sizing.get_slm_size() - pixel_size = float(sizing.plugin_config('SLM', sizing.plugin_config('SLM', 'default_slm')[0], 'pixel_size')) - group = self.settings.child('shaper_geometry') - for name, value in (('width', width), ('height', height), ('pixel_size_x', pixel_size), - ('pixel_size_y', pixel_size)): - group.child(name).setValue(value) + slm = sizing.plugin_config('SLM', 'default_slm')[0] + fill_shaper_settings(self.settings, slm, width, height, + float(sizing.plugin_config('SLM', slm, 'pixel_size')), + modulator_type=ModulatorType(sizing.plugin_config('SLM', slm, 'modulator_type')[0]), + has_internal_calibration=bool(sizing.plugin_config('SLM', slm, + 'has_internal_calibration'))) def get_actuator_value(self): """Get the current value from the hardware with scaling conversion. diff --git a/src/pymodaq_plugins_beam_shaping/extensions/spatial_beam_shaping.py b/src/pymodaq_plugins_beam_shaping/extensions/spatial_beam_shaping.py index df34255..4f9cf85 100644 --- a/src/pymodaq_plugins_beam_shaping/extensions/spatial_beam_shaping.py +++ b/src/pymodaq_plugins_beam_shaping/extensions/spatial_beam_shaping.py @@ -140,11 +140,17 @@ def update_modulator_field(self, field: Field): ) @property def shaper(self) -> Shaper: - if self._shaper is None: - self._shaper = get_shaper() - if self.has_action('calibration'): - self.set_action_visible('calibration', self._shaper.modulator_type == ModulatorType.PHASE) - return self._shaper + """The shaper as told by the Shaper actuator (once initialized), else the one of the configuration""" + shaper = None + if self._shaper_actuator is not None: + shaper = Shaper.from_module(self._shaper_actuator) + if shaper is None: + shaper = get_shaper() + if (self._shaper is None or self._shaper.modulator_type != shaper.modulator_type) and \ + self.has_action('calibration'): + self.set_action_visible('calibration', shaper.modulator_type == ModulatorType.PHASE) + self._shaper = shaper + return shaper def send_field_to_shaper(self, field: Field, send_to_shaper=True, with_corrections=True): """ Send the modulator field to the shaper depending on its modulation type diff --git a/src/pymodaq_plugins_beam_shaping/utilities/shaper.py b/src/pymodaq_plugins_beam_shaping/utilities/shaper.py index 6b3eb09..132999f 100644 --- a/src/pymodaq_plugins_beam_shaping/utilities/shaper.py +++ b/src/pymodaq_plugins_beam_shaping/utilities/shaper.py @@ -1,85 +1,14 @@ -import dataclasses -from typing import Optional - -from pymodaq_utils.enums import StrEnum from pymodaq_utils.config import GlobalConfig -config = GlobalConfig() - - -class ModulatorType(StrEnum): - PHASE = 'phase' - AMPLITUDE = 'amplitude' - BOTH = 'both' - - -@dataclasses.dataclass -class Shaper: - name: str - width: int - height: int - pixel_size: float # in micrometer - has_internal_calibration: bool - use_internal_calibration: bool - modulator_type: ModulatorType - - -SHAPER_GEOMETRY_NAME = 'shaper_geometry' - +# The shaper contract (settings groups of the instrument plugin, Shaper dataclass) lives in pymodaq_plugins_utils +from pymodaq_plugins_utils.hardware.shaper import Shaper, ModulatorType, shaper_params, fill_shaper_settings -def shaper_geometry_params() -> dict: - """Settings group that an instrument plugin driving a shaper (SLM, DMD...) adds to its parameters - - The BeamShaping extension reads the number of pixels and their size from this group in the settings of the - control module (the plugin itself lives in another thread and fills the group when it is initialized). A plugin - does not need to import this module: the group is a contract, its name and the names of its children are what - matters. All the children are read only, sizes in micrometer. - """ - return {'title': 'Shaper geometry:', 'name': SHAPER_GEOMETRY_NAME, 'type': 'group', 'children': [ - {'title': 'Width (pixels):', 'name': 'width', 'type': 'int', 'value': 0, 'readonly': True}, - {'title': 'Height (pixels):', 'name': 'height', 'type': 'int', 'value': 0, 'readonly': True}, - {'title': 'Pixel size x (um):', 'name': 'pixel_size_x', 'type': 'float', 'value': 0., 'readonly': True}, - {'title': 'Pixel size y (um):', 'name': 'pixel_size_y', 'type': 'float', 'value': 0., 'readonly': True}, - ]} - - -@dataclasses.dataclass(frozen=True) -class ShaperGeometry: - """Pixel layout of a shaper (SLM, DMD...), as reported by its instrument plugin""" - width: int # number of pixels along x (horizontal) - height: int # number of pixels along y (vertical) - pixel_size_x: float # in micrometer - pixel_size_y: float # in micrometer - - @property - def shape(self) -> tuple[int, int]: - """The shape (height, width) of the arrays to send to the shaper""" - return self.height, self.width - - @property - def pixel_size(self) -> tuple[float, float]: - """The pixel sizes (x, y) in micrometer""" - return self.pixel_size_x, self.pixel_size_y - - @classmethod - def from_parameter(cls, group) -> 'ShaperGeometry': - """Read the group created by :func:`shaper_geometry_params`""" - return cls(int(group['width']), int(group['height']), - float(group['pixel_size_x']), float(group['pixel_size_y'])) - - -def get_module_shaper_geometry(module) -> Optional[ShaperGeometry]: - """The geometry given by a DAQ_Move control module, None if its plugin has no shaper geometry group or did not - fill it yet (not initialized)""" - try: - group = module.settings.child(module._hw_settings_name, SHAPER_GEOMETRY_NAME) - except (KeyError, AttributeError): - return None - geometry = ShaperGeometry.from_parameter(group) - return geometry if geometry.width > 0 and geometry.height > 0 and geometry.pixel_size_x > 0 else None +config = GlobalConfig() def get_shaper() -> Shaper: + """The shaper as described in the configuration, used when the Shaper actuator does not tell (plugin not + initialized or not exposing the shaper settings groups of pymodaq_plugins_utils)""" default_slm = config('beam_shaping', 'SLM', 'default_slm')[0] return Shaper(name=default_slm, width=config('beam_shaping', 'SLM', default_slm, 'width'), @@ -88,4 +17,4 @@ def get_shaper() -> Shaper: has_internal_calibration=config('beam_shaping', 'SLM', default_slm, 'has_internal_calibration'), use_internal_calibration=config('beam_shaping', 'SLM', default_slm, 'use_internal_calibration'), modulator_type=ModulatorType(config('beam_shaping', 'SLM', default_slm, 'modulator_type')[0]) - ) \ No newline at end of file + ) diff --git a/tests/test_shaper_interface.py b/tests/test_shaper_interface.py index 4edf533..06755de 100644 --- a/tests/test_shaper_interface.py +++ b/tests/test_shaper_interface.py @@ -1,14 +1,11 @@ -import pytest -from pymodaq_gui.parameter import Parameter - -from pymodaq_plugins_beam_shaping.utilities.shaper import (ShaperGeometry, shaper_geometry_params, - get_module_shaper_geometry) +import types +from pymodaq_gui.parameter import Parameter -def test_geometry(): - geometry = ShaperGeometry(width=1920, height=1080, pixel_size_x=8., pixel_size_y=9.) - assert geometry.shape == (1080, 1920) - assert geometry.pixel_size == (8., 9.) +from pymodaq_plugins_utils.hardware.shaper import (Shaper, ModulatorType, shaper_params, fill_shaper_settings, + GEOMETRY_NAME, PROPERTIES_NAME) +from pymodaq_plugins_beam_shaping.extensions.spatial_beam_shaping import BeamShaping +from pymodaq_plugins_beam_shaping.utilities.shaper import get_shaper class FakeModule: @@ -20,18 +17,28 @@ def __init__(self, children): {'name': self._hw_settings_name, 'type': 'group', 'children': children}]) -def test_read_from_module(): - module = FakeModule([shaper_geometry_params()]) - assert get_module_shaper_geometry(module) is None # not filled yet: plugin not initialized - group = module.settings.child('actuator_settings', 'shaper_geometry') - for name, value in (('width', 1920), ('height', 1080), ('pixel_size_x', 8.), ('pixel_size_y', 9.)): - group.child(name).setValue(value) - assert get_module_shaper_geometry(module) == ShaperGeometry(1920, 1080, 8., 9.) +def _extension(actuator): + return types.SimpleNamespace(_shaper_actuator=actuator, _shaper=None, has_action=lambda name: False) + + +def test_extension_uses_the_actuator_once_initialized(): + module = FakeModule(shaper_params()) + extension = _extension(module) + assert BeamShaping.shaper.fget(extension) == get_shaper() # not initialized yet: configuration + + fill_shaper_settings(module.settings.child('actuator_settings'), 'My SLM', 1920, 1080, 8., 9., + modulator_type=ModulatorType.PHASE, has_internal_calibration=True) + shaper = BeamShaping.shaper.fget(extension) + assert (shaper.name, shaper.shape, shaper.pixel_size) == ('My SLM', (1080, 1920), 8.) + assert shaper.has_internal_calibration and shaper.use_internal_calibration + + module.settings.child('actuator_settings', PROPERTIES_NAME, 'use_internal_calibration').setValue(False) + assert not BeamShaping.shaper.fget(extension).use_internal_calibration -def test_plugin_without_the_group(): - assert get_module_shaper_geometry(FakeModule([])) is None - assert get_module_shaper_geometry(object()) is None +def test_extension_without_actuator_or_groups(): + assert BeamShaping.shaper.fget(_extension(None)) == get_shaper() + assert BeamShaping.shaper.fget(_extension(FakeModule([]))) == get_shaper() def test_mock_shaper_gives_the_configured_geometry(qapp): @@ -40,7 +47,7 @@ def test_mock_shaper_gives_the_configured_geometry(qapp): from pymodaq_plugins_beam_shaping.utilities import sizing plugin = make_actuator(DAQ_Move_MockShaper) - geometry = ShaperGeometry.from_parameter(plugin.settings.child('shaper_geometry')) - assert geometry.shape == sizing.get_slm_size() - assert geometry.pixel_size_x > 0 + shaper = Shaper.from_settings(plugin.settings.child(GEOMETRY_NAME), plugin.settings.child(PROPERTIES_NAME)) + assert shaper.shape == sizing.get_slm_size() + assert shaper.pixel_size > 0 plugin.close() From 462bb8da9c6fb80876c8ae733b17f94fd944d53a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 10:41:02 +0000 Subject: [PATCH 4/5] Remove the SLM section of the configuration: the shaper comes from the Shaper actuator, with one default shaper as fallback Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01CAhZwmbyCSpSgfTyicUxuS --- .../daq_move_plugins/daq_move_MockShaper.py | 15 ++++----- .../extensions/spatial_beam_shaping.py | 7 ++-- .../field/loaders/from_functions_loader.py | 2 -- .../resources/config_template.toml | 20 ----------- src/pymodaq_plugins_beam_shaping/slm/slm.py | 13 -------- .../utilities/shaper.py | 29 ++++++++-------- .../utilities/sizing.py | 18 ++++------ tests/test_shaper_interface.py | 33 ++++++++++++++----- 8 files changed, 58 insertions(+), 79 deletions(-) delete mode 100644 src/pymodaq_plugins_beam_shaping/slm/slm.py diff --git a/src/pymodaq_plugins_beam_shaping/daq_move_plugins/daq_move_MockShaper.py b/src/pymodaq_plugins_beam_shaping/daq_move_plugins/daq_move_MockShaper.py index a5e2947..0a77b7c 100644 --- a/src/pymodaq_plugins_beam_shaping/daq_move_plugins/daq_move_MockShaper.py +++ b/src/pymodaq_plugins_beam_shaping/daq_move_plugins/daq_move_MockShaper.py @@ -10,7 +10,7 @@ from pymodaq_gui.parameter import Parameter from pymodaq_plugins_beam_shaping.hardware.mock_shaper_camera import ShaperCamera -from pymodaq_plugins_beam_shaping.utilities.shaper import shaper_params, fill_shaper_settings, ModulatorType +from pymodaq_plugins_beam_shaping.utilities.shaper import shaper_params, fill_shaper_settings, DEFAULT_SHAPER from pymodaq_plugins_beam_shaping.utilities import sizing @@ -41,14 +41,11 @@ def ini_attributes(self): self.controller: ShaperCamera = None def set_shaper_geometry(self): - """Fill the shaper settings groups read by the BeamShaping extension""" - height, width = sizing.get_slm_size() - slm = sizing.plugin_config('SLM', 'default_slm')[0] - fill_shaper_settings(self.settings, slm, width, height, - float(sizing.plugin_config('SLM', slm, 'pixel_size')), - modulator_type=ModulatorType(sizing.plugin_config('SLM', slm, 'modulator_type')[0]), - has_internal_calibration=bool(sizing.plugin_config('SLM', slm, - 'has_internal_calibration'))) + """Fill the shaper settings groups read by the BeamShaping extension, with the default shaper""" + shaper = DEFAULT_SHAPER + fill_shaper_settings(self.settings, shaper.name, shaper.width, shaper.height, shaper.pixel_size, + modulator_type=shaper.modulator_type, + has_internal_calibration=shaper.has_internal_calibration) def get_actuator_value(self): """Get the current value from the hardware with scaling conversion. diff --git a/src/pymodaq_plugins_beam_shaping/extensions/spatial_beam_shaping.py b/src/pymodaq_plugins_beam_shaping/extensions/spatial_beam_shaping.py index 4f9cf85..2326e16 100644 --- a/src/pymodaq_plugins_beam_shaping/extensions/spatial_beam_shaping.py +++ b/src/pymodaq_plugins_beam_shaping/extensions/spatial_beam_shaping.py @@ -33,7 +33,7 @@ from pymodaq_plugins_beam_shaping.utilities import regions from pymodaq_plugins_beam_shaping.utilities.calibrating import BeamShapingCalibration, Calibration from pymodaq_plugins_beam_shaping.utilities.data import DataShaper -from pymodaq_plugins_beam_shaping.utilities.shaper import Shaper, ModulatorType, get_shaper +from pymodaq_plugins_beam_shaping.utilities.shaper import Shaper, ModulatorType, DEFAULT_SHAPER, set_shaper logger = set_logger(get_module_name(__file__)) layout_path = get_set_layout_path(user=True) @@ -140,12 +140,13 @@ def update_modulator_field(self, field: Field): ) @property def shaper(self) -> Shaper: - """The shaper as told by the Shaper actuator (once initialized), else the one of the configuration""" + """The shaper as told by the Shaper actuator (once initialized), else the default one""" shaper = None if self._shaper_actuator is not None: shaper = Shaper.from_module(self._shaper_actuator) if shaper is None: - shaper = get_shaper() + shaper = DEFAULT_SHAPER + set_shaper(None if shaper is DEFAULT_SHAPER else shaper) if (self._shaper is None or self._shaper.modulator_type != shaper.modulator_type) and \ self.has_action('calibration'): self.set_action_visible('calibration', shaper.modulator_type == ModulatorType.PHASE) diff --git a/src/pymodaq_plugins_beam_shaping/field/loaders/from_functions_loader.py b/src/pymodaq_plugins_beam_shaping/field/loaders/from_functions_loader.py index a835fcf..9ba2cd5 100644 --- a/src/pymodaq_plugins_beam_shaping/field/loaders/from_functions_loader.py +++ b/src/pymodaq_plugins_beam_shaping/field/loaders/from_functions_loader.py @@ -13,8 +13,6 @@ from pymodaq_plugins_beam_shaping import config as plugin_config -SLM = plugin_config('SLM', 'default_slm') - class BaseFieldLoader(FieldLoader): diff --git a/src/pymodaq_plugins_beam_shaping/resources/config_template.toml b/src/pymodaq_plugins_beam_shaping/resources/config_template.toml index 07cc80f..02874db 100644 --- a/src/pymodaq_plugins_beam_shaping/resources/config_template.toml +++ b/src/pymodaq_plugins_beam_shaping/resources/config_template.toml @@ -21,26 +21,6 @@ setup_type = ['2f', '4f', 'no_lens'] #readonly set from the chosen algorithm lengths = [100, 100, 150, 150] focals = [100, 150] -[SLM] -default_slm = ["PLUTO", "LC2012"] #first elet in the list used as default - - -[SLM.PLUTO] -width = 1920 -height = 1080 -pixel_size = 8 # in micrometer -has_internal_calibration = true -use_internal_calibration = false -modulator_type = ['phase', 'amplitude', 'both'] - -[SLM.LC2012] -width = 1024 -height = 768 -pixel_size = 36 # in micrometer -has_internal_calibration = true -use_internal_calibration = false -modulator_type = ['phase', 'amplitude', 'both'] - [target] default_loader = ["ImageFiles"] #will be populated at startup from the reading of the factory #first elet in the list used as default diff --git a/src/pymodaq_plugins_beam_shaping/slm/slm.py b/src/pymodaq_plugins_beam_shaping/slm/slm.py deleted file mode 100644 index 0c7a255..0000000 --- a/src/pymodaq_plugins_beam_shaping/slm/slm.py +++ /dev/null @@ -1,13 +0,0 @@ -from pymodaq_gui.managers.parameter_manager import ParameterManager - -from pymodaq_plugins_beam_shaping import config as plugin_config - -class SLM(ParameterManager): - - params = [ - {'title': 'SLM:', 'name': 'slm', 'type': 'list', - 'limits': plugin_config.get_children('SLM'), 'value': plugin_config('SLM', 'default_slm')}, - {''} - ] - - diff --git a/src/pymodaq_plugins_beam_shaping/utilities/shaper.py b/src/pymodaq_plugins_beam_shaping/utilities/shaper.py index 132999f..510475d 100644 --- a/src/pymodaq_plugins_beam_shaping/utilities/shaper.py +++ b/src/pymodaq_plugins_beam_shaping/utilities/shaper.py @@ -1,20 +1,23 @@ -from pymodaq_utils.config import GlobalConfig +from typing import Optional # The shaper contract (settings groups of the instrument plugin, Shaper dataclass) lives in pymodaq_plugins_utils from pymodaq_plugins_utils.hardware.shaper import Shaper, ModulatorType, shaper_params, fill_shaper_settings -config = GlobalConfig() +# Used as long as no Shaper actuator tells what the shaper is (actuator absent or not initialized, or a plugin that +# does not provide the shaper settings groups): the same for all the shapers +DEFAULT_SHAPER = Shaper(name='Default shaper', width=1920, height=1080, pixel_size=8., # in micrometer + has_internal_calibration=False, use_internal_calibration=False, + modulator_type=ModulatorType.PHASE) + +_current_shaper: Optional[Shaper] = None + + +def set_shaper(shaper: Optional[Shaper]): + """Set the shaper that get_shaper returns, None to go back to the default one""" + global _current_shaper + _current_shaper = shaper def get_shaper() -> Shaper: - """The shaper as described in the configuration, used when the Shaper actuator does not tell (plugin not - initialized or not exposing the shaper settings groups of pymodaq_plugins_utils)""" - default_slm = config('beam_shaping', 'SLM', 'default_slm')[0] - return Shaper(name=default_slm, - width=config('beam_shaping', 'SLM', default_slm, 'width'), - height=config('beam_shaping', 'SLM', default_slm, 'height'), - pixel_size=config('beam_shaping', 'SLM', default_slm, 'pixel_size'), - has_internal_calibration=config('beam_shaping', 'SLM', default_slm, 'has_internal_calibration'), - use_internal_calibration=config('beam_shaping', 'SLM', default_slm, 'use_internal_calibration'), - modulator_type=ModulatorType(config('beam_shaping', 'SLM', default_slm, 'modulator_type')[0]) - ) + """The shaper last told by the Shaper actuator (see the BeamShaping extension), else the default one""" + return _current_shaper if _current_shaper is not None else DEFAULT_SHAPER diff --git a/src/pymodaq_plugins_beam_shaping/utilities/sizing.py b/src/pymodaq_plugins_beam_shaping/utilities/sizing.py index 7dc0fe3..ec4e736 100644 --- a/src/pymodaq_plugins_beam_shaping/utilities/sizing.py +++ b/src/pymodaq_plugins_beam_shaping/utilities/sizing.py @@ -1,6 +1,7 @@ import numpy as np from pymodaq_utils.math_utils import greater2n, is_power_of_two from pymodaq_plugins_beam_shaping import config as plugin_config +from pymodaq_plugins_beam_shaping.utilities.shaper import get_shaper def get_binning() -> int: @@ -14,9 +15,7 @@ def get_effective_needed_field_size() -> tuple[int, int]: if not is_power_of_two(binning): raise ValueError('binning must be a multiple of 2') if plugin_config('sizing', 'square_size'): - needed_field_size = (greater2n(max(plugin_config('SLM', plugin_config('SLM', 'default_slm')[0], 'height'), - plugin_config('SLM', plugin_config('SLM', 'default_slm')[0], 'width'))) // - binning) + needed_field_size = greater2n(max(get_slm_size())) // binning needed_field_size = (needed_field_size, needed_field_size) else: needed_field_size = tuple(np.array(get_slm_size()) // binning) @@ -24,25 +23,22 @@ def get_effective_needed_field_size() -> tuple[int, int]: def get_slm_size() -> tuple[int, int]: - return (plugin_config('SLM', plugin_config('SLM', 'default_slm')[0], 'height'), - plugin_config('SLM', plugin_config('SLM', 'default_slm')[0], 'width')) + """The shape (height, width) of the shaper in pixels""" + return get_shaper().shape def get_effective_slm_size() -> tuple[int, int]: binning = plugin_config('sizing', 'binning') if not is_power_of_two(binning): raise ValueError('binning must be a multiple of 2') - size = (np.array([plugin_config('SLM', plugin_config('SLM', 'default_slm')[0], 'height'), - plugin_config('SLM', plugin_config('SLM', 'default_slm')[0], 'width')]) // - binning) + size = np.array(get_slm_size()) // binning return int(size[0]), int(size[1]) def get_effective_slm_pixel_size() -> float: - """ Compute from the configuration values the effective SLM pixel size in microns""" + """ Compute the effective SLM pixel size in microns (the one of the shaper times the binning)""" - return (plugin_config('SLM', plugin_config('SLM', 'default_slm')[0], 'pixel_size') * - plugin_config('sizing', 'binning')) + return get_shaper().pixel_size * plugin_config('sizing', 'binning') def unbin_to_real_slm(phase: np.ndarray, binning: int = None) -> np.ndarray: diff --git a/tests/test_shaper_interface.py b/tests/test_shaper_interface.py index 06755de..705d01b 100644 --- a/tests/test_shaper_interface.py +++ b/tests/test_shaper_interface.py @@ -1,11 +1,13 @@ import types +import pytest + from pymodaq_gui.parameter import Parameter from pymodaq_plugins_utils.hardware.shaper import (Shaper, ModulatorType, shaper_params, fill_shaper_settings, GEOMETRY_NAME, PROPERTIES_NAME) from pymodaq_plugins_beam_shaping.extensions.spatial_beam_shaping import BeamShaping -from pymodaq_plugins_beam_shaping.utilities.shaper import get_shaper +from pymodaq_plugins_beam_shaping.utilities.shaper import DEFAULT_SHAPER, get_shaper, set_shaper class FakeModule: @@ -21,33 +23,48 @@ def _extension(actuator): return types.SimpleNamespace(_shaper_actuator=actuator, _shaper=None, has_action=lambda name: False) +@pytest.fixture(autouse=True) +def reset_shaper(): + set_shaper(None) + yield + set_shaper(None) + + def test_extension_uses_the_actuator_once_initialized(): module = FakeModule(shaper_params()) extension = _extension(module) - assert BeamShaping.shaper.fget(extension) == get_shaper() # not initialized yet: configuration + assert BeamShaping.shaper.fget(extension) is DEFAULT_SHAPER # not initialized yet: default shaper + assert get_shaper() is DEFAULT_SHAPER fill_shaper_settings(module.settings.child('actuator_settings'), 'My SLM', 1920, 1080, 8., 9., modulator_type=ModulatorType.PHASE, has_internal_calibration=True) shaper = BeamShaping.shaper.fget(extension) assert (shaper.name, shaper.shape, shaper.pixel_size) == ('My SLM', (1080, 1920), 8.) assert shaper.has_internal_calibration and shaper.use_internal_calibration + assert get_shaper() is shaper # what the sizing functions use module.settings.child('actuator_settings', PROPERTIES_NAME, 'use_internal_calibration').setValue(False) assert not BeamShaping.shaper.fget(extension).use_internal_calibration def test_extension_without_actuator_or_groups(): - assert BeamShaping.shaper.fget(_extension(None)) == get_shaper() - assert BeamShaping.shaper.fget(_extension(FakeModule([]))) == get_shaper() + assert BeamShaping.shaper.fget(_extension(None)) is DEFAULT_SHAPER + assert BeamShaping.shaper.fget(_extension(FakeModule([]))) is DEFAULT_SHAPER -def test_mock_shaper_gives_the_configured_geometry(qapp): +def test_mock_shaper_gives_the_default_shaper(qapp): from pymodaq.utils.plugin_testing import make_actuator from pymodaq_plugins_beam_shaping.daq_move_plugins.daq_move_MockShaper import DAQ_Move_MockShaper - from pymodaq_plugins_beam_shaping.utilities import sizing plugin = make_actuator(DAQ_Move_MockShaper) shaper = Shaper.from_settings(plugin.settings.child(GEOMETRY_NAME), plugin.settings.child(PROPERTIES_NAME)) - assert shaper.shape == sizing.get_slm_size() - assert shaper.pixel_size > 0 + assert shaper == DEFAULT_SHAPER plugin.close() + + +def test_sizes_follow_the_shaper(): + from pymodaq_plugins_beam_shaping.utilities import sizing + assert sizing.get_slm_size() == DEFAULT_SHAPER.shape + set_shaper(Shaper('Other', 640, 480, 12., False, False, ModulatorType.PHASE)) + assert sizing.get_slm_size() == (480, 640) + assert sizing.get_effective_slm_pixel_size() == 12. * sizing.get_binning() From 24da348bc37ee7d8d4c0581e42d84abe6a4ef34f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 10:47:15 +0000 Subject: [PATCH 5/5] Read the shaper after the experiment is set and when the Shaper actuator is initialized, update the fields; default shaper editable in the configuration Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01CAhZwmbyCSpSgfTyicUxuS --- .../daq_move_plugins/daq_move_MockShaper.py | 4 +-- .../extensions/spatial_beam_shaping.py | 27 ++++++++++++-- .../resources/config_template.toml | 11 ++++++ .../utilities/shaper.py | 23 ++++++++---- tests/test_custom_apps.py | 35 +++++++++++++++++++ tests/test_shaper_interface.py | 22 ++++++++---- 6 files changed, 104 insertions(+), 18 deletions(-) diff --git a/src/pymodaq_plugins_beam_shaping/daq_move_plugins/daq_move_MockShaper.py b/src/pymodaq_plugins_beam_shaping/daq_move_plugins/daq_move_MockShaper.py index 0a77b7c..0e1660a 100644 --- a/src/pymodaq_plugins_beam_shaping/daq_move_plugins/daq_move_MockShaper.py +++ b/src/pymodaq_plugins_beam_shaping/daq_move_plugins/daq_move_MockShaper.py @@ -10,7 +10,7 @@ from pymodaq_gui.parameter import Parameter from pymodaq_plugins_beam_shaping.hardware.mock_shaper_camera import ShaperCamera -from pymodaq_plugins_beam_shaping.utilities.shaper import shaper_params, fill_shaper_settings, DEFAULT_SHAPER +from pymodaq_plugins_beam_shaping.utilities.shaper import shaper_params, fill_shaper_settings, get_default_shaper from pymodaq_plugins_beam_shaping.utilities import sizing @@ -42,7 +42,7 @@ def ini_attributes(self): def set_shaper_geometry(self): """Fill the shaper settings groups read by the BeamShaping extension, with the default shaper""" - shaper = DEFAULT_SHAPER + shaper = get_default_shaper() fill_shaper_settings(self.settings, shaper.name, shaper.width, shaper.height, shaper.pixel_size, modulator_type=shaper.modulator_type, has_internal_calibration=shaper.has_internal_calibration) diff --git a/src/pymodaq_plugins_beam_shaping/extensions/spatial_beam_shaping.py b/src/pymodaq_plugins_beam_shaping/extensions/spatial_beam_shaping.py index 2326e16..0712db3 100644 --- a/src/pymodaq_plugins_beam_shaping/extensions/spatial_beam_shaping.py +++ b/src/pymodaq_plugins_beam_shaping/extensions/spatial_beam_shaping.py @@ -33,7 +33,7 @@ from pymodaq_plugins_beam_shaping.utilities import regions from pymodaq_plugins_beam_shaping.utilities.calibrating import BeamShapingCalibration, Calibration from pymodaq_plugins_beam_shaping.utilities.data import DataShaper -from pymodaq_plugins_beam_shaping.utilities.shaper import Shaper, ModulatorType, DEFAULT_SHAPER, set_shaper +from pymodaq_plugins_beam_shaping.utilities.shaper import Shaper, ModulatorType, get_default_shaper, set_shaper logger = set_logger(get_module_name(__file__)) layout_path = get_set_layout_path(user=True) @@ -121,10 +121,25 @@ def get_slm_slices(self) -> tuple[slice, slice]: def do_things_after_experiment_set(self, experiment_name: str): super().do_things_after_experiment_set(experiment_name) + self._disconnect_shaper_actuator() if self.modules_manager is not None and 'Shaper' in self.modules_manager.actuators_name: self._shaper_actuator = self.modules_manager.get_mod_from_name('Shaper', 'act') + # the plugin tells what the shaper is when it is initialized, possibly after the experiment is set + self._shaper_actuator.init_signal.connect(self._shaper_actuator_initialized) else: self._shaper_actuator = None + self.update_from_shaper() + + def _disconnect_shaper_actuator(self): + if self._shaper_actuator is not None: + try: + self._shaper_actuator.init_signal.disconnect(self._shaper_actuator_initialized) + except (RuntimeError, TypeError): # the module is gone or was not connected + pass + + def _shaper_actuator_initialized(self, initialized: bool): + if initialized: + self.update_from_shaper() def update_modulator_field(self, field: Field): """ field contains here the modulator field""" @@ -144,9 +159,9 @@ def shaper(self) -> Shaper: shaper = None if self._shaper_actuator is not None: shaper = Shaper.from_module(self._shaper_actuator) + set_shaper(shaper) # None: the default shaper if shaper is None: - shaper = DEFAULT_SHAPER - set_shaper(None if shaper is DEFAULT_SHAPER else shaper) + shaper = get_default_shaper() if (self._shaper is None or self._shaper.modulator_type != shaper.modulator_type) and \ self.has_action('calibration'): self.set_action_visible('calibration', shaper.modulator_type == ModulatorType.PHASE) @@ -633,6 +648,12 @@ def update_target_loader_from_algo(self, algo: AlgoBase): self._target_field_loader.update_pixels(algo.get_target_pixels_size(field_size)) def do_things_after_config_changed(self): + self.update_from_shaper() + + def update_from_shaper(self): + """Read the shaper again (Shaper actuator or default one) and update what depends on it: the fields""" + if self._input_field_loader is None or self._target_field_loader is None or self._algorithm is None: + return # the user interface is not set up yet self._shaper = None # make sure to check if the shaper object should be reinit self._input_field_loader.updated_slm(self.shaper.name) self._input_field_loader.update_apply_mask(size=self.slm_shape, apply=True) diff --git a/src/pymodaq_plugins_beam_shaping/resources/config_template.toml b/src/pymodaq_plugins_beam_shaping/resources/config_template.toml index 02874db..bb75d1f 100644 --- a/src/pymodaq_plugins_beam_shaping/resources/config_template.toml +++ b/src/pymodaq_plugins_beam_shaping/resources/config_template.toml @@ -21,6 +21,17 @@ setup_type = ['2f', '4f', 'no_lens'] #readonly set from the chosen algorithm lengths = [100, 100, 150, 150] focals = [100, 150] +# Shaper used as long as the Shaper actuator does not tell what the shaper is (actuator not initialized, or a plugin +# that does not provide it): the same for all the shapers +[default_shaper] +name = "Default shaper" +width = 1920 +height = 1080 +pixel_size = 8.0 # in micrometer +has_internal_calibration = false +use_internal_calibration = false +modulator_type = ['phase', 'amplitude', 'both'] #first elet in the list used as default + [target] default_loader = ["ImageFiles"] #will be populated at startup from the reading of the factory #first elet in the list used as default diff --git a/src/pymodaq_plugins_beam_shaping/utilities/shaper.py b/src/pymodaq_plugins_beam_shaping/utilities/shaper.py index 510475d..31bd5e7 100644 --- a/src/pymodaq_plugins_beam_shaping/utilities/shaper.py +++ b/src/pymodaq_plugins_beam_shaping/utilities/shaper.py @@ -3,15 +3,26 @@ # The shaper contract (settings groups of the instrument plugin, Shaper dataclass) lives in pymodaq_plugins_utils from pymodaq_plugins_utils.hardware.shaper import Shaper, ModulatorType, shaper_params, fill_shaper_settings -# Used as long as no Shaper actuator tells what the shaper is (actuator absent or not initialized, or a plugin that -# does not provide the shaper settings groups): the same for all the shapers -DEFAULT_SHAPER = Shaper(name='Default shaper', width=1920, height=1080, pixel_size=8., # in micrometer - has_internal_calibration=False, use_internal_calibration=False, - modulator_type=ModulatorType.PHASE) +from pymodaq_plugins_beam_shaping import config _current_shaper: Optional[Shaper] = None +def get_default_shaper() -> Shaper: + """The shaper described in the ``default_shaper`` section of the configuration file + + It is used as long as no Shaper actuator tells what the shaper is (actuator absent or not initialized, or a plugin + that does not provide the shaper settings groups): the same for all the shapers. + """ + return Shaper(name=config('default_shaper', 'name'), + width=config('default_shaper', 'width'), + height=config('default_shaper', 'height'), + pixel_size=config('default_shaper', 'pixel_size'), + has_internal_calibration=config('default_shaper', 'has_internal_calibration'), + use_internal_calibration=config('default_shaper', 'use_internal_calibration'), + modulator_type=ModulatorType(config('default_shaper', 'modulator_type')[0])) + + def set_shaper(shaper: Optional[Shaper]): """Set the shaper that get_shaper returns, None to go back to the default one""" global _current_shaper @@ -20,4 +31,4 @@ def set_shaper(shaper: Optional[Shaper]): def get_shaper() -> Shaper: """The shaper last told by the Shaper actuator (see the BeamShaping extension), else the default one""" - return _current_shaper if _current_shaper is not None else DEFAULT_SHAPER + return _current_shaper if _current_shaper is not None else get_default_shaper() diff --git a/tests/test_custom_apps.py b/tests/test_custom_apps.py index b169844..6cb7e6f 100644 --- a/tests/test_custom_apps.py +++ b/tests/test_custom_apps.py @@ -1,5 +1,7 @@ """Create and use the CustomApp / CustomExt of the package, as the Dashboard does, to catch the incompatibilities with the PyMoDAQ release (base classes, viewers, ROI API)""" +import types + import pytest from qtpy import QtWidgets @@ -78,3 +80,36 @@ def test_extension_on_empty_dashboard(qtbot): assert roi.pos()[0] * 1e6 == pytest.approx(pos[1] + size[1] / 6, rel=1e-3) assert extension.quit_fun() + + +def test_extension_updates_the_fields_from_the_shaper(qtbot): + from pymodaq.dashboard import create_load_dashboard + from pymodaq.utils.gui_utils.loader_utils import create_extension + from pymodaq_plugins_beam_shaping.extensions.spatial_beam_shaping import BeamShaping + from pymodaq_plugins_beam_shaping.utilities.shaper import (ModulatorType, set_shaper, shaper_params, + fill_shaper_settings) + from pymodaq_gui.parameter import Parameter + from pymodaq_plugins_beam_shaping.utilities import sizing + + _, dashboard = create_load_dashboard() + win_ext, extension = create_extension(dashboard, BeamShaping) + try: + before = extension.input_field.shape + + # a Shaper actuator that tells what the shaper is, as the extension reads it once initialized + actuator = types.SimpleNamespace(_hw_settings_name='actuator_settings', settings=Parameter.create( + name='settings', type='group', children=[ + {'name': 'actuator_settings', 'type': 'group', 'children': shaper_params()}])) + fill_shaper_settings(actuator.settings.child('actuator_settings'), 'Other', 512, 256, 12., + modulator_type=ModulatorType.PHASE) + extension._shaper_actuator = actuator + extension.update_from_shaper() + + assert extension.shaper.name == 'Other' + assert sizing.get_slm_size() == (256, 512) + assert extension.slm_shape == (256 // 2, 512 // 2) # binned + assert extension.input_field.shape != before + assert extension.input_field.shape == sizing.get_effective_needed_field_size() + finally: + set_shaper(None) + extension.quit_fun() diff --git a/tests/test_shaper_interface.py b/tests/test_shaper_interface.py index 705d01b..be3b8d6 100644 --- a/tests/test_shaper_interface.py +++ b/tests/test_shaper_interface.py @@ -7,7 +7,7 @@ from pymodaq_plugins_utils.hardware.shaper import (Shaper, ModulatorType, shaper_params, fill_shaper_settings, GEOMETRY_NAME, PROPERTIES_NAME) from pymodaq_plugins_beam_shaping.extensions.spatial_beam_shaping import BeamShaping -from pymodaq_plugins_beam_shaping.utilities.shaper import DEFAULT_SHAPER, get_shaper, set_shaper +from pymodaq_plugins_beam_shaping.utilities.shaper import get_default_shaper, get_shaper, set_shaper class FakeModule: @@ -33,8 +33,8 @@ def reset_shaper(): def test_extension_uses_the_actuator_once_initialized(): module = FakeModule(shaper_params()) extension = _extension(module) - assert BeamShaping.shaper.fget(extension) is DEFAULT_SHAPER # not initialized yet: default shaper - assert get_shaper() is DEFAULT_SHAPER + assert BeamShaping.shaper.fget(extension) == get_default_shaper() # not initialized yet: default shaper + assert get_shaper() == get_default_shaper() fill_shaper_settings(module.settings.child('actuator_settings'), 'My SLM', 1920, 1080, 8., 9., modulator_type=ModulatorType.PHASE, has_internal_calibration=True) @@ -48,8 +48,8 @@ def test_extension_uses_the_actuator_once_initialized(): def test_extension_without_actuator_or_groups(): - assert BeamShaping.shaper.fget(_extension(None)) is DEFAULT_SHAPER - assert BeamShaping.shaper.fget(_extension(FakeModule([]))) is DEFAULT_SHAPER + assert BeamShaping.shaper.fget(_extension(None)) == get_default_shaper() + assert BeamShaping.shaper.fget(_extension(FakeModule([]))) == get_default_shaper() def test_mock_shaper_gives_the_default_shaper(qapp): @@ -58,13 +58,21 @@ def test_mock_shaper_gives_the_default_shaper(qapp): plugin = make_actuator(DAQ_Move_MockShaper) shaper = Shaper.from_settings(plugin.settings.child(GEOMETRY_NAME), plugin.settings.child(PROPERTIES_NAME)) - assert shaper == DEFAULT_SHAPER + assert shaper == get_default_shaper() plugin.close() +def test_default_shaper_from_the_configuration(): + from pymodaq_plugins_beam_shaping import config + default = get_default_shaper() + assert (default.width, default.height) == (config('default_shaper', 'width'), config('default_shaper', 'height')) + assert default.modulator_type == ModulatorType.PHASE + assert not default.has_internal_calibration + + def test_sizes_follow_the_shaper(): from pymodaq_plugins_beam_shaping.utilities import sizing - assert sizing.get_slm_size() == DEFAULT_SHAPER.shape + assert sizing.get_slm_size() == get_default_shaper().shape set_shaper(Shaper('Other', 640, 480, 12., False, False, ModulatorType.PHASE)) assert sizing.get_slm_size() == (480, 640) assert sizing.get_effective_slm_pixel_size() == 12. * sizing.get_binning()