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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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.

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"""
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@

from pymodaq_plugins_beam_shaping import config as plugin_config

SLM = plugin_config('SLM', 'default_slm')



class BaseFieldLoader(FieldLoader):
Expand Down
23 changes: 7 additions & 16 deletions src/pymodaq_plugins_beam_shaping/resources/config_template.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 0 additions & 13 deletions src/pymodaq_plugins_beam_shaping/slm/slm.py

This file was deleted.

51 changes: 25 additions & 26 deletions src/pymodaq_plugins_beam_shaping/utilities/shaper.py
Original file line number Diff line number Diff line change
@@ -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])
)
"""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()
18 changes: 7 additions & 11 deletions src/pymodaq_plugins_beam_shaping/utilities/sizing.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -14,35 +15,30 @@ 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)
return needed_field_size


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:
Expand Down
35 changes: 35 additions & 0 deletions tests/test_custom_apps.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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()
Loading
Loading