├── .gitignore ├── CAENpy ├── CAENDesktopHighVoltagePowerSupply.py ├── CAENDigitizer.py └── __init__.py ├── LICENSE ├── README.md ├── examples ├── HV_example_1.py ├── digitizer_example_1.py ├── digitizer_example_2.py └── digitizer_find_trigger.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | -------------------------------------------------------------------------------- /CAENpy/CAENDesktopHighVoltagePowerSupply.py: -------------------------------------------------------------------------------- 1 | import serial 2 | import socket 3 | import platform 4 | import time 5 | from threading import RLock 6 | 7 | def create_command_string(BD, CMD, PAR, CH=None, VAL=None): 8 | try: 9 | BD = int(BD) 10 | except: 11 | raise ValueError(f' must be an integer number. Received {BD} of type {type(BD)}.') 12 | if not 0 <= BD <= 31: 13 | raise ValueError(f' must be one of {{0,1,...,31}}, received {BD}.') 14 | command = f'$BD:{BD},CMD:{CMD},' 15 | if CH is not None: 16 | try: 17 | CH = int(CH) 18 | except: 19 | raise ValueError(f' must be an integer number, received {CH} of type {type(CH)}.') 20 | if not 0 <= CH <= 8: 21 | raise ValueError(f' must be one of {{0,1,...,8}}, received {CH}.') 22 | command += f'CH:{CH},' 23 | command += f'PAR:{PAR},' 24 | if VAL is not None: 25 | command += f'VAL:{VAL},' 26 | command = command[:-1] # Remove the last ',' 27 | command += '\r\n' 28 | return command 29 | 30 | def check_successful_response(response_string): 31 | if not isinstance(response_string, str): 32 | raise TypeError(f' must be an instance of , received {response_string} of type {type(response_string)}.') 33 | return 'OK' in response_string # According to the user manual, if there was no error the answer always contains an "OK". 34 | 35 | def _validate_type(variable, variable_name, variable_type): 36 | if not isinstance(variable, variable_type): 37 | raise TypeError(f'<{variable_name}> expected object of type {variable_type}, received object of type {type(variable)}.') 38 | 39 | def _validate_numeric_type(variable, variable_name, variable_numeric_type): 40 | try: 41 | variable = variable_numeric_type(variable) 42 | except: 43 | raise TypeError(f'<{variable_name}> expected object of type {variable_numeric_type}, received object of type {type(variable)}.') 44 | return variable 45 | 46 | class CAENDesktopHighVoltagePowerSupply: 47 | # This class was implemented according to the specifications in the 48 | # user manual here: https://www.caen.it/products/dt1470et/ 49 | # The implementation in this class should be thread safe. 50 | def __init__(self, port=None, ip=None, default_BD0=True, timeout=1): 51 | # The defines the number of seconds to wait until an error is raised if the instrument is not responding. Note that this instrument has the "not nice" behavior that some errors in the commands simply produce a silent answer, instead of reporting an error. For example, if you request the value of a parameter with a "BD" that is not in the daisy-chain, the instrument will give no answer at all, only silence. And you will have to guess what happened. 52 | if default_BD0 not in [True, False]: 53 | raise ValueError(f'The argument must be either True of False. Received {default_BD0}.') 54 | self.default_BD0 = default_BD0 55 | 56 | if ip is not None and port is not None: # This is an error, which connection protocol should we use? 57 | raise ValueError(f'You have specified both and . Please specify only one of them to use.') 58 | elif ip is not None and port is None: # Connect via Ethernet. 59 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 60 | self.socket.connect((ip, 1470)) # According to the user manual the port 1470 always has to be used. 61 | self.socket.settimeout(timeout) 62 | elif port is not None and ip is None: # Connect via USB serial port. 63 | self.serial_port = serial.Serial( 64 | # This configuration is specified in the user manual. 65 | port = port, 66 | baudrate = 9600, 67 | parity = serial.PARITY_NONE, 68 | stopbits = 1, 69 | bytesize = 8, 70 | xonxoff = True, 71 | timeout = timeout, 72 | ) 73 | else: # Both and are none... 74 | raise ValueError(f'Please specify a serial port or an IP addres in which the CAEN device can be found.') 75 | 76 | self._communication_lock = RLock() # To make a thread safe implementation. 77 | 78 | def send_command(self, CMD, PAR, CH=None, VAL=None, BD=None): 79 | # Send a command to the CAEN device. The parameters of this method are the ones specified in the user manual. 80 | if BD is None: 81 | if self.default_BD0 == True: 82 | BD = 0 83 | else: 84 | raise ValueError(f'Please specify a value for the parameter. Refer to the CAEN user manual.') 85 | bytes2send = create_command_string(BD=BD, CMD=CMD, PAR=PAR, CH=CH, VAL=VAL).encode('ASCII') 86 | if hasattr(self, 'serial_port'): # This means that we are talking through the serial port. 87 | with self._communication_lock: 88 | self.serial_port.write(bytes2send) 89 | elif hasattr(self, 'socket'): # This means that we are talking through an Ethernet connection. 90 | with self._communication_lock: 91 | self.socket.sendall(bytes2send) 92 | else: 93 | raise RuntimeError(f'There is no serial or Ethernet communication.') 94 | 95 | def read_response(self): 96 | # Reads the answer from the CAEN device. 97 | if hasattr(self, 'serial_port'): # This means that we are talking through the serial port. 98 | with self._communication_lock: 99 | received_bytes = self.serial_port.readline() 100 | elif hasattr(self, 'socket'): # This means that we are talking through an Ethernet connection. 101 | with self._communication_lock: 102 | received_bytes = self.socket.recv(1024) 103 | else: 104 | raise RuntimeError(f'There is no serial or Ethernet communication.') 105 | return received_bytes.decode('ASCII').replace('\n','').replace('\r','') # Remove the annoying '\r\n' in the end and convert into a string. 106 | 107 | def query(self, CMD, PAR, CH=None, VAL=None, BD=None): 108 | # Sends a command and reads the answer. 109 | with self._communication_lock: # Lock it to ensure that the answer I return corresponds to this command. Otherwise another thread could send a new command between my send and my read. 110 | self.send_command(BD=BD, CMD=CMD, PAR=PAR, CH=CH, VAL=VAL) 111 | response = self.read_response() 112 | return response 113 | 114 | def get_single_channel_parameter(self, parameter: str, channel: int, device: int=None): 115 | # Gets the current value of some parameter (see "MONITOR commands related to the Channels" in the CAEN user manual.) 116 | # parameter: This is the value in "PAR:whatever" that is specified in the user manual. 117 | # channel: Integer number specifying the numer of the channel. 118 | # device: If you have more than 1 device connected in the daisy chain, use this parameter to specify the device number (In the user manual this is the that goes in "BD:whatever"). 119 | response = self.query(CMD='MON', PAR=parameter, CH=channel, BD=device) 120 | if check_successful_response(response) == False: 121 | raise RuntimeError(f'Error trying to get the parameter {parameter}. The response from the instrument is: "{response}"') 122 | parameter_value = response.split('VAL:')[-1] 123 | if parameter_value.isdigit(): # This means it only contains numerical values, thus it is an int. 124 | return int(parameter_value) 125 | try: 126 | parameter_value = float(parameter_value) 127 | except: 128 | pass 129 | return parameter_value 130 | 131 | def set_single_channel_parameter(self, parameter: str, channel: int, value, device: int=None): 132 | # Sets the value of some parameter (see "SET commands related to the Channels" in the CAEN user manual.) 133 | # parameter: This is the value in "PAR:whatever" that is specified in the user manual. 134 | # channel: Integer number specifying the numer of the channel. 135 | # device: If you have more than 1 device connected in the daisy chain, use this parameter to specify the device number (In the user manual this is the that goes in "BD:whatever"). 136 | response = self.query(CMD='SET', PAR=parameter, CH=channel, BD=device, VAL=value) 137 | if check_successful_response(response) == False: 138 | raise RuntimeError(f'Error trying to set the parameter {parameter}. The response from the instrument is: "{response}"') 139 | 140 | def ramp_voltage(self, voltage: float, channel: int, device: int = None, ramp_speed_VperSec: float = 5, timeout: float = 10): 141 | # Blocks the execution until the ramp is completed. 142 | # timeout: It is the number of seconds to wait until the VMON (measured voltage) is stable. After this number of seconds, an error will be raised because the voltage cannot stabilize. 143 | ramp_speed_VperSec = _validate_numeric_type(ramp_speed_VperSec, 'ramp_speed_VperSec', float) 144 | voltage = _validate_numeric_type(voltage, 'voltage', float) 145 | timeout = _validate_numeric_type(timeout, 'timeout', float) 146 | channel = _validate_numeric_type(channel, 'channel', int) 147 | if device is not None: 148 | device = _validate_numeric_type(device, 'device', int) 149 | 150 | current_ramp_speed_settings = {} 151 | for par in ['RUP', 'RDW']: 152 | current_ramp_speed_settings[par] = self.get_single_channel_parameter(parameter=par, channel=channel, device=device) 153 | self.set_single_channel_parameter(parameter=par, channel=channel, device=device, value=ramp_speed_VperSec) 154 | current_voltage = self.get_single_channel_parameter(parameter='VSET', channel=channel, device=device) 155 | expected_ramping_seconds = ((current_voltage-voltage)**2)**.5/ramp_speed_VperSec 156 | try: 157 | self.set_single_channel_parameter( 158 | parameter = 'VSET', 159 | channel = channel, 160 | device = device, 161 | value = voltage, 162 | ) 163 | n_waited_seconds = 0 164 | while True: # Here I wait until it stabilizes. 165 | time.sleep(1) 166 | n_waited_seconds += 1 167 | if self.channel_status(channel = channel)['ramping up'] == 'no' and self.channel_status(channel = channel)['ramping down'] == 'no': 168 | break 169 | if n_waited_seconds > expected_ramping_seconds + timeout: # If this happens, better to raise an error that I cannot set the voltage. Otherwise this can be blocked forever. 170 | raise RuntimeError(f'Cannot reach a stable voltage after a timeout of {timeout} seconds.') 171 | except Exception as e: 172 | raise e 173 | finally: 174 | # Set back original configuration. 175 | for par in ['RUP', 'RDW']: 176 | self.set_single_channel_parameter( 177 | parameter = par, 178 | channel = channel, 179 | device = device, 180 | value = current_ramp_speed_settings[par], 181 | ) 182 | 183 | def channel_status(self, channel: int, device: int=None): 184 | """Returns the information from the status byte for the specified 185 | channel. Returns a dictionary containint the status byte and also 186 | some "human friendly" interpretations of the status byte.""" 187 | _validate_type(channel, 'channel', int) 188 | status_byte = int(self.query(CMD='MON', PAR='STAT', CH=channel, BD=device)[-5:]) 189 | status_byte_str = f"{status_byte:016b}" 190 | status_byte_str = status_byte_str[::-1] 191 | return { 192 | 'status byte': status_byte, 193 | 'output': 'on' if status_byte_str[0]=='1' else 'off', 194 | 'ramping up': 'yes' if status_byte_str[1]=='1' else 'no', 195 | 'ramping down': 'yes' if status_byte_str[2]=='1' else 'no', 196 | 'there was overcurrent': 'yes' if status_byte_str[3]=='1' else 'no', 197 | } 198 | 199 | @property 200 | def model_name(self): 201 | if not hasattr(self, '_model_name'): 202 | response = self.query(CMD='MON', PAR='BDNAME') 203 | if check_successful_response(response) == False: 204 | raise RuntimeError(f'The instument responded with error: {response}.') 205 | self._model_name = response.split('VAL:')[-1] 206 | return self._model_name 207 | 208 | @property 209 | def serial_number(self): 210 | if not hasattr(self, '_serial_number'): 211 | response = self.query(CMD='MON', PAR='BDSNUM') 212 | if check_successful_response(response) == False: 213 | raise RuntimeError(f'The instument responded with error: {response}.') 214 | self._serial_number = response.split('VAL:')[-1] 215 | return self._serial_number 216 | 217 | @property 218 | def idn(self) -> str: 219 | """Returns a string with information about the device identity.""" 220 | if not hasattr(self, '_idn'): 221 | name = self.model_name 222 | serial_number = self.serial_number 223 | self._idn = f'CAEN {name}, SN:{serial_number}' 224 | return self._idn 225 | 226 | @property 227 | def channels_count(self) -> int: 228 | """Return the number of channels available in the power supply.""" 229 | if not hasattr(self, '_channels_count'): 230 | response = self.query(CMD='MON', PAR='BDNCH') 231 | if check_successful_response(response) == False: 232 | raise RuntimeError(f'The instument responded with error: {response}.') 233 | self._channels_count = int(response.split('VAL:')[-1]) 234 | return self._channels_count 235 | 236 | @property 237 | def channels(self): 238 | """Returns a list with `OneCAENChannel` objects to individually handle 239 | each channel in a more convenient way.""" 240 | if not hasattr(self, '_channels'): 241 | self._channels = [OneCAENChannel(self, n) for n in range(self.channels_count)] 242 | return self._channels 243 | 244 | class OneCAENChannel: 245 | def __init__(self, caen, channel_number, device: int=None): 246 | """A wrapper for a single channel of the CAEN power supply, to ease 247 | its usage and avoid confisuions with channel numbers.""" 248 | _validate_type(caen, 'caen', CAENDesktopHighVoltagePowerSupply) 249 | _validate_type(channel_number, 'channel_number', int) 250 | if device is not None: 251 | _validate_type(device, 'device', int) 252 | self._caen = caen 253 | self._channel_number = channel_number 254 | self._device = device 255 | 256 | @property 257 | def idn(self): 258 | return f'{self._caen.idn}, CH{self._channel_number}' 259 | 260 | def set(self, PAR, VAL): 261 | VALID_PARs = {'VSET','ISET','MAXV','RUP','RDW','TRIP','PDWN','IMRANGE','ON','OFF','ZCADJ'} 262 | if PAR not in VALID_PARs: 263 | raise ValueError(f' must be one of {VALID_PARs}. Refer to the user manual of the CAEN power supply for more information.') 264 | self._caen.set_single_channel_parameter(parameter=PAR, value=VAL, channel=self.channel_number, device=self._device) 265 | 266 | def get(self, PAR): 267 | return self._caen.get_single_channel_parameter(parameter=PAR, channel=self.channel_number, device=self._device) 268 | 269 | @property 270 | def belongs_to(self): 271 | return f'CAEN model {self._caen.model_name}, serial number {self._caen.serial_number}' 272 | 273 | @property 274 | def channel_number(self): 275 | return self._channel_number 276 | 277 | @property 278 | def V_mon(self): 279 | channel_polarity = self.polarity 280 | if channel_polarity == '+': 281 | polarity = 1 282 | elif channel_polarity == '-': 283 | polarity = -1 284 | else: 285 | raise RuntimeError(f'Unexpected polarity response from the insturment. I was expecting one of {{"+","-"}} but received instead {channel_polarity}.') 286 | return polarity*self.get(PAR='VMON') 287 | 288 | @property 289 | def I_mon(self): 290 | return 1e-6*self.get(PAR='IMON') 291 | 292 | @property 293 | def V_set(self): 294 | return self.get('VSET') 295 | @V_set.setter 296 | def V_set(self, voltage): 297 | _validate_numeric_type(voltage,'voltage',float) 298 | self.set(PAR='VSET',VAL=voltage) 299 | 300 | @property 301 | def polarity(self): 302 | return self.get(PAR='POL') 303 | 304 | @property 305 | def status_byte(self): 306 | return self._caen.channel_status(channel=self.channel_number, device=self._device)['status byte'] 307 | @property 308 | def is_ramping(self): 309 | return self._caen.channel_status(channel=self.channel_number, device=self._device)['ramping up']=='yes' or self._caen.channel_status(channel=self.channel_number, device=self._device)['ramping down']=='yes' 310 | @property 311 | def there_was_overcurrent(self): 312 | return self._caen.channel_status(channel=self.channel_number, device=self._device)['there was overcurrent']=='yes' 313 | 314 | @property 315 | def output(self): 316 | return self._caen.channel_status(channel=self.channel_number, device=self._device)['output'] 317 | @output.setter 318 | def output(self, output_status: str): 319 | _validate_type(output_status, 'output_status', str) 320 | output_status = output_status.lower() 321 | if output_status not in {'on','off'}: 322 | raise ValueError(f' must be either "on" or "off", received {output_status}.') 323 | if output_status == 'on': 324 | self.set(PAR='ON',VAL=0) 325 | else: 326 | self.set(PAR='OFF',VAL=0) 327 | 328 | @property 329 | def current_compliance(self): 330 | return self.get('ISET')*1e-6 331 | @current_compliance.setter 332 | def current_compliance(self, amperes): 333 | _validate_numeric_type(amperes, 'amperes', float) 334 | self.set(PAR='ISET',VAL=1e6*amperes) 335 | 336 | def ramp_voltage(self, voltage, ramp_speed_VperSec: float = 5, timeout: float = 10): 337 | _validate_numeric_type(voltage, 'voltage', float) 338 | _validate_numeric_type(ramp_speed_VperSec, 'ramp_speed_VperSec', float) 339 | _validate_numeric_type(timeout, 'timeout', float) 340 | self._caen.ramp_voltage(voltage=voltage, channel=self.channel_number, device = self._device, ramp_speed_VperSec = ramp_speed_VperSec, timeout = timeout) 341 | 342 | def __str__(self): 343 | return f'Channel {self.channel_number} of {self.belongs_to}' 344 | 345 | def __repr__(self): 346 | return f'<{str(type(self))[1:-1]}, {self}>' 347 | 348 | -------------------------------------------------------------------------------- /CAENpy/CAENDigitizer.py: -------------------------------------------------------------------------------- 1 | # CAEN DT5742 control module, not all original libCAENDigitizer features supported. 2 | 3 | # ~ This file was taken and adapted from https://github.com/LucaMenzio/UFSD_DigiDAQ 4 | 5 | from ctypes import * 6 | import time 7 | import numpy 8 | 9 | libCAENDigitizer = CDLL('/usr/lib/libCAENDigitizer.so') # Change the path according to your installation. This is the default one in Ubuntu 22.04. The official library can be found here https://www.caen.it/products/caendigitizer-library/ 10 | 11 | CAEN_DGTZ_DRS4Frequency_MEGA_HERTZ = { 12 | 750: 3, 13 | 1000: 2, 14 | 2500: 1, 15 | 5000: 0 16 | } 17 | CAEN_DGTZ_TriggerMode = { 18 | 'disabled': 0, 19 | 'extout only': 2, 20 | 'acquisition only': 1, 21 | 'acquisition and extout': 3, 22 | } 23 | 24 | def check_error_code(code): 25 | """Check if the code returned by a function of the libCAENDigitizer 26 | library is an error or not. If it is not an error, nothing is done, 27 | if it is an error, a `RuntimeError` is raised with the error code. 28 | """ 29 | if code != 0: 30 | raise RuntimeError(f'libCAENDigitizer has returned error code {code}.') 31 | 32 | def struct2dict(struct): 33 | return dict((field, getattr(struct, field)) for field, _ in struct._fields_) 34 | 35 | class BoardInfo(Structure): 36 | _fields_ = [ 37 | ("ModelName", c_char*12), 38 | ("Model", c_uint32), 39 | ("Channels", c_uint32), 40 | ("FormFactor", c_uint32), 41 | ("FamilyCode", c_uint32), 42 | ("ROC_FirmwareRel", c_char*20), 43 | ("AMC_FirmwareRel", c_char*40), 44 | ("SerialNumber", c_uint32), 45 | ("MezzanineSerNum", (c_char*8)*4), 46 | ("PCB_Revision", c_uint32), 47 | ("ADC_NBits", c_uint32), 48 | ("SAMCorrectionDataLoaded", c_uint32), 49 | ("CommHandle", c_int), 50 | ("VMEHandle", c_int), 51 | ("License", c_char*999), 52 | ] 53 | 54 | class Group(Structure): 55 | _fields_ = [ 56 | ("ChSize", c_uint32*9), 57 | ("DataChannel", POINTER(c_float)*9), 58 | ("TriggerTimeLag", c_uint32), 59 | ("StartIndexCell", c_uint16)] 60 | 61 | class Event(Structure): 62 | _fields_ = [ 63 | ("GrPresent", c_uint8*4), 64 | ("DataGroup", Group*4)] 65 | 66 | class EventInfo(Structure): 67 | _fields_ = [ 68 | ("EventSize", c_uint32), 69 | ("BoardId", c_uint32), 70 | ("Pattern", c_uint32), 71 | ("ChannelMask", c_uint32), 72 | ("EventCounter", c_uint32), 73 | ("TriggerTimeTag", c_uint32)] 74 | 75 | def decode_event_waveforms_to_python_friendly_stuff(event:Event, ADC_peak_to_peak_dynamic_range_volts:float=None, time_axis_parameters:dict=None, ADC_dynamic_range_margin:int=77): 76 | """Decode the waveforms contained in an `Event` object into human friendly 77 | pythonic objects. 78 | 79 | Arguments 80 | --------- 81 | event: Event 82 | The event object to be decoded. 83 | ADC_peak_to_peak_dynamic_range_volts: `float`, default `None` 84 | The dynamic range of the ADC to be used to convert to volts. If 85 | `None` (default) then the values are not converted to volts. 86 | time_axis_parameters: `dict`, default `None` 87 | A dictionary of the form 88 | ``` 89 | dict( 90 | post_trigger_size: int, 91 | fast_trigger_mode: bool, 92 | sampling_frequency_Hz: float, 93 | ) 94 | ``` 95 | to reconstruct the time axis for the waveforms. Each of the parameters 96 | in this dictionary is related to the configuration of the digitizer. 97 | If `None` then no time axis is reconstructed, i.e. the waveforms 98 | are returned without a time array, only the samples. 99 | ADC_dynamic_range_margin: int, default `55` 100 | Samples that are in `0+ADC_dynamic_range_margin` or in `MAX_ADC-ADC_dynamic_range_margin` 101 | will be replaced by NaN values, to indicate ADC overflow. Setting 102 | this to 0 will disable this feature. 103 | 104 | Returns 105 | ------- 106 | event_waveforms: dict 107 | A dictionary with the waveforms as numpy arrays, example: 108 | ``` 109 | { 110 | 'CH0': { 111 | 'Amplitude (V)': array([0.01965812, 0.01965812, 0.01966247, ..., 0.01651428, 0.01672277, 0.01527676]), 112 | 'Time (s)': array([-1.626e-07, -1.624e-07, -1.622e-07, ..., 4.160e-08, 4.180e-08, 4.200e-08]) 113 | }, 114 | 'CH1': { 115 | 'Amplitude (V)': array([0.00500611, 0.00500611, 0.00501475, ..., 0.00524261, 0.00525031, 0.00428298]), 116 | 'Time (s)': array([-1.626e-07, -1.624e-07, -1.622e-07, ..., 4.160e-08, 4.180e-08, 4.200e-08]) 117 | }, 118 | 119 | CH2, CH3, etc..., 120 | 121 | 'trigger_group_0': { 122 | 'Amplitude (V)': array([0.03015873, 0.03015873, 0.03015009, ..., 0.025542 , 0.02599743, 0.0255237 ]), 123 | 'Time (s)': array([-1.626e-07, -1.624e-07, -1.622e-07, ..., 4.160e-08, 4.180e-08, 4.200e-08]) 124 | }, 125 | 'trigger_group_1': { 126 | 'Amplitude (V)': array([0.03235653, 0.03235767, 0.032594 , ..., 0.02868429, 0.02772543, 0.02747378]), 127 | 'Time (s)': array([-1.626e-07, -1.624e-07, -1.622e-07, ..., 4.160e-08, 4.180e-08, 4.200e-08]) 128 | } 129 | } 130 | ``` 131 | """ 132 | CHANNELS_NAMES = tuple([f'CH{n}' for n in [0,1,2,3,4,5,6,7]] + ['trigger_group_0'] + [f'CH{n-1}' for n in [9,10,11,12,13,14,15,16]] + ['trigger_group_1']) # Human friendly names. 133 | MAX_ADC = 2**12-1 # It is a 12 bit ADC. 134 | 135 | event_waveforms = {} 136 | for n_channel in range(18): 137 | n_group = int(n_channel / 9) 138 | if event.GrPresent[n_group] != 1: 139 | continue # If this group was disabled then skip it 140 | 141 | channel_name = CHANNELS_NAMES[n_channel] 142 | 143 | # Convert the data for this channel into something Python-friendly. 144 | n_channel_within_group = n_channel - (9 * n_group) 145 | block = event.DataGroup[n_group] 146 | waveform_length = block.ChSize[n_channel_within_group] 147 | 148 | if time_axis_parameters is not None and 'time_array' not in locals(): 149 | sampling_frequency = time_axis_parameters['sampling_frequency'] 150 | post_trigger_size = time_axis_parameters['post_trigger_size'] 151 | fast_trigger_mode = time_axis_parameters['fast_trigger_mode'] 152 | if not isinstance(sampling_frequency, (int, float)): 153 | raise TypeError(f'Sampling frequency must be a float, received object of type {type(sampling_frequency)}. ') 154 | if not isinstance(post_trigger_size, int): 155 | raise TypeError(f'post_trigger_size must be an integer number, received object of type {type(post_trigger_size)}. ') 156 | if not isinstance(fast_trigger_mode, bool): 157 | raise TypeError(f'fast_trigger_mode must be a boolean, received object of type {type(fast_trigger_mode)}. ') 158 | 159 | time_array = numpy.array(range(waveform_length))/sampling_frequency 160 | if fast_trigger_mode == True: 161 | trigger_latency = 42e-9 # This comes from the user manual, see § 9.8.3 of 'UM4270_DT5742_UserManual_rev11.pdf'. 162 | else: 163 | trigger_latency = 0 # Unknown value, cannot use NaN as it would destroy all the time array. 164 | time_array -= time_array.max()*(100-post_trigger_size)/100 - trigger_latency 165 | 166 | samples = numpy.array(block.DataChannel[n_channel_within_group][0:waveform_length]) 167 | samples[(samplesMAX_ADC-ADC_dynamic_range_margin)] = float('NaN') # These values are considered as ADC overflow, thus it is safer to replace them with NaN so they don't go unnoticed. 168 | 169 | wf = {} 170 | if ADC_peak_to_peak_dynamic_range_volts is not None: 171 | if not isinstance(ADC_peak_to_peak_dynamic_range_volts, (int,float)): 172 | raise TypeError(f'`ADC_peak_to_peak_dynamic_range_volts` must be a float or integer number, received object of type {type(ADC_peak_to_peak_dynamic_range_volts)}. ') 173 | wf['Amplitude (V)'] = (samples-MAX_ADC/2)*ADC_peak_to_peak_dynamic_range_volts/MAX_ADC 174 | else: 175 | wf['Amplitude (ADCu)'] = samples 176 | if time_axis_parameters is not None: 177 | wf['Time (s)'] = time_array 178 | 179 | event_waveforms[channel_name] = wf 180 | return event_waveforms 181 | 182 | class CAEN_DT5742_Digitizer: 183 | """A class designed to interface with CAEN DT5742 digitizers in an 184 | easy and Pythonic way. 185 | 186 | Usage example 187 | ------------- 188 | ``` 189 | digitizer = CAEN_DT5742_Digitizer(0) # Open the connection. 190 | 191 | # Now configure the digitizer: 192 | digitizer.set_sampling_frequency(5000) # Set to 5 GHz. 193 | digitizer.set_max_num_events_BLT(1) # One event per call to `digitizer.get_waveforms`. 194 | # More configuration here... 195 | 196 | # Now enter into acquisition mode using the `with` statement: 197 | with digitizer: 198 | waveforms = digitizer.get_waveforms() 199 | # The `with` statement takes care of initializing and closing the 200 | # acquisition, as well as all the ugly stuff required for this to 201 | # happen. 202 | # You can now acquire again with a new `with` block: 203 | with digitizer: 204 | new_waveforms = digitizer.get_waveforms() 205 | # and you can as well keep the acquisition running while you do 206 | # other things: 207 | with digitizer: 208 | waveforms = [] 209 | for voltage in [0,100,200]: 210 | voltage_source.set_voltage(voltage) 211 | wf = digitizer.get_waveforms() 212 | waveforms.append(wf) 213 | ``` 214 | # That's it, you don't need to take care of anything else. 215 | """ 216 | 217 | def __init__(self, LinkNum:int, reset_upon_connection:bool=True): 218 | """Creates an instance of CAEN_DT5742_Digitizer. Upon creation 219 | this method also establishes the connection with the digitizer 220 | (so you don't need to call anything like `digitizer.connect()` or 221 | whatever) and it also checks that the digitizer model is specifically 222 | DT5742, otherwise it rises a `RuntimeError`. This means that if 223 | the object was created, the digitizer should be ready to start 224 | operating it. 225 | 226 | Arguments 227 | --------- 228 | LinkNum: int 229 | According to the documentation of the official CAENDigitizer 230 | library: 'the link numbers are assigned by the PC when you 231 | connect the cable to the device; it is 0 for the first device, 232 | 1 for the second, and so on. There is not a fixed correspondence 233 | between the USB port and link number'. 234 | reset_upon_connection: bool, default True 235 | Specifies whether to reset the digitizer after proper connection. 236 | It is usually a good practice to do this, as then you start 237 | to work with the instrument in a known and well defined 238 | state to configure it. 239 | """ 240 | self._connected = False 241 | self._LinkNum = LinkNum 242 | self.__handle = c_int() # Handle object, keep track of our connection. 243 | 244 | # These are some objects required by the libCAENDigitizer. 245 | self.eventObject = POINTER(Event)()# Stores the event that's currently being processed. Is overwritten by the next event as soon as decodeEvent() is called. 246 | self.eventPointer = POINTER(c_char)() # Points to the event that's currently being processed. 247 | self.eventInfo = EventInfo() # Stores some stats about the event that's currently being processed. 248 | self.eventBuffer = POINTER(c_char)() # Stores the last block of events transferred from the digitizer. 249 | self.eventAllocatedSize = c_uint32() # Size in memory of the event object. 250 | self.eventBufferSize = c_uint32() # Size in memory of the events' block transfer. 251 | self.eventVoidPointer = cast(byref(self.eventObject), POINTER(c_void_p)) # Need to create a **void since technically speaking other kinds of Event() esist as well (the CAENDigitizer library supports a multitude of devices, with different Event() structures) and we need to pass this to "universal" methods. 252 | 253 | self._open() # Open the connection to the digitizer. 254 | 255 | model = self.get_info()['ModelName'].decode('utf8') 256 | if 'DT5742' not in model: 257 | raise RuntimeError(f'This class was designed to command a CAEN DT5742 digitizer, but instead now you are connected to a CAEN {model}. It may be possible that with a small adaption this code still works, but you have to take care of this...') 258 | 259 | if reset_upon_connection == True: 260 | self.reset() 261 | 262 | @property 263 | def idn(self): 264 | """Return a string with information about the digitizer (model, 265 | serial number, etc).""" 266 | if not hasattr(self, '_idn'): 267 | info = self.get_info() 268 | model = info['ModelName'].decode('utf8') 269 | serial_number = info['SerialNumber'] 270 | self._idn = f'CAEN {model} digitizer, serial number {serial_number}' 271 | return self._idn 272 | 273 | def start_acquisition(self, DRS4_correction:bool=True): 274 | """Puts the device into acquisition mode and runs all the required 275 | configurations of the `libCAENDigitizer` so the data can be read 276 | out from the digitizer. 277 | 278 | Arguments 279 | --------- 280 | DRS4_correction: bool, default True 281 | Specifies whether to apply the DRS4 correction. I don't see 282 | a reason why not to use it, but anyway... 283 | """ 284 | if self.get_acquisition_status()['acquiring now'] == True: 285 | raise RuntimeError(f'The digitizer is already acquiring, cannot start a new acquisition.') 286 | if DRS4_correction == True: 287 | self._LoadDRS4CorrectionData(MHz=self.get_sampling_frequency()) 288 | self._DRS4_correction(enable=DRS4_correction) 289 | self._start_acquisition() 290 | self.get_acquisition_status() # This makes it work better. Don't know why. 291 | 292 | def stop_acquisition(self): 293 | """Stops the acquisition and cleans the memory used by the `libCAENDigitizer` 294 | library to read out the instrument.""" 295 | self._stop_acquisition() 296 | 297 | def __enter__(self): 298 | self.start_acquisition() 299 | 300 | def __exit__(self, exc_type, exc_val, exc_tb): 301 | self.stop_acquisition() 302 | 303 | def __del__(self): 304 | self.close() 305 | 306 | def _open(self): 307 | """Open the connection to the digitizer.""" 308 | if self._connected == False: 309 | code = libCAENDigitizer.CAEN_DGTZ_OpenDigitizer( 310 | c_long(0), # LinkType (0 is USB). 311 | c_int(self._LinkNum), 312 | c_int(0), # ConetNode. 313 | c_uint32(0), # VMEBaseAddress. 314 | byref(self.__handle) 315 | ) 316 | check_error_code(code) 317 | self._connected = True 318 | 319 | def close(self): 320 | """Close the connection with the digitizer.""" 321 | if self._connected == True: 322 | code = libCAENDigitizer.CAEN_DGTZ_CloseDigitizer(self.__handle) # Most of the times this line produces a `Segmentation fault (core dumped)`... 323 | check_error_code(code) 324 | self._connected = False 325 | 326 | def _get_handle(self): 327 | """Get the connection handle in a safe way no matter the connection 328 | status. If the connection is not open, this method raises a 329 | `RuntimeError`.""" 330 | if self._connected == True: 331 | return self.__handle 332 | else: 333 | raise RuntimeError(f'The connection with the CAEN Digitizer is not open!') 334 | 335 | def reset(self): 336 | """Reset the digitizer.""" 337 | code = libCAENDigitizer.CAEN_DGTZ_Reset(self._get_handle()) 338 | check_error_code(code) 339 | 340 | def write_register(self, address, data): 341 | """Write data to a given register. It is advised by the manual 342 | of the CAENDigitizer library that one should avoid using this 343 | function, and use the specific functions instead.""" 344 | code = libCAENDigitizer.CAEN_DGTZ_WriteRegister( 345 | self._get_handle(), 346 | c_uint32(address), 347 | c_uint32(data) 348 | ) 349 | check_error_code(code) 350 | 351 | def read_register(self, address): 352 | """Read bytes from a specific register. Returns the data in the 353 | register as an `int` object.""" 354 | if not isinstance(address, int) or not 0 <=address<2**16: 355 | raise ValueError(f'`address` must be a 16 bit integer, received {repr(address)}. ') 356 | data = c_uint32() 357 | code = libCAENDigitizer.CAEN_DGTZ_ReadRegister( 358 | self._get_handle(), 359 | c_uint32(address), 360 | byref(data), 361 | ) 362 | check_error_code(code) 363 | return data.value 364 | 365 | def set_acquisition_mode(self, mode:str): 366 | """Set the acquisition mode. 367 | 368 | Arguments 369 | --------- 370 | mode: str 371 | Specifies the acquisition mode, see CAENDigitizer library 372 | manual for details. Options are `'sw_controlled', `'in_controlled'` 373 | and `'first_trg_controlled'`. 374 | 375 | """ 376 | MODES = {'sw_controlled': 0, 'in_controlled': 1, 'first_trg_controlled': 2} 377 | if mode not in MODES: 378 | raise ValueError(f'`mode` must be one of {set(MODES.keys())}, received {repr(mode)}. ') 379 | code = libCAENDigitizer.CAEN_DGTZ_SetAcquisitionMode( 380 | self._get_handle(), 381 | c_long(MODES[mode]), 382 | ) 383 | check_error_code(code) 384 | 385 | def get_info(self)->dict: 386 | """Get information related to the board such as serial number, etc.""" 387 | info = BoardInfo() 388 | code = libCAENDigitizer.CAEN_DGTZ_GetInfo( 389 | self._get_handle(), 390 | byref(info) 391 | ) 392 | check_error_code(code) 393 | return struct2dict(info) 394 | 395 | def _allocateEvent(self): 396 | """Allocate space in memory for the event object.""" 397 | code = libCAENDigitizer.CAEN_DGTZ_AllocateEvent( 398 | self._get_handle(), 399 | self.eventVoidPointer 400 | ) 401 | check_error_code(code) 402 | 403 | def _mallocBuffer(self): 404 | """Allocate space in memory for the events' block transfer.""" 405 | code = libCAENDigitizer.CAEN_DGTZ_MallocReadoutBuffer( 406 | self._get_handle(), 407 | byref(self.eventBuffer), 408 | byref(self.eventAllocatedSize) 409 | ) 410 | check_error_code(code) 411 | 412 | def _freeEvent(self): 413 | """Free memory that was allocated for the event object.""" 414 | ptr = cast(pointer(self.eventObject), POINTER(c_void_p)) 415 | code = libCAENDigitizer.CAEN_DGTZ_FreeEvent( 416 | self._get_handle(), 417 | ptr 418 | ) 419 | check_error_code(code) 420 | 421 | def _freeBuffer(self): 422 | """Free memory that was allocated for the events' block transfer.""" 423 | code = libCAENDigitizer.CAEN_DGTZ_FreeReadoutBuffer(byref(self.eventBuffer)) 424 | check_error_code(code) 425 | 426 | def set_max_num_events_BLT(self, numEvents): 427 | """Max number of events per block transfer. Minimum is 1, maximum 428 | is 1023. It's recommended to set it to the maximum allowed value 429 | and continuously poll the digitizer for new data. Binary transfer 430 | is fast while a full buffer means the digitizer will discard events. 431 | 432 | In essence, this is the number of events you will get each time 433 | you call the method `get_waveforms`. 434 | 435 | This is a wrapper of the method `CAEN_DGTZ_SetMaxNumEventsBLT` 436 | from the CAENDigitizer library. 437 | """ 438 | code = libCAENDigitizer.CAEN_DGTZ_SetMaxNumEventsBLT( 439 | self._get_handle(), 440 | c_uint32(numEvents) 441 | ) 442 | check_error_code(code) 443 | 444 | def get_acquisition_status(self) -> dict: 445 | """Reads and returns the 'Acquisition Status' register. 446 | 447 | Returns 448 | ------- 449 | status: dict 450 | A dictionary of the form: 451 | ``` 452 | { 453 | 'acquisition_status_register': int, # Actual value read from the register. 454 | 'acquiring now': bool, 455 | 'at least one event available for readout': bool, 456 | 'events memory is full': bool, 457 | 'clock source': str, # 'external' or 'external'. 458 | 'PLL unlock detected': bool, 459 | 'ready for acquisition': bool, 460 | 'S-IN/GPI pin status': bool, 461 | 'TRIG-IN status': bool, 462 | } 463 | ``` 464 | """ 465 | acquisition_status_register = self.read_register(0x8104) 466 | return { 467 | 'acquisition_status_register': acquisition_status_register, 468 | 'acquiring now': True if acquisition_status_register & 1<<2 else False, 469 | 'at least one event available for readout': True if acquisition_status_register & 1<<3 else False, 470 | 'events memory is full': True if acquisition_status_register & 1<<4 else False, 471 | 'clock source': 'external' if acquisition_status_register & 1<<5 else 'external', 472 | 'PLL unlock detected': False if acquisition_status_register & 1<<7 else True, 473 | 'ready for acquisition': True if acquisition_status_register & 1<<8 else False, 474 | 'S-IN/GPI pin status': True if acquisition_status_register & 1<<15 else False, 475 | 'TRIG-IN status': True if acquisition_status_register & 1<<16 else False, 476 | } 477 | 478 | def set_fast_trigger_mode(self, enabled:bool): 479 | """Enable or disable the TRn as the local trigger in the x742 series. 480 | 481 | Arguments 482 | --------- 483 | enabled: bool 484 | True or false to enable or disable. 485 | """ 486 | if not isinstance(enabled, bool): 487 | raise TypeError(f'`enabled` must be of type {repr(bool)}, received object of type {repr(type(enabled))} instead.') 488 | code = libCAENDigitizer.CAEN_DGTZ_SetFastTriggerMode( 489 | self._get_handle(), 490 | c_long(0 if enabled == False else 1) 491 | ) 492 | check_error_code(code) 493 | 494 | def get_fast_trigger_mode(self): 495 | """Get the status (enabled or disabled) of the TRn as the local 496 | trigger in the x742 series.""" 497 | status = c_long() 498 | code = libCAENDigitizer.CAEN_DGTZ_GetFastTriggerMode( 499 | self._get_handle(), 500 | byref(status) 501 | ) 502 | check_error_code(code) 503 | return int(status.value) == 1 504 | 505 | def set_fast_trigger_digitizing(self, enabled:bool): 506 | """Regarding the x742 series, enables/disables (set) the presence 507 | of the TRn signal in the data readout. 508 | 509 | Arguments 510 | --------- 511 | enabled: bool 512 | True or false to enable or disable. 513 | """ 514 | if not isinstance(enabled, bool): 515 | raise TypeError(f'`enabled` must be of type {repr(bool)}, received object of type {repr(type(enabled))} instead.') 516 | code = libCAENDigitizer.CAEN_DGTZ_SetFastTriggerDigitizing( 517 | self._get_handle(), 518 | c_long(0 if enabled == False else 1) 519 | ) 520 | check_error_code(code) 521 | 522 | def set_fast_trigger_DC_offset(self, DAC:int=None, V:float=None): 523 | """Set the DC offset for the trigger channel TRn. 524 | 525 | Note: Arguments `DAC` and `V` are to be used separately, either 526 | one or the other but not both. 527 | 528 | Arguments 529 | --------- 530 | DAC: int 531 | Value for the offset, in ADC units between 0 and 2**16-1 (65535). 532 | V: float 533 | Value for the offset in units of volt, between -1 and +1. 534 | """ 535 | if (DAC is None and V is None) or (DAC is not None and V is not None): 536 | raise ValueError(f'Both `DAC` and `V` are empty or were provided. You must provide one and only one of them.') 537 | if DAC is not None: 538 | if not isinstance(DAC, int) or not 0 <= DAC < 2**16: 539 | raise ValueError(f'`DAC` must be an integer number between 0 and 2**16-1.') 540 | if V is not None: 541 | if not isinstance(V, (int,float)) or not -1 <= V <= 1: 542 | raise ValueError('`V` must be a float between -1 and 1.') 543 | DAC = int((V+1)/2*(2**16-1)) 544 | code = libCAENDigitizer.CAEN_DGTZ_SetGroupFastTriggerDCOffset( 545 | self._get_handle(), 546 | c_uint32(0), # This is for the 'group', not sure what it is but it is always 0 for us. 547 | c_uint32(DAC) 548 | ) 549 | check_error_code(code) 550 | 551 | def set_fast_trigger_threshold(self, threshold:int): 552 | """Set the fast trigger threshold. 553 | 554 | Arguments 555 | --------- 556 | threshold: int 557 | Threshold value in ADC units, between 0 and 2**16-1 (65535). 558 | """ 559 | if not isinstance(threshold, int) or not 0 <= threshold < 2**16: 560 | raise ValueError(f'`threshold` must be an integer number between 0 and 2**16-1.') 561 | code = libCAENDigitizer.CAEN_DGTZ_SetGroupFastTriggerThreshold( 562 | self._get_handle(), 563 | c_uint32(0), # This is for the 'group', not sure what it is but it is always 0 for us. 564 | c_uint32(threshold) 565 | ) 566 | check_error_code(code) 567 | 568 | def set_post_trigger_size(self, percentage:int): 569 | """Set the 'post trigger size', i.e. the position of the trigger 570 | within the acquisition window. 571 | 572 | Arguments 573 | --------- 574 | percentage: int 575 | Percentage of the record length. 0 % means that the trigger 576 | is at the end of the window, while 100 % means that it is at 577 | the beginning. 578 | """ 579 | if not isinstance(percentage, int) or not 0 <= percentage <= 100: 580 | raise ValueError(f'`percentage` must be an integer number between 0 and 100.') 581 | code = libCAENDigitizer.CAEN_DGTZ_SetPostTriggerSize( 582 | self._get_handle(), 583 | c_uint32(percentage), 584 | ) 585 | check_error_code(code) 586 | 587 | def get_post_trigger_size(self)->int: 588 | """Get the 'post trigger size', i.e. the position of the trigger 589 | within the acquisition window. 590 | 591 | Returns 592 | --------- 593 | percentage: int 594 | Percentage of the record length. 0 % means that the trigger 595 | is at the end of the window, while 100 % means that it is at 596 | the beginning. 597 | """ 598 | percentage = c_uint32() 599 | code = libCAENDigitizer.CAEN_DGTZ_GetPostTriggerSize( 600 | self._get_handle(), 601 | byref(percentage), 602 | ) 603 | check_error_code(code) 604 | return int(percentage.value) 605 | 606 | def set_record_length(self, length:int): 607 | """Set how many samples should be taken for each event. 608 | Arguments 609 | --------- 610 | length: int 611 | The size of the record (in samples). 612 | """ 613 | code = libCAENDigitizer.CAEN_DGTZ_SetRecordLength( 614 | self._get_handle(), 615 | c_uint32(length), 616 | ) 617 | check_error_code(code) 618 | 619 | def set_ext_trigger_input_mode(self, mode:str): 620 | """Enable or disable the external trigger (TRIG IN). 621 | 622 | Arguments 623 | --------- 624 | mode: str 625 | One of `'disabled'`, `'extout only'`, `'acquisition only'`, 626 | `'acquisition and extout'`. 627 | """ 628 | if mode not in CAEN_DGTZ_TriggerMode: 629 | raise ValueError(f'`mode` must be one of {set(CAEN_DGTZ_TriggerMode.keys())}, received {repr(mode)}. ') 630 | code = libCAENDigitizer.CAEN_DGTZ_SetExtTriggerInputMode( 631 | self._get_handle(), 632 | c_long(CAEN_DGTZ_TriggerMode[mode]) 633 | ) 634 | check_error_code(code) 635 | 636 | def set_trigger_polarity(self, channel:int, edge:str): 637 | """Set the trigger polarity of a specified channel. 638 | 639 | Arguments 640 | --------- 641 | channel: int 642 | Number of channel. 643 | edge: str 644 | Either `'rising'` or `'falling'`. 645 | """ 646 | EDGE_VALUES = {'rising','falling'} 647 | if edge not in EDGE_VALUES: 648 | raise ValueError(f'`edge` must be one of {EDGE_VALUES}, received {repr(edge)}. ') 649 | code = libCAENDigitizer.CAEN_DGTZ_SetTriggerPolarity( 650 | self._get_handle(), 651 | c_uint32(channel), 652 | c_long(0 if edge == 'rising' else 1), 653 | ) 654 | check_error_code(code) 655 | 656 | def set_sampling_frequency(self, MHz:int): 657 | """Set the sampling frequency of the digitizer. 658 | 659 | Arguments 660 | --------- 661 | MHz: int 662 | The sampling frequency in Mega Hertz. Note that only some 663 | discrete values are allowed, which are 750, 1000, 2500 and 5000. 664 | """ 665 | 666 | FREQUENCY_VALUES = CAEN_DGTZ_DRS4Frequency_MEGA_HERTZ 667 | if MHz not in FREQUENCY_VALUES: 668 | raise ValueError(f'`MHz` must be one of {set(FREQUENCY_VALUES.keys())}, received {repr(MHz)}. ') 669 | code = libCAENDigitizer.CAEN_DGTZ_SetDRS4SamplingFrequency( 670 | self._get_handle(), 671 | c_long(FREQUENCY_VALUES[MHz]), 672 | ) 673 | check_error_code(code) 674 | 675 | def get_sampling_frequency(self) -> int: 676 | """Returns the sampling frequency as an integer number in mega Hertz.""" 677 | freq = c_long() 678 | code = libCAENDigitizer.CAEN_DGTZ_GetDRS4SamplingFrequency( 679 | self._get_handle(), 680 | byref(freq), 681 | ) 682 | check_error_code(code) 683 | return {code: MHz for MHz,code in CAEN_DGTZ_DRS4Frequency_MEGA_HERTZ.items()}[int(freq.value)] 684 | 685 | def get_record_length(self) -> int: 686 | """Returns the record length.""" 687 | record_length = c_long() 688 | code = libCAENDigitizer.CAEN_DGTZ_GetRecordLength( 689 | self._get_handle(), 690 | byref(record_length), 691 | ) 692 | check_error_code(code) 693 | return int(record_length.value) 694 | 695 | def enable_channels(self, group_1:bool, group_2:bool): 696 | """Set which groups to enable and/or disable. 697 | 698 | Arguments 699 | --------- 700 | group_1: bool 701 | Enable or disable group 1, i.e. channels 0, 1, ..., 7. 702 | group_2: bool 703 | Enable or disable group 2, i.e. channels 8, 9, ..., 15. 704 | """ 705 | mask = 0 706 | for i,group in enumerate([group_1, group_2]): 707 | mask |= (1 if group else 0) << i 708 | code = libCAENDigitizer.CAEN_DGTZ_SetGroupEnableMask( 709 | self._get_handle(), 710 | c_uint32(mask), 711 | ) 712 | check_error_code(code) 713 | 714 | def set_channel_DC_offset(self, channel:int, DAC:int=None, V:float=None): 715 | """ 716 | Set the DC offset for a channel. 717 | 718 | Note: Arguments `DAC` and `V` are to be used separately, either 719 | one or the other but not both. 720 | 721 | Arguments 722 | --------- 723 | channel: int 724 | Number of the channel to set the offset. 725 | DAC: int 726 | Value for the offset, in ADC units between 0 and 2**16-1 (65535). 727 | V: float 728 | Value for the offset in units of volt, between -1 and +1. 729 | """ 730 | if (DAC is None and V is None) or (DAC is not None and V is not None): 731 | raise ValueError(f'Both `DAC` and `V` are empty or were provided. You must provide one and only one of them.') 732 | if DAC is not None: 733 | if not isinstance(DAC, int) or not 0 <= DAC < 2**16: 734 | raise ValueError(f'`DAC` must be an integer number between 0 and 2**16-1.') 735 | if V is not None: 736 | if not isinstance(V, (int,float)) or not -1 <= V <= 1: 737 | raise ValueError('`V` must be a float between -1 and 1.') 738 | DAC = int((V+1)/2*(2**16-1)) 739 | code = libCAENDigitizer.CAEN_DGTZ_SetChannelDCOffset( 740 | self._get_handle(), 741 | c_uint32(channel), 742 | c_uint32(DAC), 743 | ) 744 | check_error_code(code) 745 | 746 | def get_channel_DC_offset(self, channel)->int: 747 | """Get the DC offset value for a channel. 748 | 749 | Arguments 750 | --------- 751 | channel: int 752 | Number of channel. 753 | """ 754 | if not isinstance(channel, int) or not 0 <= channel < 16: 755 | raise ValueError(f'`channel` must be 0, 1, ..., 15, received {repr(channel)}. ') 756 | value = c_uint32(0) 757 | code = libCAENDigitizer.CAEN_DGTZ_GetChannelDCOffset( 758 | self._get_handle(), 759 | c_uint32(channel), 760 | byref(value), 761 | ) 762 | check_error_code(code) 763 | return value.value 764 | 765 | def _start_acquisition(self): 766 | """Start the acquisition in the board. The RUN LED will turn on.""" 767 | code = libCAENDigitizer.CAEN_DGTZ_SWStartAcquisition(self._get_handle()) 768 | check_error_code(code) 769 | 770 | def _stop_acquisition(self): 771 | """Stop the acquisition. The RUN LED will turn off.""" 772 | code = libCAENDigitizer.CAEN_DGTZ_SWStopAcquisition(self._get_handle()) 773 | check_error_code(code) 774 | 775 | def _ReadData(self): 776 | """Reads data from the digitizer into the computer.""" 777 | code = libCAENDigitizer.CAEN_DGTZ_ReadData( 778 | self._get_handle(), 779 | c_long(0), 780 | self.eventBuffer, 781 | byref(self.eventBufferSize) 782 | ) 783 | check_error_code(code) 784 | 785 | def _GetNumEvents(self): 786 | """Get the number of events contained in the last block transfer 787 | initiated.""" 788 | eventNumber = c_uint32() 789 | code = libCAENDigitizer.CAEN_DGTZ_GetNumEvents( 790 | self._get_handle(), 791 | self.eventBuffer, 792 | self.eventBufferSize, 793 | byref(eventNumber) 794 | ) 795 | check_error_code(code) 796 | return eventNumber.value 797 | 798 | def _GetEventInfo(self, n_event:int): 799 | """Fill the eventInfo object declared in __init__ with stats from 800 | the i-th event in the buffer (and thus from the last block transfer). 801 | At the end of this function eventPointer will point to the i-th event. 802 | 803 | Arguments 804 | --------- 805 | n_event: int 806 | Number of event to get the event info. 807 | """ 808 | code = libCAENDigitizer.CAEN_DGTZ_GetEventInfo( 809 | self._get_handle(), 810 | self.eventBuffer, 811 | self.eventBufferSize, 812 | c_uint32(n_event), 813 | byref(self.eventInfo), 814 | byref(self.eventPointer) 815 | ) 816 | check_error_code(code) 817 | 818 | def _DecodeEvent(self): 819 | """Decode the event in eventPointer and put all data in the eventObject 820 | created in __init__. eventPointer is filled by calling getEventInfo first. 821 | """ 822 | code = libCAENDigitizer.CAEN_DGTZ_DecodeEvent( 823 | self._get_handle(), 824 | self.eventPointer, 825 | self.eventVoidPointer 826 | ) 827 | check_error_code(code) 828 | 829 | def _LoadDRS4CorrectionData(self, MHz:int): 830 | """Load correction tables from digitizer's memory at right frequency. 831 | 832 | Arguments 833 | --------- 834 | MHz: int 835 | The sampling frequency in Mega Hertz. Note that only some 836 | discrete values are allowed, which are 750, 1000, 2500 and 5000. 837 | """ 838 | FREQUENCY_VALUES = CAEN_DGTZ_DRS4Frequency_MEGA_HERTZ 839 | if MHz not in FREQUENCY_VALUES: 840 | raise ValueError(f'`MHz` must be one of {set(FREQUENCY_VALUES.keys())}, received {repr(MHz)}. ') 841 | code = libCAENDigitizer.CAEN_DGTZ_LoadDRS4CorrectionData( 842 | self._get_handle(), 843 | c_long(FREQUENCY_VALUES[MHz]) 844 | ) 845 | check_error_code(code) 846 | 847 | def _DRS4_correction(self, enable:bool): 848 | """Enable raw data correction using tables loaded with loadCorrectionData. 849 | This corrects for slight differences in ADC capacitors' values and 850 | different latency between the two groups' circutry. Refer to manual for 851 | more. 852 | """ 853 | if not isinstance(enable, bool): 854 | raise ValueError(f'`enable` must be an instance of {repr(bool)}, received object of type {repr(type(enable))}.') 855 | if enable == True: 856 | code = libCAENDigitizer.CAEN_DGTZ_EnableDRS4Correction(self._get_handle()) 857 | else: 858 | code = libCAENDigitizer.CAEN_DGTZ_DisableDRS4Correction(self._get_handle()) 859 | check_error_code(code) 860 | 861 | def get_waveforms(self, get_time:bool=True, get_ADCu_instead_of_volts:bool=False): 862 | """Reads all the data from the digitizer into the computer and parses 863 | it, returning a human friendly data structure with the waveforms. 864 | 865 | Note: The time array is produced in such a way that t=0 is the 866 | trigger time. So far I have only implemented this for the so called 867 | 'fast trigger', other options will have a time axis with an arbitrary 868 | origin. Note also that even for the fast trigger the jitter 869 | is quite high (at least for the highest sampling frequency) so 870 | it may be off. Refer to the digitizer's user manual for more details. 871 | 872 | Arguments 873 | --------- 874 | get_time: bool, default True 875 | If `True`, an array containing the time for each sample is 876 | returned within the `waveforms` dict. If `False`, the `'Time (s)'` 877 | component is omitted. This may be useful to produce smaller 878 | amounts of data to store. 879 | get_ADCu_instead_of_volts: bool, default False 880 | If `True` the `'Amplitude (V)'` component in the returned 881 | `waveforms` dict is replaced by an array containing the samples 882 | in ADC units (i.e. 0, 1, ..., 2**N_BITS-1) and called 883 | `'Amplitude (ADCu)'`. 884 | 885 | Returns 886 | ------- 887 | events: list of dict 888 | A list of dictionaries with the waveforms, of the form: 889 | ``` 890 | single_event_waveforms[channel_name][variable] 891 | ``` 892 | where `channel_name` is a string denoting the channel and 893 | `variable` is either `'Time (s)'` or `'Amplitude (V)'`. The 894 | `channel_name` takes values in `'CH0'`, `'CH1'`, ..., `'CH15'` 895 | and additionally `'trigger_group_0'` and `'trigger_group_1'` 896 | if the digitization of the trigger is enabled. In such case 897 | it is automatically added in the return dictionaries. 898 | """ 899 | 900 | self._allocateEvent() 901 | self._mallocBuffer() 902 | 903 | self._ReadData() # Bring data from digitizer to PC. 904 | 905 | # Convert the data into something human friendly for the user, i.e. all the ugly stuff is happening below... 906 | n_events = self._GetNumEvents() 907 | events = [] 908 | pointer_to_event = POINTER(Event)() 909 | for n_event in range(n_events): 910 | self._GetEventInfo(n_event) # Put the "header info" of event number `n_event` inside `self.eventInfo`, which was created in the `__init__` method. 911 | self._DecodeEvent() # Decode the event whose info was get by the previous line, and place the decoded event info in `self.eventObject`, which was created in the `__init__` method. 912 | event = self.eventObject.contents # The decoded event. Unfortunately, this still has lots of pointers to the temporary buffer so it is not persistent, we cannot return this. And I still don't know how to properly create a copy of this into my own memory block without processing each waveform individually. 913 | 914 | event_waveforms = decode_event_waveforms_to_python_friendly_stuff( 915 | event, 916 | ADC_peak_to_peak_dynamic_range_volts = 1 if get_ADCu_instead_of_volts==False else None, 917 | time_axis_parameters = dict( 918 | sampling_frequency = self.get_sampling_frequency()*1e6, 919 | post_trigger_size = self.get_post_trigger_size(), 920 | fast_trigger_mode = self.get_fast_trigger_mode(), 921 | ) if get_time else None, 922 | ) 923 | events.append(event_waveforms) 924 | 925 | self._freeEvent() 926 | self._freeBuffer() 927 | 928 | return events 929 | 930 | def wait_for(self, at_least_one_event:bool, memory_full:bool=False, timeout_seconds:float=None): 931 | """Halts the execution of the program until any of the conditions 932 | is met. Note that this means that as soon as any of the conditions 933 | is met, the execution will continue. 934 | 935 | Arguments 936 | --------- 937 | at_least_one_event: bool 938 | Specifies whether to un-halt the execution when at least one event 939 | is present in the digitizer's memory. 940 | memory_full: bool, dafault False 941 | Specifies whether to un-halt the execution then the memory of 942 | the digitizer is full. 943 | timeout_seconds: float, default None 944 | Timeout in seconds before un-halting even if no condition is 945 | met. `None` means to halt forever. 946 | """ 947 | if not isinstance(at_least_one_event, bool): 948 | raise TypeError(f'`at_least_one_event` must be an instance of {repr(bool)}, received an object of type {repr(type(at_least_one_event))}. ') 949 | if not isinstance(memory_full, bool): 950 | raise TypeError(f'`memory_full` must be an instance of {repr(bool)}, received an object of type {repr(type(memory_full))}. ') 951 | if timeout_seconds is not None: 952 | if not isinstance(timeout_seconds, (float,int)): 953 | raise TypeError(f'`timeout_seconds` must be an instance of {repr(float)}, received object of type {repr(type(timeout_seconds))}. ') 954 | if 0>timeout_seconds: 955 | raise ValueError(f'`timeout_seconds` must be greater than zero, received {timeout_seconds}. ') 956 | else: 957 | timeout_seconds = 1e99 958 | began = time.time() 959 | while True: 960 | if time.time()-began > timeout_seconds: 961 | break 962 | status = self.get_acquisition_status() 963 | if at_least_one_event==True and status['at least one event available for readout']: 964 | break 965 | if memory_full==True and status['events memory is full']: 966 | break 967 | time.sleep(.1) 968 | 969 | def __init__(): 970 | functions = [ 971 | libCAENDigitizer.CAEN_DGTZ_OpenDigitizer, 972 | libCAENDigitizer.CAEN_DGTZ_CloseDigitizer, 973 | libCAENDigitizer.CAEN_DGTZ_Reset, 974 | libCAENDigitizer.CAEN_DGTZ_WriteRegister, 975 | libCAENDigitizer.CAEN_DGTZ_ReadRegister, 976 | libCAENDigitizer.CAEN_DGTZ_SetAcquisitionMode, 977 | libCAENDigitizer.CAEN_DGTZ_GetInfo, 978 | libCAENDigitizer.CAEN_DGTZ_AllocateEvent, 979 | libCAENDigitizer.CAEN_DGTZ_MallocReadoutBuffer, 980 | libCAENDigitizer.CAEN_DGTZ_FreeEvent, 981 | libCAENDigitizer.CAEN_DGTZ_FreeReadoutBuffer, 982 | libCAENDigitizer.CAEN_DGTZ_SetMaxNumEventsBLT, 983 | libCAENDigitizer.CAEN_DGTZ_SetFastTriggerMode, 984 | libCAENDigitizer.CAEN_DGTZ_SetFastTriggerDigitizing, 985 | libCAENDigitizer.CAEN_DGTZ_SetGroupFastTriggerDCOffset, 986 | libCAENDigitizer.CAEN_DGTZ_SetGroupFastTriggerThreshold, 987 | libCAENDigitizer.CAEN_DGTZ_SetPostTriggerSize, 988 | libCAENDigitizer.CAEN_DGTZ_SetRecordLength, 989 | libCAENDigitizer.CAEN_DGTZ_SetExtTriggerInputMode, 990 | libCAENDigitizer.CAEN_DGTZ_SetTriggerPolarity, 991 | libCAENDigitizer.CAEN_DGTZ_SendSWtrigger, 992 | libCAENDigitizer.CAEN_DGTZ_SetDRS4SamplingFrequency, 993 | libCAENDigitizer.CAEN_DGTZ_SetGroupEnableMask, 994 | libCAENDigitizer.CAEN_DGTZ_SetChannelDCOffset, 995 | libCAENDigitizer.CAEN_DGTZ_GetChannelDCOffset, 996 | libCAENDigitizer.CAEN_DGTZ_SWStartAcquisition, 997 | libCAENDigitizer.CAEN_DGTZ_SWStopAcquisition, 998 | libCAENDigitizer.CAEN_DGTZ_ReadData, 999 | libCAENDigitizer.CAEN_DGTZ_GetNumEvents, 1000 | libCAENDigitizer.CAEN_DGTZ_GetEventInfo, 1001 | libCAENDigitizer.CAEN_DGTZ_DecodeEvent, 1002 | libCAENDigitizer.CAEN_DGTZ_LoadDRS4CorrectionData, 1003 | libCAENDigitizer.CAEN_DGTZ_EnableDRS4Correction 1004 | ] 1005 | for f in functions: # All digitizer functions return a long, indicating the operation outcome. Make ctypes be aware of that. 1006 | f.restype = c_long 1007 | -------------------------------------------------------------------------------- /CAENpy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SengerM/CAENpy/63dd0f69c5a716dab08e3428d6a34505f7f52e15/CAENpy/__init__.py -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Matias Senger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CAENpy 2 | 3 | Easily control [CAEN](https://www.caen.it/) equipment with Python. 4 | 5 | ## Installation 6 | 7 | ``` 8 | pip install git+https://github.com/SengerM/CAENpy 9 | ``` 10 | 11 | ## Usage 12 | 13 | Examples can be found in the [examples directory](examples). 14 | 15 | ### CAEN power supply 16 | 17 | ![Picture of a CAEN power supply](https://www.caen.it/wp-content/uploads/2017/10/DT1471HET_g.jpg) 18 | 19 | The communication with the device is done via the `set_single_channel_parameter` and `get_single_channel_parameter` methods. 20 | 21 | Simple usage example: 22 | 23 | ```python 24 | from CAENpy.CAENDesktopHighVoltagePowerSupply import CAENDesktopHighVoltagePowerSupply 25 | import time 26 | 27 | caen = CAENDesktopHighVoltagePowerSupply(port='/dev/ttyACM0') # Open the connection. 28 | print('Connected with: ', caen.idn) # This should print the name and serial number of the CAEN. 29 | 30 | # Control each channel manually: 31 | print('Ramping voltage up...') 32 | caen.channels[0].ramp_voltage(44, ramp_speed_VperSec=5) # Increase the voltage. 33 | print('V = ', caen.channels[0].V_mon, ' V') # Measure the voltage. 34 | print('I = ', caen.channels[0].I_mon, ' A') # Measure the current. 35 | caen.channels[0].ramp_voltage(0) # Go back to 0 V. 36 | print('V = ', caen.channels[0].V_mon, ' V') 37 | 38 | # Loop over the channels: 39 | for n_channel,channel in enumerate(caen.channels): 40 | print(f'##################') 41 | channel.ramp_voltage(voltage=11*(n_channel+1), ramp_speed_VperSec=5) # Ramp voltage and freeze program execution until finished. 42 | print(f'Set voltage channel {n_channel}: {channel.V_set} V') 43 | print(f'Measured voltage channel {n_channel}: {channel.V_mon} V') 44 | print(f'Set current channel {n_channel}: {channel.current_compliance} A') 45 | print(f'Measured current channel {n_channel}: {channel.I_mon} A') 46 | channel.V_set = 0 # Set the voltage to 0 without freezing program flow, voltage will ramp according to the settings in the CAEN, which of course you can change remotely using CAENpy as well. 47 | ``` 48 | 49 | The `.channels` method returns a list of `OneCAENChannel` objects, making it easier to control. To see what `OneCAENChannel` is, go to [the source code](CAENpy/CAENDesktopHighVoltagePowerSupply.py). 50 | 51 | You can also control the device by sending the commands: 52 | 53 | ```Python 54 | from CAENpy.CAENDesktopHighVoltagePowerSupply import CAENDesktopHighVoltagePowerSupply 55 | import time 56 | 57 | caen = CAENDesktopHighVoltagePowerSupply(ip='130.60.165.238', timeout=10) # Increase timeout for slow networks. 58 | # caen = CAENDesktopHighVoltagePowerSupply(port='/dev/ttyACM0') # You can also connect via USB (name of port changes in different operating systems, check the user manual of your device). 59 | 60 | # Check that the connection was successful: 61 | print(caen.idn) # Prints 'CAEN DT1470ET, SN:13398' 62 | 63 | caen.set_single_channel_parameter(parameter='ON', channel=0, value=None) 64 | for v in range(22): 65 | caen.set_single_channel_parameter( # This does not block execution! You have to manually wait the required time until the voltage is changed. 66 | parameter = 'VSET', 67 | channel = 0, 68 | value = float(v), 69 | ) 70 | print(f'VMON = {caen.get_single_channel_parameter(parameter="VMON", channel=0)} | IMON = {caen.get_single_channel_parameter(parameter="IMON", channel=0)}') 71 | time.sleep(1) 72 | caen.set_single_channel_parameter(parameter='OFF', channel=0, value=None) 73 | ``` 74 | 75 | Note that in the previous example **the execution is not blocked while the voltage is being changed**, because the ramp feature is implemented in the CAEN power supplies themselves. If you want to automatically block the execution of your code until the voltage has been smoothly ramped to the final value, use the method `ramp_voltage`. Example: 76 | 77 | ```Python 78 | from CAENpy.CAENDesktopHighVoltagePowerSupply import CAENDesktopHighVoltagePowerSupply 79 | 80 | caen = CAENDesktopHighVoltagePowerSupply(ip='130.60.165.238', timeout=10) # Increase timeout for slow networks. 81 | # caen = CAENDesktopHighVoltagePowerSupply(port='/dev/ttyACM0') # You can also connect via USB (name of port changes in different operating systems, check the user manual of your device). 82 | 83 | caen.set_single_channel_parameter(paramhttps://www.caen.it/products/caendigitizer-library/eter='ON', channel=0, value=None) 84 | for v in range(22): 85 | caen.ramp_voltage( # This blocks the execution until the VMON (i.e. measured voltage) is stable, so you don't have to manually wait/check that it has reached the final voltage. 86 | voltage = v, 87 | channel = 0, 88 | ramp_speed_VperSec = 1, # Default is 5 V/s. 89 | ) 90 | print(f'VMON = {caen.get_single_channel_parameter(parameter="VMON", channel=0)} | IMON = {caen.get_single_channel_parameter(parameter="IMON", channel=0)}') 91 | caen.set_single_channel_parameter(parameter='OFF', channel=0, value=None) 92 | ``` 93 | 94 | For more insights on how to use it, go through [the source code](CAENpy/CAENDesktopHighVoltagePowerSupply.py) which was written in a (hopefully) self explanatory way. 95 | 96 | 97 | ### CAEN digitizer 98 | 99 | ![Picture of the DT5742 digitizer](https://caen.it/wp-content/uploads/2017/10/DT5742S_featured.jpg) 100 | 101 | **Note 1** To control these digitizers you first have to install the [CAENDigitizer](https://www.caen.it/products/caendigitizer-library/) library. You can test the installation of such library using the [CAEN Wavedump](https://www.caen.it/products/caen-wavedump/) software. Once that is running, now *CAENpy* should be able to work as well. 102 | 103 | **Note 2** Depending on your operating system you may need to change the path to the *CAENDigitizer* library installation. The default path is the one for Ubuntu 22.04, but this may change. You can change the path in the file [CAENDigitizer.py](CAENpy/CAENDigitizer.py), look for the line `libCAENDigitizer = CDLL('/usr/lib/libCAENDigitizer.so')` close to the beginning of the file. 104 | 105 | Once you have everything set up, you can easily control your digitizer: 106 | 107 | ```python 108 | from CAENpy.CAENDigitizer import CAEN_DT5742_Digitizer 109 | 110 | digitizer = CAEN_DT5742_Digitizer(0) # Open the connection. 111 | 112 | print(digitizer.idn) # Print general info about the digitizer. 113 | 114 | # Now configure the digitizer: 115 | digitizer.set_sampling_frequency(5000) # Set to 5 GHz. 116 | digitizer.set_max_num_events_BLT(1) # One event per call to `digitizer.get_waveforms`. 117 | # More configuration here... 118 | 119 | # Now enter into acquisition mode using the `with` statement: 120 | with digitizer: 121 | waveforms = digitizer.get_waveforms() 122 | # The `with` statement takes care of initializing and closing the 123 | # acquisition, as well as all the ugly stuff required for this to 124 | # happen. 125 | # You can now acquire again with a new `with` block: 126 | with digitizer: 127 | new_waveforms = digitizer.get_waveforms() 128 | # and you can as well keep the acquisition running while you do 129 | # other things: 130 | with digitizer: # This takes care of starting and stopping the digitizer automatically. 131 | waveforms = [] 132 | for voltage in [0,100,200]: 133 | voltage_source.set_voltage(voltage) 134 | wf = digitizer.get_waveforms() 135 | waveforms.append(wf) 136 | ``` 137 | 138 | You can also manually start and stop the digitizer: 139 | 140 | ```python 141 | digitizer.start_acquisition() 142 | time.sleep(.5) # Wait some time for the digitizer to trigger. 143 | digitizer.stop_acquisition() 144 | waveforms = digitizer.get_waveforms() # Acquire the data. 145 | ``` 146 | 147 | Further usage examples can be found in [examples](examples). 148 | -------------------------------------------------------------------------------- /examples/HV_example_1.py: -------------------------------------------------------------------------------- 1 | from CAENpy.CAENDesktopHighVoltagePowerSupply import CAENDesktopHighVoltagePowerSupply 2 | import time 3 | 4 | caen = CAENDesktopHighVoltagePowerSupply(port='/dev/ttyACM0') # Open the connection. 5 | print('Connected with: ', caen.idn) # This should print the name and serial number of the CAEN. 6 | 7 | # Control each channel manually: 8 | print('Ramping voltage up...') 9 | caen.channels[0].ramp_voltage(44, ramp_speed_VperSec=5) # Increase the voltage. 10 | print('V = ', caen.channels[0].V_mon, ' V') # Measure the voltage. 11 | print('I = ', caen.channels[0].I_mon, ' A') # Measure the current. 12 | caen.channels[0].ramp_voltage(0) # Go back to 0 V. 13 | print('V = ', caen.channels[0].V_mon, ' V') 14 | 15 | # Loop over the channels: 16 | for n_channel,channel in enumerate(caen.channels): 17 | print(f'##################') 18 | channel.ramp_voltage(voltage=11*(n_channel+1), ramp_speed_VperSec=5) # Ramp voltage and freeze program execution until finished. 19 | print(f'Set voltage channel {n_channel}: {channel.V_set} V') 20 | print(f'Measured voltage channel {n_channel}: {channel.V_mon} V') 21 | print(f'Set current channel {n_channel}: {channel.current_compliance} A') 22 | print(f'Measured current channel {n_channel}: {channel.I_mon} A') 23 | channel.V_set = 0 # Set the voltage to 0 without freezing program flow, voltage will ramp according to the settings in the CAEN, which of course you can change remotely using CAENpy as well. -------------------------------------------------------------------------------- /examples/digitizer_example_1.py: -------------------------------------------------------------------------------- 1 | from CAENpy.CAENDigitizer import CAEN_DT5742_Digitizer 2 | import pandas 3 | import numpy 4 | import time 5 | 6 | def configure_digitizer(digitizer:CAEN_DT5742_Digitizer): 7 | digitizer.set_sampling_frequency(MHz=5000) 8 | digitizer.set_record_length(1024) 9 | digitizer.set_max_num_events_BLT(4) 10 | digitizer.set_acquisition_mode('sw_controlled') 11 | digitizer.set_ext_trigger_input_mode('disabled') 12 | digitizer.write_register(0x811C, 0x000D0001) # Enable busy signal on GPO. 13 | digitizer.set_fast_trigger_mode(enabled=True) 14 | digitizer.set_fast_trigger_digitizing(enabled=True) 15 | digitizer.enable_channels(group_1=True, group_2=True) 16 | digitizer.set_fast_trigger_threshold(22222) 17 | digitizer.set_fast_trigger_DC_offset(V=0) 18 | digitizer.set_post_trigger_size(0) 19 | for ch in [0,1]: 20 | digitizer.set_trigger_polarity(channel=ch, edge='rising') 21 | 22 | def convert_dicitonaries_to_data_frame(waveforms:dict): 23 | data = [] 24 | for n_event,event_waveforms in enumerate(waveforms): 25 | for n_channel in event_waveforms: 26 | df = pandas.DataFrame(event_waveforms[n_channel]) 27 | df['n_event'] = n_event 28 | df['n_channel'] = n_channel 29 | df.set_index(['n_event','n_channel'], inplace=True) 30 | data.append(df) 31 | return pandas.concat(data) 32 | 33 | if __name__ == '__main__': 34 | d = CAEN_DT5742_Digitizer(LinkNum=0) 35 | print('Connected with:',d.idn) 36 | 37 | configure_digitizer(d) 38 | d.set_max_num_events_BLT(1024) # Override the maximum number of events to be stored in the digitizer's self buffer. 39 | 40 | # Data acquisition --- 41 | n_events = 0 42 | ACQUIRE_AT_LEAST_THIS_NUMBER_OF_EVENTS = 2222 43 | data = [] 44 | with d: # Enable digitizer to take data 45 | print('Digitizer is enabled! Acquiring data...') 46 | while n_events < ACQUIRE_AT_LEAST_THIS_NUMBER_OF_EVENTS: 47 | time.sleep(.05) 48 | waveforms = d.get_waveforms() # Acquire the data. 49 | this_readout_n_events = len(waveforms) 50 | n_events += this_readout_n_events 51 | data += waveforms 52 | print(f'{n_events} out of {ACQUIRE_AT_LEAST_THIS_NUMBER_OF_EVENTS} were acquired.') 53 | print(f'A total of {n_events} were acquired, finishing acquisition and stopping digitizer.') 54 | 55 | print(f'Creating a pandas data frame with the data...') 56 | data = convert_dicitonaries_to_data_frame(data) 57 | 58 | print('Acquired data is:') 59 | print(data) 60 | # The previous line should print something like this: 61 | # 62 | # Amplitude (V) Time (s) 63 | # n_event n_channel 64 | # 0 CH0 0.019902 -1.626000e-07 65 | # CH0 0.019902 -1.624000e-07 66 | # CH0 0.019662 -1.622000e-07 67 | # CH0 0.021121 -1.620000e-07 68 | # CH0 0.020635 -1.618000e-07 69 | # ... ... ... 70 | # 1023 trigger_group_1 0.026258 4.120000e-08 71 | # trigger_group_1 0.026986 4.140000e-08 72 | # trigger_group_1 0.027227 4.160000e-08 73 | # trigger_group_1 0.026986 4.180000e-08 74 | # trigger_group_1 0.027718 4.200000e-08 75 | # 76 | # [18874368 rows x 2 columns] 77 | -------------------------------------------------------------------------------- /examples/digitizer_example_2.py: -------------------------------------------------------------------------------- 1 | from CAENpy.CAENDigitizer import CAEN_DT5742_Digitizer 2 | import pandas 3 | import plotly.express as px 4 | import time 5 | from digitizer_example_1 import configure_digitizer, convert_dicitonaries_to_data_frame 6 | from pathlib import Path 7 | 8 | if __name__ == '__main__': 9 | d = CAEN_DT5742_Digitizer(LinkNum=0) 10 | print('Connected with:',d.idn) 11 | 12 | configure_digitizer(d) 13 | d.set_max_num_events_BLT(3) # Override the maximum number of events to be stored in the digitizer's self buffer. 14 | 15 | # Data acquisition --- 16 | with d: 17 | print(f'Digitizer is enabled! Waiting 1 second for it to trigger...') 18 | time.sleep(1) # Wait some time for the digitizer to trigger. 19 | # At this point there should be 3 events in the digitizer (provided at least 3 trigger signals went into the trigger input). 20 | 21 | print(f'Reading data from the digitizer...') 22 | waveforms = d.get_waveforms() 23 | 24 | # Data analysis and plotting --- 25 | if len(waveforms) == 0: 26 | raise RuntimeError('Could not acquire any event. The reason may be that you dont have anything connected to the inputs of the digitizer, or a wrong trigger threshold and/or offset setting.') 27 | 28 | data = convert_dicitonaries_to_data_frame(waveforms) 29 | 30 | print('Acquired data is:') 31 | print(data) 32 | 33 | fig = px.line( 34 | title = 'CAEN digitizer testing', 35 | data_frame = data.reset_index(), 36 | x = 'Time (s)', 37 | y = 'Amplitude (V)', 38 | color = 'n_channel', 39 | facet_row = 'n_event', 40 | markers = True, 41 | ) 42 | path_to_plot = (Path(__file__).parent/'plot.html').resolve() 43 | fig.write_html(path_to_plot, include_plotlyjs='cdn') 44 | print(f'A plot with the waveforms can be found in {path_to_plot}') 45 | -------------------------------------------------------------------------------- /examples/digitizer_find_trigger.py: -------------------------------------------------------------------------------- 1 | from CAENpy.CAENDigitizer import CAEN_DT5742_Digitizer 2 | import pandas 3 | import plotly.express as px 4 | import time 5 | from digitizer import configure_digitizer, convert_dicitonaries_to_data_frame 6 | 7 | def interlaced(lst): 8 | # https://en.wikipedia.org/wiki/Interlacing_(bitmaps) 9 | lst = sorted(lst)[::-1] 10 | if len(lst) == 1: 11 | return lst 12 | result = [lst[0], lst[-1]] 13 | ranges = [(1, len(lst) - 1)] 14 | for start, stop in ranges: 15 | if start < stop: 16 | middle = (start + stop) // 2 17 | result.append(lst[middle]) 18 | ranges += (start, middle), (middle + 1, stop) 19 | return result 20 | 21 | if __name__ == '__main__': 22 | import time 23 | 24 | TIME_WINDOW_SECONDS = 1 25 | OFILENAME = 'deleteme.csv' 26 | 27 | d = CAEN_DT5742_Digitizer(LinkNum=0) 28 | print('Connected with:',d.idn) 29 | 30 | configure_digitizer(d) 31 | 32 | d.set_max_num_events_BLT(999) 33 | 34 | with open(OFILENAME, 'w') as ofile: 35 | print(f'fast_trigger_threshold,n_triggers_in_{TIME_WINDOW_SECONDS}_seconds', file=ofile) 36 | for threshold in interlaced(range(0,2**16-1)): 37 | d.set_fast_trigger_threshold(threshold) 38 | with d: 39 | time.sleep(TIME_WINDOW_SECONDS) # Give some time to record some triggers. 40 | waveforms = d.get_waveforms(get_ADCu_instead_of_volts=False) # Acquire the data. 41 | print(threshold, len(waveforms)) 42 | with open(OFILENAME, 'a') as ofile: 43 | print(f'{threshold},{len(waveforms)}', file=ofile) 44 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup( 4 | name = "CAENpy", 5 | version = "0.0.0", 6 | author = "Matias H. Senger", 7 | author_email = "m.senger@hotmail.com", 8 | description = "Control CAEN equipment with pure Python", 9 | url = "https://github.com/SengerM/CAENpy", 10 | packages = setuptools.find_packages(), 11 | classifiers = [ 12 | "Programming Language :: Python :: 3", 13 | "License :: OSI Approved :: MIT License", 14 | "Operating System :: OS Independent", 15 | ], 16 | install_requires = [ 17 | 'pyserial', 18 | ], 19 | ) 20 | --------------------------------------------------------------------------------