From 81506529600c345cd46d36e2aff2c989a60296c3 Mon Sep 17 00:00:00 2001 From: adteurtrie Date: Wed, 4 Feb 2026 18:53:58 +0100 Subject: [PATCH 1/2] cleaned picam code --- README.rst | 89 ++--- pyproject.toml | 8 +- .../plugins_2D/daq_2Dviewer_Picam.py | 341 ++++++++++++++++++ .../plugins_2D/daq_2Dviewer_Pixis.py | 280 -------------- .../hardware/picam.py | 84 +++++ .../hardware/picamconfig.py | 35 ++ .../hardware/pixis.py | 27 -- .../resources/config_princeton.toml | 7 +- 8 files changed, 488 insertions(+), 383 deletions(-) create mode 100644 src/pymodaq_plugins_princeton_instruments/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Picam.py delete mode 100644 src/pymodaq_plugins_princeton_instruments/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Pixis.py create mode 100644 src/pymodaq_plugins_princeton_instruments/hardware/picam.py create mode 100644 src/pymodaq_plugins_princeton_instruments/hardware/picamconfig.py delete mode 100644 src/pymodaq_plugins_princeton_instruments/hardware/pixis.py diff --git a/README.rst b/README.rst index 1ff7b03..50f9527 100644 --- a/README.rst +++ b/README.rst @@ -1,88 +1,41 @@ -pymodaq_plugins_template -######################## +pymodaq_plugins_princeton_instruments +##################################### -.. the following must be adapted to your developed package, links to pypi, github description... - -.. image:: https://img.shields.io/pypi/v/pymodaq_plugins_template.svg - :target: https://pypi.org/project/pymodaq_plugins_template/ - :alt: Latest Version - -.. image:: https://readthedocs.org/projects/pymodaq/badge/?version=latest - :target: https://pymodaq.readthedocs.io/en/stable/?badge=latest - :alt: Documentation Status - -.. image:: https://github.com/PyMoDAQ/pymodaq_plugins_template/workflows/Upload%20Python%20Package/badge.svg - :target: https://github.com/PyMoDAQ/pymodaq_plugins_template +.. image:: https://github.com/PyMoDAQ/pymodaq_plugins_princeton_instruments/workflows/Upload%20Python%20Package/badge.svg + :target: https://github.com/PyMoDAQ/pymodaq_plugins_princeton_instruments :alt: Publication Status -.. image:: https://github.com/PyMoDAQ/pymodaq_plugins_template/actions/workflows/Test.yml/badge.svg - :target: https://github.com/PyMoDAQ/pymodaq_plugins_template/actions/workflows/Test.yml - - -Use this template to create a repository on your account and start the development of your own PyMoDAQ plugin! - +.. image:: https://github.com/PyMoDAQ/pymodaq_plugins_princeton_instruments/actions/workflows/Test.yml/badge.svg + :target: https://github.com/PyMoDAQ/pymodaq_plugins_princeton_instruments/actions/workflows/Test.yml Authors ======= -* First Author (myemail@xxx.org) -* Other author (myotheremail@xxx.org) - -.. if needed use this field - - Contributors - ============ - - * First Contributor - * Other Contributors - -.. if needed use this field - - Depending on the plugin type, delete/complete the fields below - +* Adrien Teurtrie (adrien.teurtrie@cemes.fr) Instruments =========== Below is the list of instruments included in this plugin -Actuators -+++++++++ - -* **yyy**: control of yyy actuators -* **xxx**: control of xxx actuators - -Viewer0D -++++++++ - -* **yyy**: control of yyy 0D detector -* **xxx**: control of xxx 0D detector - -Viewer1D -++++++++ - -* **yyy**: control of yyy 1D detector -* **xxx**: control of xxx 1D detector - - Viewer2D ++++++++ -* **yyy**: control of yyy 2D detector -* **xxx**: control of xxx 2D detector - - -PID Models -========== - - -Extensions -========== - +* **Fergie**: control of the Fergie spectrometer and its camera. It works through communication with the lightfield software. +* **Picam**: control of 2D detectors that can be controlled with the Picam.dll of Teledyne. Installation instructions ========================= -* PyMoDAQ’s version. -* Operating system’s version. -* What manufacturer’s drivers should be installed to make this plugin run? +* PyMoDAQ 5.1.x +* Windows 11 64 bits +* For the Fergie, installing the LightField software of Teledyne is required : https://www.teledynevisionsolutions.com/en-au/products/lightfield/?model=LF&vertical=tvs-princeton-instruments&segment=tvs (tested with an outdated licence.) +* For the Picam, installing the Picam software of Teledyne is required : https://www.teledynevisionsolutions.com/en-au/products/picam-sdk-amp-driver/?model=PICAM64&vertical=tvs-princeton-instruments&segment=tvs + +Work to be done +=============== + +* Create a DAQMove to control the wavelength of the Fergie. +* Test multiple Picam cameras working together. +* Improve the code and documentation of the Fergie DAQ2D_Viewer +* Implement ROIs for the Picam \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a452370..d7c1bce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,16 +14,14 @@ description = 'some word about your plugin' dependencies = [ "pymodaq>=5.0.0", "pythonnet", - #todo: list here all dependencies your package may have + "pylablib" ] authors = [ - {name = "Name Surname", email = "myname@test.fr"}, - #todo: list here all authors of your plugin + {name = "Adrien Teurtrie", email = "adrien.teurtrie@cemes.fr"}, ] maintainers = [ - {name = "Name Surname", email = "myname@test.fr"}, - #todo: list here all maintainers of your plugin + {name = "Adrien Teurtrie", email = "adrien.teurtrie@cemes.fr"}, ] # nottodo: leave everything below as is! diff --git a/src/pymodaq_plugins_princeton_instruments/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Picam.py b/src/pymodaq_plugins_princeton_instruments/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Picam.py new file mode 100644 index 0000000..e7b32f0 --- /dev/null +++ b/src/pymodaq_plugins_princeton_instruments/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Picam.py @@ -0,0 +1,341 @@ +import numpy as np + +from pymodaq_utils.utils import ThreadCommand +from pymodaq_data.data import DataToExport, Axis +from pymodaq_gui.parameter import Parameter +import math + +from qtpy import QtWidgets, QtCore + +from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters, main +from pymodaq.utils.data import DataFromPlugins +from pymodaq_plugins_princeton_instruments.hardware.picam import Picam + +from pymodaq_utils.logger import set_logger, get_module_name + +logger = set_logger(get_module_name(__file__)) + +def bin2d(array : np.ndarray, x : int,y : int) -> np.ndarray: + """ + Summing over 2d arrays for SNR improvement. + + Parameters + ---------- + array : np.ndarray + input bi-dimensional data. + x : int + Size reduction factor of the 1-st axis + y : int + Size reduction factor of the 0-th axis + + Returns + ------- + binned_arrray : np.ndarray + Summed array with reduced size + """ + x_bins = array.shape[1]//x + y_bins = array.shape[0]//y + binned_array = array.reshape(y_bins,y,x_bins,x).sum(axis=3).sum(axis=1) + # We want array that have shape e.g. (y,1) to reduce to (y,). + if binned_array.shape[1] == 1 : + binned_array = binned_array.sum(axis = 1) + if binned_array.shape[0] == 1 : + binned_array = binned_array.sum(axis = 0) + return binned_array + # https://stackoverflow.com/questions/61325586/fast-way-to-bin-a-2d-array-in-python + +def get_bin_list(size) : + """ + Produces the list of binning factors for powers of 2 shape (has a default value for other shapes) + TODO : Improve this function to produce binning factors for any integer. + + Parameters + ---------- + size : int + size of the data array to be binned. e.g. data.shape[0] + + Returns + ------- + bin_list : list[int] + The list of binning factors + + Notes + ----- + For example `get_bin_list(256)` will return [1,2,4,8,16,32,64,128,256] + """ + bin_list = [] + # Either we get the power of two of the input size (integer) or the input number is not a power of two. + power = math.log(size,2) + if power.is_integer() : + for i in range(round(power + 1)) : + bin_list.append(size//(2**i)) + else : + bin_list = [size,1] + # Better to reverse for display reasons + bin_list.reverse() + return bin_list + +class DAQ_2DViewer_Picam(DAQ_Viewer_base): + """ + Instrument plugin class for a 2D viewer of the cameras that can be interfaced with the picam software. + + This object inherits all functionalities to communicate with PyMoDAQ’s DAQ_Viewer module through inheritance via + DAQ_Viewer_base. It makes a bridge between the DAQ_Viewer module and the Python wrapper of a particular instrument. + + This plugin was tested with a PIXIS 256E and a (B)(MP)(eXcelon) but it should be compatible with any camera that can run with the picam dll. + See : https://www.teledynevisionsolutions.com/fr-fr/products/picam-sdk-amp-driver/?model=PICAM64&vertical=tvs-princeton-instruments&segment=tvs + + This plugin was tested with PyMoDAQ 5.1.x. on Windows 11 64 bits. + + Attributes: + ----------- + controller: Picam + This object inherits from the PicamCamera of pylablib with almost no interface. + x_axis : object + 2nd axis of the data, in pixel for generic use. + y_axis : object + 1st axis of the data, in pixel for generic use. + x_binning : int + Summing factor on the 2nd axis of the data. The higher the binning, the lower is the length of the 2nd axis. + y_binning : int + Summing factor on the 1st axis of the data. The higher the binning, the lower is the length of the 1st axis. + """ + live_mode_available = True + callback_signal = QtCore.Signal(int) + params = comon_parameters + [ + {'title' : 'x binning', 'name' : 'x_binning', 'type' : 'list', 'value' : 1, 'limits' : [1]}, + {'title' : 'y binning', 'name' : 'y_binning', 'type' : 'list', 'value' : 1, 'limits' : [1]}, + {'title' : 'exposure time (s)', 'name' : 'exposure', 'type' : 'float', 'value' : 0.1}, + {'title' : 'readout speed (MHz)', 'name' : 'adc_speed','type' : 'list', 'value' : 0.1} + ] + + def ini_attributes(self): + self.controller: Picam = None + + self.x_axis = None + self.y_axis = None + self.x_binning = 1 + self.y_binning = 1 + self._x_size = 1 + self._y_size = 1 + + def commit_settings(self, param: Parameter): + """Apply the consequences of a change of value in the detector settings + + Parameters + ---------- + param: Parameter + A given parameter (within detector_settings) whose value has been changed by the user + """ + if param.name() == 'x_binning' : + self.x_binning = param.value() + self.set_axes() + if param.name() == 'y_binning' : + self.y_binning = param.value() + self.set_axes() + if param.name() == 'exposure' : + self.controller.set_camera_setting('exposure',param.value()) + if param.name() == 'adc_speed' : + self.controller.set_camera_setting('ADC Speed', param.value()) + + def ini_detector(self, controller=None) -> tuple[str, bool]: + """ + Detector communication and Callbacks initialization + + Parameters + ---------- + controller: (object) + custom object of a PyMoDAQ plugin (Slave case). None if only one actuator/detector by controller + (Master case) + + Returns + ------- + info: str + initialized: bool + False if initialization failed otherwise True + """ + self.controller = self.ini_detector_init(slave_controller=controller, new_controller= Picam()) + if self.is_master: + initialized = True + self.callback = PicamCallback(self.controller) + self.callback_thread = QtCore.QThread() + self.callback.moveToThread(self.callback_thread) + self.callback_thread.callback = self.callback + self.callback_thread.start() + self.callback_signal.connect(self.callback.readout_sequence) + self.callback.data_sig.connect(self.emit_data) + # The main idea behind this code is as follows : + # The main thread that runs the DAQ2D_viewer can start and stop the acquistion at any time. + # The second thread that runs the PicamCallback is called by the main thread on a press of the play button. + # The second thread then loops : waits for acquisition, once ready (`wait_for_frame() == True`) sends the data to the the emit_data function. + # If the acquisition is stopped (`wait_for_frame() == False`) then the loop stops. + else: + self.controller = controller + initialized = True + + self._x_size = self.controller.get_detector_size()[0] + self._y_size = self.controller.get_detector_size()[1] + self.settings.child('x_binning').setLimits(get_bin_list(self.x_size)) + self.settings.child('y_binning').setLimits(get_bin_list(self.y_size)) + adc_attribute = self.controller.get_attribute('ADC Speed') + self.settings.child('adc_speed').setLimits(adc_attribute.values) + self.set_axes() + + info = "Whatever info you want to log" + return info, initialized + + @property + def y_size(self) -> int : + return self._y_size + + @property + def x_size(self) -> int : + return self._x_size + + def set_axes(self) : + """ + Set the axes for display depending on binning values. Can change the representation from 2D to 1D or 0D. + """ + data_x_axis = np.linspace(start= 0, stop = self.x_size//self.x_binning, num = self.x_size//self.x_binning) + data_y_axis = np.linspace(start= 0, stop = self.y_size//self.y_binning, num = self.y_size//self.y_binning) + # Case 1 : full binning both directions -> 0D data + if self.x_binning == self.x_size and self.y_binning == self.y_size : + dummy_data = np.array([0.0]) + # Case 2 : full binning in the x direction -> 1D data + elif self.x_binning == self.x_size and self.y_binning != self.y_size : + dummy_data = np.zeros((self.y_size//self.y_binning,)) + self.y_axis = Axis(data=data_y_axis, label='', units='', index=0) + # Case 3 : full binning in the y direction -> 1D data + elif self.y_binning == self.y_size and self.x_binning != self.x_size : + dummy_data = np.zeros((self.x_size//self.x_binning,)) + self.x_axis = Axis(data=data_x_axis, label='Energy loss', units='eV', index=0) + # Case 4 : No full binning -> 2D data + else : + dummy_data = np.zeros((self.y_size//self.y_binning,self.x_size//self.x_binning)) + self.y_axis = Axis(data=data_y_axis, label='', units='', index=0) + self.x_axis = Axis(data=data_x_axis, label='Energy loss', units='eV', index=1) + + dfp = self.prepare_dfp(dummy_data) + # Prepares the viewer + self.dte_signal_temp.emit(DataToExport('Picam', + data=dfp)) + + def prepare_dfp (self, data : np.ndarray) -> DataFromPlugins : + """ + Prepares DataFromPlugins for display. Chooses the right axes and data size based on the data shape. + + Parameters + ---------- + data : np.ndarray + input data. It can be any shape up to 2D. + """ + if self.x_binning == self.x_size and self.y_binning == self.y_size : + dfp = [DataFromPlugins(name = 'Picam', + data = data, + dim = 'Data0D')] + elif self.x_binning == self.x_size and self.y_binning != self.y_size : + dfp = [DataFromPlugins(name = 'Picam', + data = [np.atleast_1d(data)], + dim = 'Data1D',axes=[self.y_axis])] + elif self.y_binning == self.y_size and self.x_binning != self.x_size : + dfp = [DataFromPlugins(name = 'Picam', + data = [np.atleast_1d(data)], + dim = 'Data1D',axes=[self.x_axis])] + else : + dfp = [DataFromPlugins(name = 'Picam', + data = [np.atleast_1d(data)], + dim = 'Data2D',axes=[self.x_axis, self.y_axis])] + return dfp + + def close(self): + """Terminate the communication protocol""" + self.controller.close() + + + def grab_data(self, Naverage=1, **kwargs): + """Start a grab from the detector + + Parameters + ---------- + Naverage: int + Not implemented : Number of hardware averaging (if hardware averaging is possible, self.hardware_averaging should be set to + True in class preamble and you should code this implementation) + kwargs: dict + others optionals arguments + """ + try: + if kwargs.get('live',False) == True : + self.controller.start_acquisition() + self.callback_signal.emit(0) + else: + self.controller.start_acquisition() + self.callback_signal.emit(1) + + except Exception as e: + self.emit_status(ThreadCommand('Update_Status', [str(e), "log"])) + + def emit_data(self, data : np.ndarray) : + """ + Updates the current displayed data. + """ + binned_data = bin2d(data,self.x_binning,self.y_binning) + dfp = self.prepare_dfp(binned_data) + self.dte_signal.emit(DataToExport('Picam', + data=dfp)) + + def stop(self): + """Stop the current grab hardware wise if necessary""" + self.controller.clear_acquisition() + logger.info("Picam acquisition stopped.") + +class PicamCallback(QtCore.QObject): + """ + Callback object for the Picam camera. + + Attributes: + ----------- + controller: Picam + This object inherits from the PicamCamera of pylablib with almost no interface. + data_sig : object + Qt signal that sends the data and start the display on the main frame + """ + data_sig = QtCore.Signal(np.ndarray) + + def __init__(self,controller : Picam,*args,**kwargs): + super().__init__(*args,**kwargs) + self.controller = controller + + def readout_sequence(self, num_frames : int) : + """ + Blocking function. Waits for data to be ready, sends a signal with collected data when they are ready. + + Parameters + ---------- + num_frames : int + number of frames to be acquired + """ + if num_frames == 0 : + while True : + try : + if self.controller.wait_for_frame() : + image = self.controller.read_newest_image() + self.data_sig.emit(image) + else : + logger.info("Secondary thread readout sequence stopped.") + break + except TypeError : + break + else : + for _ in range(num_frames) : + try : + if self.controller.wait_for_frame() : + image = self.controller.read_newest_image() + self.data_sig.emit(image) + else : + logger.info("Secondary thread readout sequence stopped.") + break + except TypeError : + break + +if __name__ == '__main__': + main(__file__) diff --git a/src/pymodaq_plugins_princeton_instruments/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Pixis.py b/src/pymodaq_plugins_princeton_instruments/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Pixis.py deleted file mode 100644 index 0621957..0000000 --- a/src/pymodaq_plugins_princeton_instruments/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Pixis.py +++ /dev/null @@ -1,280 +0,0 @@ -import numpy as np - -from pymodaq_utils.utils import ThreadCommand -from pymodaq_data.data import DataToExport, Axis -from pymodaq_gui.parameter import Parameter -from pymodaq_gui.parameter.utils import iter_children - -from pymodaq.control_modules.viewer_utility_classes import DAQ_Viewer_base, comon_parameters, main -from pymodaq.utils.data import DataFromPlugins -from pymodaq_plugins_princeton_instruments.hardware.pixis import Pixis -from pylablib.devices import PrincetonInstruments as PI - -from pymodaq_utils.logger import set_logger, get_module_name - -from qtpy import QtWidgets, QtCore - -logger = set_logger(get_module_name(__file__)) - -def bin2d(array, x,y) : - x_bins = array.shape[1]//x - y_bins = array.shape[0]//y - binned_array = array.reshape(y_bins,y,x_bins,x).sum(axis=3).sum(axis=1) - if binned_array.shape[1] == 1 : - binned_array = binned_array.sum(axis = 1) - if binned_array.shape[0] == 1 : - binned_array = binned_array.sum(axis = 0) - return binned_array - # https://stackoverflow.com/questions/61325586/fast-way-to-bin-a-2d-array-in-python - -# TODO: -# (1) change the name of the following class to DAQ_2DViewer_TheNameOfYourChoice -# (2) change the name of this file to daq_2Dviewer_TheNameOfYourChoice ("TheNameOfYourChoice" should be the SAME -# for the class name and the file name.) -# (3) this file should then be put into the right folder, namely IN THE FOLDER OF THE PLUGIN YOU ARE DEVELOPING: -# pymodaq_plugins_my_plugin/daq_viewer_plugins/plugins_2D -class DAQ_2DViewer_Pixis(DAQ_Viewer_base): - """ Instrument plugin class for a 2D viewer. - - This object inherits all functionalities to communicate with PyMoDAQ’s DAQ_Viewer module through inheritance via - DAQ_Viewer_base. It makes a bridge between the DAQ_Viewer module and the Python wrapper of a particular instrument. - - TODO Complete the docstring of your plugin with: - * The set of instruments that should be compatible with this instrument plugin. - * With which instrument it has actually been tested. - * The version of PyMoDAQ during the test. - * The version of the operating system. - * Installation instructions: what manufacturer’s drivers should be installed to make it run? - - Attributes: - ----------- - controller: object - The particular object that allow the communication with the hardware, in general a python wrapper around the - hardware library. - - # TODO add your particular attributes here if any - - """ - live_mode_available = True - callback_signal = QtCore.Signal(int) - params = comon_parameters + [ - {'title' : 'x binning', 'name' : 'x_binning', 'type' : 'list', 'value' : 1, 'limits' : [1,2,4,8,16,32,64,128,256,512,1024]}, - {'title' : 'y binning', 'name' : 'y_binning', 'type' : 'list', 'value' : 1, 'limits' : [1,2,4,8,16,32,64,128,256]}, - {'title' : 'dispersion (eV/ch)', 'name' : 'dispersion', 'type' : 'float', 'value' : 1.0}, - {'title' : 'energy offet (eV)', 'name' : 'offset', 'type' : 'float', 'value' : 0.0}, - {'title' : 'exposure time', 'name' : 'exposure', 'type' : 'float', 'value' : 0.1} - ] - - def ini_attributes(self): - # TODO declare the type of the wrapper (and assign it to self.controller) you're going to use for easy - # autocompletion - self.controller: Pixis = None - - # TODO declare here attributes you want/need to init with a default value - - self.x_axis = None - self.y_axis = None - self.x_binning = 1 - self.y_binning = 1 - self.dispersion = 1.0 - self.offset = 0.0 - # self.exposure = 0.1 - - def commit_settings(self, param: Parameter): - """Apply the consequences of a change of value in the detector settings - - Parameters - ---------- - param: Parameter - A given parameter (within detector_settings) whose value has been changed by the user - """ - # TODO for your custom plugin - if param.name() == 'x_binning' : - self.x_binning = param.value() - self.set_axes() - if param.name() == 'y_binning' : - self.y_binning = param.value() - self.set_axes() - if param.name() == 'dispersion' : - self.dispersion = param.value() - self.set_axes() - if param.name() == 'offset' : - self.offset = param.value() - self.set_axes() - if param.name() == 'exposure' : - # self.exposure = param.value() - # self.callback.set_exposure(param.value) - self.set_camera_setting('exposure',param.value()) - - def ini_detector(self, controller=None): - """Detector communication initialization - - Parameters - ---------- - controller: (object) - custom object of a PyMoDAQ plugin (Slave case). None if only one actuator/detector by controller - (Master case) - - Returns - ------- - info: str - initialized: bool - False if initialization failed otherwise True - """ - self.controller = self.ini_detector_init(slave_controller=controller, new_controller= Pixis()) - if self.is_master: - initialized = True - # CT02. An object (called callback), is instanciated. - self.callback = PixisCallback(self.controller) - # CT03. A thread object is created (callback_thread) - self.callback_thread = QtCore.QThread() - # CT04. The thread object is made ready to be executed parallel to the main thread - self.callback.moveToThread(self.callback_thread) - # CT05. The function to be called by the thread is the callback object - self.callback_thread.callback = self.callback - # CT06. We make the thread ready to execute - self.callback_thread.start() - # CT07. We connect the signal to the execution of data read-out from the detector - self.callback_signal.connect(self.callback.readout_sequence) - # CT08. We connect the signal of the callback to the execution of PyMoDAQ GUI to display data. - self.callback.data_sig.connect(self.emit_data) - else: - self.controller = controller - initialized = True - - self.set_axes() - - info = "Whatever info you want to log" - return info, initialized - - def get_camera_setting(self,setting_name : str) : - all_settings = self.controller.get_settings() - return all_settings[setting_name] - - def set_camera_setting(self,setting_name : str, value) : - self.controller.dv[setting_name] = value - logger.info(f"The {setting_name} is set to : {value}.") - - def set_axes(self) : - data_x_axis = np.linspace(start= self.offset, stop = self.dispersion*(1024//self.x_binning)+self.offset, num = 1024//self.x_binning) - data_y_axis = np.linspace(start= 0, stop = 256//self.y_binning, num = 256//self.y_binning) - if self.x_binning == 1024 and self.y_binning == 256 : - dummy_data = np.array(0.0) - elif self.x_binning == 1024 and not(self.y_binning == 256) : - dummy_data = np.zeros((256//self.y_binning,)) - self.y_axis = Axis(data=data_y_axis, label='', units='', index=0) - elif self.y_binning == 256 and not(self.x_binning == 1024) : - dummy_data = np.zeros((1024//self.x_binning,)) - self.x_axis = Axis(data=data_x_axis, label='Energy loss', units='eV', index=0) - else : - dummy_data = np.zeros((256//self.y_binning,1024//self.x_binning)) - self.y_axis = Axis(data=data_y_axis, label='', units='', index=0) - self.x_axis = Axis(data=data_x_axis, label='Energy loss', units='eV', index=1) - # # get the y_axis (you may want to to this also in the commit settings if y_axis may have changed - # if possible - - dtp = self.prepare_dwa(dummy_data) - - self.dte_signal_temp.emit(DataToExport('Pixis', - data=dtp)) - - def prepare_dwa (self, data) : - if self.x_binning == 1024 and self.y_binning == 256 : - dtp = [DataFromPlugins(name = 'Pixis', - data = data, - dim = 'Data0D')] - elif self.x_binning == 1024 and not(self.y_binning == 256) : - dtp = [DataFromPlugins(name = 'Pixis', - data = [np.atleast_1d(data)], - dim = 'Data1D',axes=[self.y_axis])] - elif self.y_binning == 256 and not(self.x_binning == 1024) : - dtp = [DataFromPlugins(name = 'Pixis', - data = [np.atleast_1d(data)], - dim = 'Data1D',axes=[self.x_axis])] - else : - dtp = [DataFromPlugins(name = 'Pixis', - data = [np.atleast_1d(data)], - dim = 'Data2D',axes=[self.x_axis, self.y_axis])] - return dtp - - def close(self): - """Terminate the communication protocol""" - pass - # self.controller.close() - - - def grab_data(self, Naverage=1, **kwargs): - """Start a grab from the detector - - Parameters - ---------- - Naverage: int - Number of hardware averaging (if hardware averaging is possible, self.hardware_averaging should be set to - True in class preamble and you should code this implementation) - kwargs: dict - others optionals arguments - """ - ## TODO for your custom plugin: you should choose EITHER the synchrone or the asynchrone version following - - ##synchrone version (blocking function) - try: - if kwargs.get('live',False) == True : - self.controller.start_acquisition() - self.callback_signal.emit(0) - else: - self.controller.start_acquisition() - self.callback_signal.emit(1) - - except Exception as e: - self.emit_status(ThreadCommand('Update_Status', [str(e), "log"])) - - def emit_data(self, data : np.ndarray) : - binned_data = bin2d(data,self.x_binning,self.y_binning) - dtp = self.prepare_dwa(binned_data) - self.dte_signal.emit(DataToExport('Pixis', - data=dtp)) - - def stop(self): - """Stop the current grab hardware wise if necessary""" - ## TODO for your custom plugin - # raise NotImplementedError # when writing your own plugin remove this line - # self.controller.your_method_to_stop_acquisition() # when writing your own plugin replace this line - - self.controller.clear_acquisition() - # logger.info("Pixis acquisition stopped.") - # ############################# - # return '' - -class PixisCallback(QtCore.QObject): - data_sig = QtCore.Signal(np.ndarray) - - def __init__(self,controller,*args,**kwargs): - super().__init__(*args,**kwargs) - self.controller = controller - - def readout_sequence(self, num_frames) : - if num_frames == 0 : - while True : - try : - if self.controller.wait_for_frame() : - image = self.controller.read_newest_image() - self.data_sig.emit(image) - else : - logger.info("Secondary thread readout sequence stopped.") - break - except TypeError : - break - else : - for frame in range(num_frames) : - try : - if self.controller.wait_for_frame() : - image = self.controller.read_newest_image() - self.data_sig.emit(image) - else : - logger.info("Secondary thread readout sequence stopped.") - break - except TypeError : - break - -if __name__ == '__main__': - main(__file__) diff --git a/src/pymodaq_plugins_princeton_instruments/hardware/picam.py b/src/pymodaq_plugins_princeton_instruments/hardware/picam.py new file mode 100644 index 0000000..10ec05b --- /dev/null +++ b/src/pymodaq_plugins_princeton_instruments/hardware/picam.py @@ -0,0 +1,84 @@ +from pylablib.devices import PrincetonInstruments as PI +from pymodaq_plugins_princeton_instruments.hardware.picamconfig import PicamConfig +from pylablib.devices.PrincetonInstruments.picam_lib import PicamError + +from pymodaq_utils.logger import set_logger, get_module_name + +logger = set_logger(get_module_name(__file__)) + +class Picam(PI.PicamCamera): + """ + Hardware class. Wrapper of the PicamCamera object of pylablib. + + Attributes + ---------- + config : PicamConfig + PicamConfig object that contains user preferences. + SN : str + preferred serial number + """ + def __init__(self,*args,**kwargs): + self.config = PicamConfig().config + self.SN = self.config['PICAM']['config']['serial_number'] + super().__init__(self.SN,*args,**kwargs) + + def get_camera_setting(self,setting_name : str) : + """ + Method to get either setting or attribute from the camera. + + Parameter + ------- + setting_name : str + name of the attribute or setting to get + + Returns + ------- + value + value of the fetched attribute or setting + """ + try : + return self.dv[setting_name] + except KeyError : + try : + return self.cav[setting_name] + except PicamError as e : + raise KeyError('The parameter %s is not part of the available camera parameters.',setting_name) from e + + def set_camera_setting(self,setting_name : str, value) : + """ + Method to set either setting or attribute from the camera. + + Parameter + ------- + setting_name : str + name of the attribute or setting to set + value + value to set + """ + try : + self.dv[setting_name] = value + logger.info("The %s is set to : %s.",setting_name,value) + except KeyError : + try : + if self.get_attribute(setting_name).writable : + self.cav[setting_name] = value + logger.info("The %s is set to : %s.",setting_name,value) + else : + raise PicamError + except PicamError : + logger.info('The parameter value couple : %s, %s is not valid.',setting_name, value) + + +if __name__ == '__main__' : + with PI.PicamCamera() as cam : + images = [] + cam.start_acquisition() + for i in range(20) : + cam.wait_for_frame() + img = cam.read_newest_image() + images.append(img) + print('new frame read') + + print("last image") + print(images[-1]) + print(type(images[0])) \ No newline at end of file diff --git a/src/pymodaq_plugins_princeton_instruments/hardware/picamconfig.py b/src/pymodaq_plugins_princeton_instruments/hardware/picamconfig.py new file mode 100644 index 0000000..e5f24e5 --- /dev/null +++ b/src/pymodaq_plugins_princeton_instruments/hardware/picamconfig.py @@ -0,0 +1,35 @@ +from pymodaq_plugins_princeton_instruments.utils import Config +from pylablib.devices import PrincetonInstruments as PI + +class PicamConfig : + """ + Config object for the Picam hardware. + + Attributes + ---------- + config : Config + PyMoDAQ config object that contains user preferences. + """ + + def __init__(self): + self.config = Config() + SN_list = self.initialise_SN_list() + self.config['PICAM']['config']['serial_numbers'] = SN_list + if self.config['PICAM']['config']['serial_number'] == '' : + self.config['PICAM']['config']['serial_number'] = SN_list[0] + self.config.save() + + def initialise_SN_list(self) -> list[str] : + """ + Probes for Picam devices and gathers their serial numbers. + + Returns + ------- + SN_list : list + list of serial numbers of available Picam devices. + """ + camera_list = PI.list_cameras() + SN_list = [tup.serial_number for tup in camera_list] + if SN_list == [] : + raise ImportError("No camera was detected. Please connect a camera.") + return SN_list \ No newline at end of file diff --git a/src/pymodaq_plugins_princeton_instruments/hardware/pixis.py b/src/pymodaq_plugins_princeton_instruments/hardware/pixis.py deleted file mode 100644 index e5b3ecf..0000000 --- a/src/pymodaq_plugins_princeton_instruments/hardware/pixis.py +++ /dev/null @@ -1,27 +0,0 @@ -from pylablib.devices import PrincetonInstruments as PI -from pymodaq_plugins_princeton_instruments.utils import Config - -class PixisConfig : - - def __init__(self): - self.config = Config() - -class Pixis(PI.PicamCamera) : - def __init__(self,*args,**kwargs): - self.config = PixisConfig().config - self.SN = self.config['PIXIS']['config']['serial_number'] - super().__init__(self.SN) - -if __name__ == '__main__' : - with PI.PicamCamera() as cam : - images = [] - cam.start_acquisition() - for i in range(20) : - cam.wait_for_frame() - img = cam.read_newest_image() - images.append(img) - print('new frame read') - - print("last image") - print(images[-1]) - print(type(images[0])) \ No newline at end of file diff --git a/src/pymodaq_plugins_princeton_instruments/resources/config_princeton.toml b/src/pymodaq_plugins_princeton_instruments/resources/config_princeton.toml index 5f55433..48a176d 100644 --- a/src/pymodaq_plugins_princeton_instruments/resources/config_princeton.toml +++ b/src/pymodaq_plugins_princeton_instruments/resources/config_princeton.toml @@ -8,8 +8,9 @@ settings_file = 'C:\\Users\\HFLight\\Documents\\LightField\\fergie_settings.json name = 'FERGIE: 256B/FT eXcelon' serial_number = 'F071718001' -[PIXIS] +[PICAM] -[PIXIS.config] +[PICAM.config] -serial_number = '0205060002' \ No newline at end of file +serial_number = '' +serial_numbers = [] \ No newline at end of file From 00bdea4c719e89f10537bf2976e71c307ad77f3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Weber?= Date: Mon, 9 Feb 2026 09:27:08 +0100 Subject: [PATCH 2/2] cleaning --- .../plugins_2D/daq_2Dviewer_Picam.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/pymodaq_plugins_princeton_instruments/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Picam.py b/src/pymodaq_plugins_princeton_instruments/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Picam.py index e7b32f0..0373597 100644 --- a/src/pymodaq_plugins_princeton_instruments/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Picam.py +++ b/src/pymodaq_plugins_princeton_instruments/daq_viewer_plugins/plugins_2D/daq_2Dviewer_Picam.py @@ -264,7 +264,7 @@ def grab_data(self, Naverage=1, **kwargs): others optionals arguments """ try: - if kwargs.get('live',False) == True : + if kwargs.get('live', False) : self.controller.start_acquisition() self.callback_signal.emit(0) else: @@ -274,20 +274,20 @@ def grab_data(self, Naverage=1, **kwargs): except Exception as e: self.emit_status(ThreadCommand('Update_Status', [str(e), "log"])) - def emit_data(self, data : np.ndarray) : + def emit_data(self, data: np.ndarray) : """ Updates the current displayed data. """ - binned_data = bin2d(data,self.x_binning,self.y_binning) + binned_data = bin2d(data,self.x_binning, self.y_binning) dfp = self.prepare_dfp(binned_data) - self.dte_signal.emit(DataToExport('Picam', - data=dfp)) + self.dte_signal.emit(DataToExport('Picam', data=dfp)) def stop(self): """Stop the current grab hardware wise if necessary""" self.controller.clear_acquisition() logger.info("Picam acquisition stopped.") + class PicamCallback(QtCore.QObject): """ Callback object for the Picam camera. @@ -337,5 +337,6 @@ def readout_sequence(self, num_frames : int) : except TypeError : break + if __name__ == '__main__': main(__file__)