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 6d38524..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,6 +10,8 @@ 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, get_default_shaper +from pymodaq_plugins_beam_shaping.utilities import sizing class DAQ_Move_MockShaper(DAQ_Move_base): @@ -32,12 +34,19 @@ class DAQ_Move_MockShaper(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_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 settings groups read by the BeamShaping extension, with the 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) + def get_actuator_value(self): """Get the current value from the hardware with scaling conversion. @@ -79,6 +88,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/extensions/spatial_beam_shaping.py b/src/pymodaq_plugins_beam_shaping/extensions/spatial_beam_shaping.py index df34255..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, get_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""" @@ -140,11 +155,18 @@ 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 default one""" + 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 = 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) + 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 @@ -626,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/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..bb75d1f 100644 --- a/src/pymodaq_plugins_beam_shaping/resources/config_template.toml +++ b/src/pymodaq_plugins_beam_shaping/resources/config_template.toml @@ -21,25 +21,16 @@ 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] +# 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 # 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 +pixel_size = 8.0 # in micrometer +has_internal_calibration = false use_internal_calibration = false -modulator_type = ['phase', 'amplitude', 'both'] +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 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 0d16e0d..31bd5e7 100644 --- a/src/pymodaq_plugins_beam_shaping/utilities/shaper.py +++ b/src/pymodaq_plugins_beam_shaping/utilities/shaper.py @@ -1,35 +1,34 @@ -import dataclasses +from typing import Optional -from pymodaq_utils.enums import StrEnum -from pymodaq_utils.config import GlobalConfig +# 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() +from pymodaq_plugins_beam_shaping import config +_current_shaper: Optional[Shaper] = None -class ModulatorType(StrEnum): - PHASE = 'phase' - AMPLITUDE = 'amplitude' - BOTH = 'both' +def get_default_shaper() -> Shaper: + """The shaper described in the ``default_shaper`` section of the configuration file -@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 + 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 + _current_shaper = shaper def get_shaper() -> Shaper: - 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]) - ) \ No newline at end of file + """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 get_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_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 new file mode 100644 index 0000000..be3b8d6 --- /dev/null +++ b/tests/test_shaper_interface.py @@ -0,0 +1,78 @@ +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_default_shaper, get_shaper, set_shaper + + +class FakeModule: + """What the extension has: the DAQ_Move control module, with the settings tree of its plugin""" + _hw_settings_name = 'actuator_settings' + + def __init__(self, children): + self.settings = Parameter.create(name='settings', type='group', children=[ + {'name': self._hw_settings_name, 'type': 'group', 'children': children}]) + + +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_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) + 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_default_shaper() + assert BeamShaping.shaper.fget(_extension(FakeModule([]))) == get_default_shaper() + + +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 + + plugin = make_actuator(DAQ_Move_MockShaper) + shaper = Shaper.from_settings(plugin.settings.child(GEOMETRY_NAME), plugin.settings.child(PROPERTIES_NAME)) + 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() == 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()