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
13 changes: 11 additions & 2 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,14 @@ A set of mock instruments that simulate one experiment together (beam steering,
setup has an experiment file in `resources/`.

**Recipe**:
A cookbook example of a device pattern: an actuator and a 0D detector, with their driver in `hardware/`, that run
on a mock device and are meant to be copied and adapted.
A cookbook example of a device pattern: plugins (an actuator and a detector, 0D or 1D) with their driver in
`hardware/`, that run on a mock device and are meant to be copied and adapted. The camera recipe is the exception: it
is an existing mock plugin (`BSCamera`) completed to show the pattern.

**Hardware ROI**:
The region of the sensor read and binned by the camera itself, in sensor pixels; the image is smaller. Opposed to the
*software ROI* drawn in the viewer and applied by the plugin after the grab (`RoiStuff`).

**Calibrated axis**:
The axis sent with the data of a detector, in physical units (um, nm), as the instrument gives it. It can change
between two grabs (ROI, binning, central wavelength), so it is asked at every grab.
25 changes: 17 additions & 8 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ Viewer1D

* **MockRandom** (`gaussian scan`_): simulates a shuffled Gaussian; shows the sort button of the 1D viewer.
* **Harmonics** (`harmonics`_): simulates the harmonics spectrum of the source; shows pint units and roi_select.
* **Mock_spectro** (`spectrometer`_): simulates a spectrometer with laser line and exposure; shows a calibrated wavelength axis (nm).
* **MockSpectroErrors** (`error tests`_): simulates the spectrometer averaged over 10 acquisitions; shows data with errors from averaging.
* **CookbookSpectrometer** (`spectrometer`_): simulates a spectrometer with grating, laser line and exposure; shows a calibrated wavelength axis (nm) and the interface of the spectrometer extension. It replaces *Mock_spectro*, removed in this version: a saved preset that uses it must pick *CookbookSpectrometer* instead.
* **MockSpectroErrors** (`error tests`_): simulates a spectrometer that averages itself (hardware averaging); shows how a plugin fills the errors of the data with the standard error of the mean, as PyMoDAQ does when it averages (PyMoDAQ PR 1401).

Viewer2D
++++++++
Expand Down Expand Up @@ -148,7 +148,7 @@ plugin to stop it), they load in the Dashboard of PyMoDAQ 5.3.

**Spectrometer with a calibrated axis**

- 1D *Mock_spectro*
- 1D *CookbookSpectrometer*, *MockSpectroErrors*; actuator *CookbookSpectrometer*
- none
- A wavelength axis that follows a setting, data with errors
* - .. _actuator interfaces:
Expand Down Expand Up @@ -181,7 +181,8 @@ Short, runnable and tested plugins to copy from when writing a plugin for a real
to a simulated device: a stage with a voltmeter (``hardware/mock_serial_visa_instrument.py``) or a 3-axis controller
behind a library (``hardware/mock_dll_controller.py``). Each file starts with a header telling what to
copy, what to adapt and what to keep. The tests (``tests/test_cookbook_drivers.py``, ``tests/test_cookbook_plugins.py``,
``tests/test_cookbook_dll.py``) show how to test a plugin against a simulated device with the harness of ``pymodaq.utils.plugin_testing``.
``tests/test_cookbook_dll.py``, ``tests/test_cookbook_spectrometer.py``, ``tests/test_bs_camera.py``) show how to test a
plugin against a simulated device with the harness of ``pymodaq.utils.plugin_testing``.

Giving the same *controller ID* to an actuator and a detector of a recipe makes them share one driver: the first
initialized is the master and creates it, the other is the slave and receives it.
Expand Down Expand Up @@ -209,6 +210,16 @@ Loading a native library ``hardware/native_library.py``,
``hardware/system_library_demo.py`` environment variable, an error that lists what was tried. Real
calls to the C library of the system (``strlen``, ``frexp``,
``qsort`` with a callback)
Camera (2D detector), ROI and binning ``daq_2Dviewer_BSCamera``, What a camera driver gives its plugin: a ROI and a binning
done by the camera ``hardware/beam_steering.py`` applied by the camera (smaller image), an exposure time that
scales the signal, axes in um (pixel size) that follow the ROI
and the binning. ``RoiStuff`` is the other way: the ROI is drawn
in the viewer and the plugin crops after the grab
Spectrometer, calibrated axis that ``daq_1Dviewer_CookbookSpectrometer``, A wavelength axis in nm asked to the instrument at every grab
changes, actuator slave of a detector ``daq_move_CookbookSpectrometer``, because the grating moves it. The actuator (central wavelength)
``hardware/spectrometer.py`` and the detector share one instrument; a homing that waits for
its end and calls ``move_done``. It also has the interface of
the spectrometer extension (laser line, exposure time)
====================================== ============================================== ================================================================

Patterns already shown by the mock instruments of this package
Expand All @@ -222,10 +233,8 @@ Several axes on one controller ``MockNamedAxes`` (actuator)
Actuator and detector on the same ``MockTempController`` (actuator and 0D A temperature controller with a PID loop: the actuator is
controller detector) the master, the detector is its slave
2D camera ``BSCamera`` (2D detector), Pixel axes, live acquisition, ROI selection done on the
``RoiStuff`` viewer (``roi_select`` with ``RoiInfo``). Not shown: binning
and exposure time of a camera
1D spectrometer with a calibrated axis ``Mock_spectro`` (1D detector) A wavelength axis in nm that follows the laser line setting,
and an exposure time that scales the signal
``RoiStuff`` viewer (``roi_select`` with ``RoiInfo``). The hardware ROI and
binning are in the cookbook above
Simulated device, no hardware every plugin of this package A pure python controller behind a small wrapper class in
``hardware/``: the plugins run on it, no hardware needed
====================================== ============================================== ================================================================
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Cookbook recipe 3b: actuator that is a slave of a detector: the grating of a spectrometer (central wavelength).

COPY this file for an actuator that is one function of an instrument mostly used as a detector: the grating of a
spectrometer, the focus of a camera, the filter wheel of a detector...
ADAPT: ``params``, ``ini_stage`` and the calls of the driver in ``move_abs``, ``move_rel``, ``move_home``,
``stop_motion`` and ``get_actuator_value``.
KEEP:
- the master/slave handling in ``ini_stage`` and ``close``: alone the actuator owns the instrument, with the same
controller ID as the detector ``CookbookSpectrometer`` it shares the instrument of the master;
- ``user_condition_to_reach_target``: the grating takes time, so the end of the move is read from the instrument;
- ``move_home``: the homing is not polled by PyMoDAQ, so the plugin waits for its end and calls ``move_done``.

Moving the grating changes the wavelength axis of the detector, see ``daq_1Dviewer_CookbookSpectrometer.py``.
"""
from qtpy.QtCore import QThread

from pymodaq.control_modules.move_utility_classes import (DAQ_Move_base, comon_parameters_fun, DataActuatorType,
DataActuator, main)

from pymodaq_plugins_mockexamples.hardware.spectrometer import Spectrometer


class DAQ_Move_CookbookSpectrometer(DAQ_Move_base):
"""Central wavelength of the simulated spectrometer"""
_controller_units = 'nm'
is_multiaxes = False
_epsilons = 0.1
data_actuator_type = DataActuatorType.DataActuator
params = comon_parameters_fun(is_multiaxes, epsilon=_epsilons)

def ini_attributes(self):
self.controller: Spectrometer = None

def ini_stage(self, controller=None):
if self.is_master:
self.controller = Spectrometer()
else: # a master detector shares its instrument
self.controller = controller
return self.controller.infos, True

def close(self):
if self.is_master and self.controller is not None:
self.controller.close()

def user_condition_to_reach_target(self) -> bool:
return not self.controller.moving

def get_actuator_value(self) -> DataActuator:
position = DataActuator(data=self.controller.get_wavelength(), units=self.axis_unit)
return self.get_position_with_scaling(position)

def move_abs(self, value: DataActuator):
value = self.check_bound(value)
self.target_value = value
value = self.set_position_with_scaling(value)
self.controller.set_wavelength(value.value(self.axis_unit))

def move_rel(self, value: DataActuator):
value = self.check_bound(self.current_value + value) - self.current_value
self.target_value = value + self.current_value
value = self.set_position_with_scaling(self.target_value)
self.controller.set_wavelength(value.value(self.axis_unit))

def move_home(self):
self.controller.find_reference()
while self.controller.moving: # PyMoDAQ does not poll the position after a home: wait for the end here
QThread.msleep(20)
self.move_done()

def stop_motion(self):
self.controller.stop()

def commit_settings(self, param):
pass


if __name__ == '__main__':
main(__file__)
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"""Cookbook recipe 3a: 1D detector with an axis that is calibrated and changes: a spectrometer.

COPY this file for an instrument whose data axis is not fixed: a spectrometer (the wavelength of each pixel depends
on the central wavelength and on the grating), an oscilloscope (time base), an analyser...
ADAPT: ``params``, ``ini_detector`` and ``grab_data`` (what the driver gives for the data and for the axis).
KEEP:
- the axis is asked to the instrument at every grab and sent with the data. Another module (here the actuator
``CookbookSpectrometer``, a slave that turns the grating) can change it at any time;
- the units of the axis and of the data;
- the master/slave handling in ``ini_detector`` and ``close``: alone the detector owns the instrument, with the
same controller ID as the actuator it shares the instrument of the master.

The methods ``get_spectro_wl``, ``set_spectro_wl``, ``get_laser_wl``, ``set_laser_wl``, ``get_exposure_ms`` and
``set_exposure_ms`` are the interface that the spectrometer extension calls on a spectrometer plugin: copy them to be
driven by it (the laser ones only if the laser line can be set). Each one answers by a status command carrying the
value.

Try it with the actuator ``CookbookSpectrometer`` in the Dashboard (same controller ID), or alone: change the
central wavelength or the grating and the axis follows. See ``hardware/spectrometer.py`` for the simulated instrument.
"""
import numpy as np
from qtpy import QtWidgets
from qtpy.QtCore import QThread

from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters, main
from pymodaq_utils.utils import ThreadCommand
from pymodaq.utils.data import DataFromPlugins, Axis
from pymodaq_data import DataToExport

from pymodaq_plugins_mockexamples.hardware.spectrometer import Spectrometer


class DAQ_1DViewer_CookbookSpectrometer(DAQ_Viewer_base):
"""Spectrum of the simulated spectrometer, on a wavelength axis in nm"""
params = comon_parameters + [
{'title': 'Grating:', 'name': 'grating', 'type': 'list', 'value': next(iter(Spectrometer.gratings)),
'limits': list(Spectrometer.gratings), 'tip': 'The dispersion of the grating sets the range of the axis'},
{'title': 'Central wavelength (nm):', 'name': 'central_wavelength', 'type': 'float', 'value': 532.,
'min': 0, 'tip': 'Turns the grating. An actuator that shares the instrument can turn it too'},
{'title': 'Laser wavelength (nm):', 'name': 'laser_wl', 'type': 'list', 'value': 532.,
'limits': [405., 515., 532., 632.8], 'tip': 'The laser line is the line that the spectrometer measures'},
{'title': 'Exposure (ms):', 'name': 'exposure_ms', 'type': 'int', 'value': 100, 'min': 1,
'tip': 'The signal is proportional to the exposure time (100 ms gives the reference signal)'},
]

def ini_attributes(self):
self.controller: Spectrometer = None

def ini_detector(self, controller=None):
if self.is_master:
self.controller = Spectrometer()
self.controller.grating = self.settings['grating'] # apply the settings loaded from a preset
self.controller.set_wavelength(self.settings['central_wavelength'])
self.controller.peak_wavelength = self.settings['laser_wl']
self.controller.exposure_ms = self.settings['exposure_ms']
else: # a master actuator shares its instrument: do not move its grating
self.controller = controller
self.settings.child('grating').setValue(self.controller.grating)
self.settings.child('central_wavelength').setValue(self.controller.get_wavelength())
self.settings.child('laser_wl').setValue(self.controller.peak_wavelength)
self.settings.child('exposure_ms').setValue(self.controller.exposure_ms)
return self.controller.infos, True

def close(self):
if self.is_master and self.controller is not None:
self.controller.close()

def commit_settings(self, param):
if param.name() == 'grating':
self.controller.grating = param.value()
elif param.name() == 'central_wavelength':
self.controller.set_wavelength(param.value())
elif param.name() == 'laser_wl':
self.controller.peak_wavelength = param.value()
elif param.name() == 'exposure_ms':
self.controller.exposure_ms = param.value()

def get_spectro_wl(self):
"""Interface of the spectrometer extension: emit the central wavelength (the "grating" position)"""
self.emit_status(ThreadCommand('spectro_wl', [self.settings['central_wavelength']]))

def set_spectro_wl(self, spectro_wl: float):
"""Interface of the spectrometer extension: set the central wavelength and emit the new one"""
self.controller.set_wavelength(spectro_wl)
self.settings.child('central_wavelength').setValue(spectro_wl)
QtWidgets.QApplication.processEvents()
self.get_spectro_wl()

def get_laser_wl(self):
"""Interface of the spectrometer extension: emit the wavelength of the laser"""
self.emit_status(ThreadCommand('laser_wl', [self.settings['laser_wl']]))

def set_laser_wl(self, laser_wl: float):
"""Interface of the spectrometer extension: set the wavelength of the laser and emit the new one"""
self.controller.peak_wavelength = laser_wl
self.settings.child('laser_wl').setValue(laser_wl)
QtWidgets.QApplication.processEvents()
self.get_laser_wl()

def get_exposure_ms(self):
"""Interface of the spectrometer extension: emit the exposure time"""
self.emit_status(ThreadCommand('exposure_ms', [self.settings['exposure_ms']]))

def set_exposure_ms(self, exposure: int):
"""Interface of the spectrometer extension: set the exposure time and emit the new one"""
self.controller.exposure_ms = exposure
self.settings.child('exposure_ms').setValue(exposure)
QtWidgets.QApplication.processEvents()
self.get_exposure_ms()

def grab_data(self, Naverage=1, **kwargs):
axis = Axis(data=self.controller.get_wavelength_axis(), label='wavelength', units='nm', index=0)
spectrum = self.average_spectra(Naverage).mean(axis=0)
self.dte_signal.emit(DataToExport(
name='CookbookSpectrometer',
data=[DataFromPlugins(name='Spectrum', data=[spectrum], dim='Data1D', labels=['intensity'],
axes=[axis])]))

def average_spectra(self, Naverage: int) -> np.ndarray:
"""Naverage spectra, each one taking the exposure time"""
spectra = []
for _ in range(max(Naverage, 1)):
QThread.msleep(self.settings['exposure_ms'])
spectra.append(self.controller.grab_spectrum())
return np.array(spectra)

def stop(self):
self.controller.stop()


if __name__ == '__main__':
main(__file__)
Loading
Loading