diff --git a/docs/src/user_guide/calibration.rst b/docs/src/user_guide/calibration.rst index 5e5926d..c74c6ad 100644 --- a/docs/src/user_guide/calibration.rst +++ b/docs/src/user_guide/calibration.rst @@ -29,7 +29,9 @@ Calibration Procedure --------------------- 1. **Open the Calibration Tool**: Click **Calibration** |equalizer| in the toolbar or navigate to **Shaping > Calibration > Calibration**. -2. **Select the SLM**: Ensure the **Shaper** module is loaded in PyMoDAQ’s Dashboard. +2. **Select the SLM and the camera**: Ensure an actuator named **Shaper** (the SLM) and a detector named **Camera** + (seeing the fringes) are loaded in PyMoDAQ’s Dashboard. The scan configuration (``BeamShapingCalibration`` + scanner, ``Shaper`` and ``Camera`` selected) is a Scan Manager entry shipped with the package. 3. **Run Calibration**: The tool performs a **double-slit experiment**: diff --git a/src/pymodaq_plugins_beam_shaping/resources/holography_calibration.scan b/src/pymodaq_plugins_beam_shaping/resources/holography_calibration.scan new file mode 100644 index 0000000..59c1b3f Binary files /dev/null and b/src/pymodaq_plugins_beam_shaping/resources/holography_calibration.scan differ diff --git a/src/pymodaq_plugins_beam_shaping/resources/holography_calibration.xml b/src/pymodaq_plugins_beam_shaping/resources/holography_calibration.xml deleted file mode 100644 index 4a9c751..0000000 --- a/src/pymodaq_plugins_beam_shaping/resources/holography_calibration.xml +++ /dev/null @@ -1 +0,0 @@ -['Camera']['Shaper']000256str('Scan1D')str('BeamShapingCalibration')1025510010101[][]011000str('scan')0000str('h5py')11http://hsds.sebastienweber.frpymodaq_userpymodaq000C:\DataScanC:\Datanp.nanstr('zlib')5 \ No newline at end of file diff --git a/src/pymodaq_plugins_beam_shaping/scanners/calibration.py b/src/pymodaq_plugins_beam_shaping/scanners/calibration.py index e999b4a..f0af671 100644 --- a/src/pymodaq_plugins_beam_shaping/scanners/calibration.py +++ b/src/pymodaq_plugins_beam_shaping/scanners/calibration.py @@ -4,7 +4,6 @@ @author: Sebastien Weber """ -from cProfile import label from typing import List, Tuple, Any, TYPE_CHECKING import re import numpy as np diff --git a/src/pymodaq_plugins_beam_shaping/scripts/make_calibration_entry.py b/src/pymodaq_plugins_beam_shaping/scripts/make_calibration_entry.py new file mode 100644 index 0000000..4463be4 --- /dev/null +++ b/src/pymodaq_plugins_beam_shaping/scripts/make_calibration_entry.py @@ -0,0 +1,56 @@ +"""Generate the Scan Manager entry used by the SLM calibration (resources/holography_calibration.scan) + +The entry selects the detector named Camera, the actuator named Shaper and the BeamShapingCalibration scanner. Only the +names matter: mock modules are loaded in a Dashboard to create the entry with the Scan Manager itself, so it stays +valid whatever the real camera and SLM are. Run it again if the format of the entries changes with PyMoDAQ. +""" +import sys +import time +from pathlib import Path + +from pymodaq.control_modules.enums import DAQTypesEnum +from pymodaq.utils.managers.modules import ModuleType +from pymodaq.utils.managers.modules.loader import PluginInfo + +from pymodaq_plugins_beam_shaping.utilities.calibrating import CALIBRATION_ENTRY + + +def create_dashboard_with_camera_and_shaper(qapp, timeout=20.): + """ Create a Dashboard with a detector named Camera and an actuator named Shaper (mock ones)""" + from pymodaq.dashboard import create_load_dashboard + + _, dashboard = create_load_dashboard() + dashboard.module_creator.load_created_modules([ + [PluginInfo(id=1, name='Shaper', class_name='MockShaper', type=ModuleType.Actuator, do_init=False)], + [PluginInfo(id=2, name='Camera', class_name='MockShaperCamera', type=ModuleType.Detector, do_init=False, + daq_type=DAQTypesEnum.DAQ2D)]]) + start = time.time() + while time.time() - start < timeout and len(dashboard.modules_manager.modules_all) < 2: + qapp.processEvents() + return dashboard + + +def main(entry_path: Path = CALIBRATION_ENTRY): + from pymodaq_gui.qt_utils import mkQApp + from pymodaq.extensions import DAQScan + from pymodaq.utils.gui_utils.loader_utils import create_extension + + qapp = mkQApp('CalibrationEntry') + dashboard = create_dashboard_with_camera_and_shaper(qapp) + _, daq_scan = create_extension(dashboard, DAQScan) + + scan_manager = daq_scan.scan_manager + modules = scan_manager.modules_manager + for name, selected in (('detectors', ['Camera']), ('actuators', ['Shaper'])): + modules.settings[name] = dict(all_items=modules.settings[name]['all_items'], selected=selected) + scan_manager.scanner.settings.child('scan_type').setValue('Scan1D') + scan_manager.scanner.settings.child('scan_sub_type').setValue('BeamShapingCalibration') + + entry_path.unlink(missing_ok=True) + scan_manager.save_entries(entry_path) + print(f'Entry saved in {entry_path}') + + +if __name__ == '__main__': + main() + sys.exit(0) diff --git a/src/pymodaq_plugins_beam_shaping/utilities/calibrating.py b/src/pymodaq_plugins_beam_shaping/utilities/calibrating.py index f7168b2..4033871 100644 --- a/src/pymodaq_plugins_beam_shaping/utilities/calibrating.py +++ b/src/pymodaq_plugins_beam_shaping/utilities/calibrating.py @@ -32,9 +32,9 @@ logger = set_logger(get_module_name(__file__)) -# TODO: the xml file resources/holography_calibration.xml was a scan configuration of the pymodaq < 5.3 scanner manager -# (ScanManager.get_scanner_folder no longer exists). It has to be rewritten as an entry of the scan manager of 5.3 -# (folder given by ScanManager.get_entry_folder) before BeamShapingCalibration can be used again. +# Scan Manager entry (pymodaq >= 5.3) selecting the detector named Camera, the actuator named Shaper and the +# BeamShapingCalibration scanner. It is generated by scripts/make_calibration_entry.py +CALIBRATION_ENTRY = Path(__file__).parents[1].joinpath('resources', 'holography_calibration.scan') def check_monotonic(phases: np.ndarray) -> bool: @@ -160,7 +160,7 @@ def do_things_after_ui_setup(self): self.show_widgets(False) def show_widgets(self, show=True): - for widget in (self.ui.dock_command, self.ui.get_toolbar('scan_manager')): + for widget in (self.dock_command, self.get_toolbar('scan_manager')): widget.setVisible(show) def do_things_after_experiment_set(self, experiment_name: str): @@ -175,11 +175,7 @@ def do_things_after_experiment_set(self, experiment_name: str): if hasattr(self, 'scan_manager'): #could happen because base class calls do_things_after_experiment_set before # scan_manager is set - self.scan_manager.entry = 'holography_calibration' - self.scan_manager.execute_entry() - - QtWidgets.QApplication.processEvents() - + self.scan_manager.execute_entry(CALIBRATION_ENTRY) self.connect_start_stop_step() def connect_start_stop_step(self): @@ -227,6 +223,7 @@ def connect_things(self): self.scan_done_signal.connect(self.save_calibration) def save_calibration(self): + dwa_calibration = None with DataLoader(self.h5saver) as loader: try: dwa_calibration = loader.load_data_from_name_origin( @@ -265,7 +262,8 @@ def save_calibration(self): saver.add_data(where=saver.raw_group, data=dwa_calibration) except NameError: pass - self.show_calibration(show=True, calibration_dwa=dwa_calibration) + if dwa_calibration is not None: + self.show_calibration(show=True, calibration_dwa=dwa_calibration) @classmethod def get_calibration_dwa(cls) -> DataWithAxes: diff --git a/tests/test_calibration.py b/tests/test_calibration.py new file mode 100644 index 0000000..cadaeef --- /dev/null +++ b/tests/test_calibration.py @@ -0,0 +1,50 @@ +"""The SLM calibration is a DAQScan whose scan configuration comes from a Scan Manager entry""" +import numpy as np +import pytest + +from pymodaq_data import DataRaw, DataToExport + + +def test_calibration_scanner_phase_from_fringes(qtbot): + from types import SimpleNamespace + from pymodaq.utils.scanner.scanner import Scanner + import pymodaq_plugins_beam_shaping.scanners.calibration # registers the scanner # noqa: F401 + + shaper = SimpleNamespace(title='Shaper', units='') + scanner = Scanner(actuators=[shaper], selected_actuators=[shaper]) + scanner.settings.child('scan_type').setValue('Scan1D') + scanner.settings.child('scan_sub_type').setValue('BeamShapingCalibration') + calibration = scanner.scanner + assert calibration.n_steps == 256 + + data = calibration.data_actuator_at(100) # grey level on the right half of the SLM + assert data.as_grey_levels + assert set(np.unique(data[0])) == {0, 100} + + x = np.arange(1024) + phases = [] + for grey in range(0, 256, 32): + fringes = 100 + 50 * np.cos(2 * np.pi * x / 16 + grey / 255 * 3 * np.pi) + dwa = DataRaw('Camera', data=[np.tile(fringes, (50, 1))]) + phase = calibration.process_data(DataToExport('Camera', data=[dwa])).get_data_from_name('Phase') + phases.append(phase[0][0]) + assert np.unwrap(np.array(phases) - phases[0])[-1] == pytest.approx(224 / 255 * 3 * np.pi, rel=1e-2) + + +def test_calibration_extension_uses_scan_manager_entry(qtbot, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) # the DAQScan creates its h5 file with a relative path on Linux + from pymodaq_gui.qt_utils import mkQApp + from pymodaq.utils.gui_utils.loader_utils import create_extension + from pymodaq_plugins_beam_shaping.scripts.make_calibration_entry import create_dashboard_with_camera_and_shaper + from pymodaq_plugins_beam_shaping.utilities.calibrating import BeamShapingCalibration, CALIBRATION_ENTRY + + assert CALIBRATION_ENTRY.is_file() + dashboard = create_dashboard_with_camera_and_shaper(mkQApp('tests')) + win, calibration = create_extension(dashboard, BeamShapingCalibration) + calibration.do_things_after_experiment_set('test') + + assert calibration.scan_manager.settings is not None + assert calibration.scanner.settings['scan_type'] == 'Scan1D' + assert calibration.scanner.settings['scan_sub_type'] == 'BeamShapingCalibration' + assert calibration.modules_manager.selected_actuators_name == ['Shaper'] + assert calibration.modules_manager.selected_detectors_name == ['Camera']