Coverage for C:\Users\hjanssen\HOME\pyCharmProjects\ethz_hvl\hvl_ccb\hvl_ccb\dev\heinzinger.py : 48%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Copyright (c) 2019-2020 ETH Zurich, SIS ID and HVL D-ITET
2#
3"""
4Device classes for Heinzinger Digital Interface I/II and Heinzinger PNC power supply.
6The Heinzinger Digital Interface I/II is used for many Heinzinger power units.
7Manufacturer homepage:
8https://www.heinzinger.com/products/accessories-and-more/digital-interfaces/
10The Heinzinger PNC series is a series of high voltage direct current power supplies.
11The class HeinzingerPNC is tested with two PNChp 60000-1neg and a PNChp 1500-1neg.
12Check the code carefully before using it with other PNC devices, especially PNC3p
13or PNCcap.
14Manufacturer homepage:
15https://www.heinzinger.com/products/high-voltage/universal-high-voltage-power-supplies/
16"""
18import logging
19import re
20from abc import ABC, abstractmethod
21from enum import IntEnum
22from time import sleep
23from typing import Union
25from .base import SingleCommDevice
26from ..comm import SerialCommunication, SerialCommunicationConfig
27from ..comm.serial import (
28 SerialCommunicationParity,
29 SerialCommunicationStopbits,
30 SerialCommunicationBytesize,
31)
32from ..configuration import configdataclass
33from ..utils.enum import AutoNumberNameEnum
34from ..utils.typing import Number
37@configdataclass
38class HeinzingerSerialCommunicationConfig(SerialCommunicationConfig):
39 #: Baudrate for Heinzinger power supplies is 9600 baud
40 baudrate: int = 9600
42 #: Heinzinger does not use parity
43 parity: Union[str, SerialCommunicationParity] = SerialCommunicationParity.NONE
45 #: Heinzinger uses one stop bit
46 stopbits: Union[int, SerialCommunicationStopbits] = SerialCommunicationStopbits.ONE
48 #: One byte is eight bits long
49 bytesize: Union[
50 int, SerialCommunicationBytesize
51 ] = SerialCommunicationBytesize.EIGHTBITS
53 #: The terminator is LF
54 terminator: bytes = b"\n"
56 #: use 3 seconds timeout as default
57 timeout: Number = 3
59 #: default time to wait between attempts of reading a non-empty text
60 wait_sec_read_text_nonempty: Number = 0.5
62 #: increased to 40 default number of attempts to read a non-empty text
63 default_n_attempts_read_text_nonempty: int = 40
66class HeinzingerSerialCommunication(SerialCommunication):
67 """
68 Specific communication protocol implementation for
69 Heinzinger power supplies.
70 Already predefines device-specific protocol parameters in config.
71 """
73 @staticmethod
74 def config_cls():
75 return HeinzingerSerialCommunicationConfig
78@configdataclass
79class HeinzingerConfig:
80 """
81 Device configuration dataclass for Heinzinger power supplies.
82 """
84 class RecordingsEnum(IntEnum):
85 ONE = 1
86 TWO = 2
87 FOUR = 4
88 EIGHT = 8
89 SIXTEEN = 16
91 #: default number of recordings used in averaging the current
92 # or the voltage [1, 2, 4, 8, 16]
93 default_number_of_recordings: Union[int, RecordingsEnum] = 1
95 #: number of decimals sent for setting the current limit or the voltage, between 1
96 # and 10
97 number_of_decimals: int = 6
99 #: Time to wait after subsequent commands during stop (in seconds)
100 wait_sec_stop_commands: Number = 0.5
102 def clean_values(self):
103 if not isinstance(self.default_number_of_recordings, self.RecordingsEnum):
104 self.force_value(
105 "default_number_of_recordings",
106 self.RecordingsEnum(self.default_number_of_recordings),
107 )
109 if self.number_of_decimals not in range(1, 11):
110 raise ValueError(
111 "The number of decimals should be " "an integer between 1 and 10."
112 )
114 if self.wait_sec_stop_commands <= 0:
115 raise ValueError(
116 "Wait time after subsequent commands during stop must be be a "
117 "positive value (in seconds)."
118 )
121class HeinzingerDI(SingleCommDevice, ABC):
122 """
123 Heinzinger Digital Interface I/II device class
125 Sends basic SCPI commands and reads the answer.
126 Only the standard instruction set from the manual is implemented.
127 """
129 class OutputStatus(IntEnum):
130 """
131 Status of the voltage output
132 """
134 UNKNOWN = -1
135 OFF = 0
136 ON = 1
138 def __init__(self, com, dev_config=None):
140 # Call superclass constructor
141 super().__init__(com, dev_config)
143 # Version of the interface (will be retrieved after com is opened)
144 self._interface_version = ""
146 # Status of the voltage output (it has to be updated via the output_on and
147 # output_off methods because querying it is not supported)
148 self._output_status = self.OutputStatus.UNKNOWN
150 def __repr__(self):
151 return f"HeinzingerDI({self._interface_version})"
153 @property
154 def output_status(self) -> OutputStatus:
155 return self._output_status
157 @staticmethod
158 def default_com_cls():
159 return HeinzingerSerialCommunication
161 @staticmethod
162 def config_cls():
163 return HeinzingerConfig
165 @abstractmethod
166 def start(self):
167 """
168 Opens the communication protocol.
170 :raises SerialCommunicationIOError: when communication port cannot be opened.
171 """
173 logging.info("Starting device " + str(self))
174 super().start()
176 self._interface_version = self.get_interface_version()
178 def stop(self) -> None:
179 """
180 Stop the device. Closes also the communication protocol.
181 """
183 logging.info(f"Stopping device {self}")
184 if not self.com.is_open:
185 logging.warning(f"Device {self} already stopped")
186 else:
187 # set the voltage to zero
188 self.set_voltage(0)
189 sleep(self.config.wait_sec_stop_commands)
190 # switch off the voltage output
191 self.output_off()
192 sleep(self.config.wait_sec_stop_commands)
193 super().stop()
195 def reset_interface(self) -> None:
196 """
197 Reset of the digital interface; only Digital Interface I:
198 Power supply is switched to the Local-Mode (Manual operation)
200 :raises SerialCommunicationIOError: when communication port is not opened
201 """
202 self.com.write_text("*RST")
204 def get_interface_version(self) -> str:
205 """
206 Queries the version number of the digital interface.
208 :raises SerialCommunicationIOError: when communication port is not opened
209 """
210 self.com.write_text("VERS?")
211 return self.com.read_text_nonempty()
213 def get_serial_number(self) -> str:
214 """
215 Ask the device for its serial number and returns the answer as a string.
217 :return: string containing the device serial number
218 :raises SerialCommunicationIOError: when communication port is not opened
219 """
220 self.com.write_text("*IDN?")
221 return self.com.read_text_nonempty()
223 def output_on(self) -> None:
224 """
225 Switch DC voltage output on and updates the output status.
227 :raises SerialCommunicationIOError: when communication port is not opened
228 """
229 self.com.write_text("OUTP ON")
230 self._output_status = self.output_status.ON
232 def output_off(self) -> None:
233 """
234 Switch DC voltage output off and updates the output status.
236 :raises SerialCommunicationIOError: when communication port is not opened
237 """
238 self.com.write_text("OUTP OFF")
239 self._output_status = self.output_status.OFF
241 def get_number_of_recordings(self) -> int:
242 """
243 Queries the number of recordings the device is using for average value
244 calculation.
246 :return: int number of recordings
247 :raises SerialCommunicationIOError: when communication port is not opened
248 """
249 self.com.write_text("AVER?")
250 answer = self.com.read_text_nonempty()
251 return int(answer)
253 def set_number_of_recordings(
254 self, value: Union[int, HeinzingerConfig.RecordingsEnum],
255 ) -> None:
256 """
257 Sets the number of recordings the device is using for average value
258 calculation. The possible values are 1, 2, 4, 8 and 16.
260 :raises SerialCommunicationIOError: when communication port is not opened
261 """
262 value = self.config.RecordingsEnum(value).value
263 self.com.write_text(f"AVER {value}")
265 def measure_voltage(self) -> float:
266 """
267 Ask the Device to measure its output voltage and return the measurement result.
269 :return: measured voltage as float
270 :raises SerialCommunicationIOError: when communication port is not opened
271 """
272 self.com.write_text("MEAS:VOLT?")
273 answer = self.com.read_text_nonempty()
274 return float(answer)
276 def set_voltage(self, value: Union[int, float]) -> None:
277 """
278 Sets the output voltage of the Heinzinger PNC to the given value.
280 :param value: voltage expressed in `self.unit_voltage`
281 :raises SerialCommunicationIOError: when communication port is not opened
282 """
284 self.com.write_text(
285 f"VOLT {{:.{self.config.number_of_decimals}f}}".format(value)
286 )
288 def get_voltage(self) -> float:
289 """
290 Queries the set voltage of the Heinzinger PNC (not the measured voltage!).
292 :raises SerialCommunicationIOError: when communication port is not opened
293 """
294 self.com.write_text("VOLT?")
295 answer = self.com.read_text_nonempty()
296 return float(answer)
298 def measure_current(self) -> float:
299 """
300 Ask the Device to measure its output current and return the measurement result.
302 :return: measured current as float
303 :raises SerialCommunicationIOError: when communication port is not opened
304 """
305 self.com.write_text("MEAS:CURR?")
306 answer = self.com.read_text_nonempty()
307 return float(answer)
309 def set_current(self, value: Union[int, float]) -> None:
310 """
311 Sets the output current of the Heinzinger PNC to the given value.
313 :param value: current expressed in `self.unit_current`
314 :raises SerialCommunicationIOError: when communication port is not opened
315 """
317 self.com.write_text(
318 f"CURR {{:.{self.config.number_of_decimals}f}}".format(value)
319 )
321 def get_current(self) -> float:
322 """
323 Queries the set current of the Heinzinger PNC (not the measured current!).
325 :raises SerialCommunicationIOError: when communication port is not opened
326 """
327 self.com.write_text("CURR?")
328 answer = self.com.read_text_nonempty()
329 return float(answer)
332class HeinzingerPNC(HeinzingerDI):
333 """
334 Heinzinger PNC power supply device class.
336 The power supply is controlled over a Heinzinger Digital Interface I/II
337 """
339 class UnitCurrent(AutoNumberNameEnum):
340 UNKNOWN = ()
341 mA = ()
342 A = ()
344 class UnitVoltage(AutoNumberNameEnum):
345 UNKNOWN = ()
346 V = ()
347 kV = ()
349 def __init__(self, com, dev_config=None):
351 # Call superclass constructor
352 super().__init__(com, dev_config)
354 # Serial number of the device (will be retrieved after com is opened)
355 self._serial_number = ""
356 # model of the device (derived from serial number)
357 self._model = ""
358 # maximum output current of the hardware (unit mA or A, depending on model)
359 self._max_current_hardware = 0
360 # maximum output voltage of the hardware (unit V or kV, depending on model)
361 self._max_voltage_hardware = 0
362 # maximum output current set by user (unit mA or A, depending on model)
363 self._max_current = 0
364 # maximum output voltage set by user (unit V or kV, depending on model)
365 self._max_voltage = 0
366 # current unit: 'mA' or 'A', depending on model
367 self._unit_current = self.UnitCurrent.UNKNOWN
368 # voltage unit: 'V' or 'kV', depending on model
369 self._unit_voltage = self.UnitVoltage.UNKNOWN
371 def __repr__(self):
372 return (
373 f"HeinzingerPNC({self._serial_number}), with "
374 f"HeinzingerDI({self._interface_version})"
375 )
377 @property
378 def max_current_hardware(self) -> Union[int, float]:
379 return self._max_current_hardware
381 @property
382 def max_voltage_hardware(self) -> Union[int, float]:
383 return self._max_voltage_hardware
385 @property
386 def unit_voltage(self) -> UnitVoltage: # noqa: F821
387 return self._unit_voltage
389 @property
390 def unit_current(self) -> UnitCurrent: # noqa: F821
391 return self._unit_current
393 @property
394 def max_current(self) -> Union[int, float]:
395 return self._max_current
397 @max_current.setter
398 def max_current(self, value: Union[int, float]):
399 if not 0 <= value <= self._max_current_hardware:
400 raise ValueError(
401 "max_current must positive " "and below max_current_hardware."
402 )
403 self._max_current = value
405 @property
406 def max_voltage(self) -> Union[int, float]:
407 return self._max_voltage
409 @max_voltage.setter
410 def max_voltage(self, value: Union[int, float]):
411 if not 0 <= value <= self._max_voltage_hardware:
412 raise ValueError(
413 "max_voltage must be positive " "and below max_voltage_hardware."
414 )
415 self._max_voltage = value
417 def start(self) -> None:
418 """
419 Opens the communication protocol and configures the device.
420 """
422 # starting Heinzinger Digital Interface
423 super().start()
425 logging.info("Starting device " + str(self))
427 # find out which type of source this is:
428 self.identify_device()
429 self.set_number_of_recordings(self.config.default_number_of_recordings)
431 def identify_device(self) -> None:
432 """
433 Identify the device nominal voltage and current based on its serial number.
435 :raises SerialCommunicationIOError: when communication port is not opened
436 """
437 serial_number = self.get_serial_number()
438 # regex to find the model of the device
439 regex_vc = r"(\d+)-(\d+)" # voltage-current info
440 regex_model = r"PNC.*?" + regex_vc + r"\s?[a-z]{3}"
441 result = re.search(regex_model, serial_number)
442 if result:
443 self._serial_number = serial_number
444 model = result.group()
445 self._model = model
446 # regex to find the nominal voltage and nominal current
447 match = re.search(regex_vc, model)
448 assert match # already matched in regex_model expression
449 voltage = int(match.group(1))
450 current = int(match.group(2))
451 # identifying the units to use for voltage and current
452 if voltage < 100000:
453 self._unit_voltage = self.UnitVoltage.V
454 self._max_voltage_hardware = voltage
455 self._max_voltage = voltage
456 else:
457 self._unit_voltage = self.UnitVoltage.kV
458 self._max_voltage_hardware = int(voltage / 1000)
459 self._max_voltage = int(voltage / 1000)
460 if current < 1000:
461 self._unit_current = self.UnitCurrent.mA
462 self._max_current_hardware = current
463 self._max_current = current
464 else:
465 self._unit_current = self.UnitCurrent.A
466 self._max_current_hardware = int(current / 1000)
467 self._max_current = int(current / 1000)
468 logging.info(f"Device {model} successfully identified")
469 else:
470 raise HeinzingerPNCDeviceNotRecognizedException(serial_number)
472 def set_voltage(self, value: Union[int, float]) -> None:
473 """
474 Sets the output voltage of the Heinzinger PNC to the given value.
476 :param value: voltage expressed in `self.unit_voltage`
477 :raises SerialCommunicationIOError: when communication port is not opened
478 """
479 if value > self.max_voltage:
480 raise HeinzingerPNCMaxVoltageExceededException
481 elif value < 0:
482 raise ValueError("voltage must be positive")
484 super().set_voltage(value)
486 def set_current(self, value: Union[int, float]) -> None:
487 """
488 Sets the output current of the Heinzinger PNC to the given value.
490 :param value: current expressed in `self.unit_current`
491 :raises SerialCommunicationIOError: when communication port is not opened
492 """
493 if value > self.max_current:
494 raise HeinzingerPNCMaxCurrentExceededException
495 elif value < 0:
496 raise ValueError("current must be positive")
498 super().set_current(value)
501class HeinzingerPNCError(Exception):
502 """
503 General error with the Heinzinger PNC voltage source.
504 """
506 pass
509class HeinzingerPNCMaxVoltageExceededException(HeinzingerPNCError):
510 """
511 Error indicating that program attempted to set the voltage
512 to a value exceeding 'max_voltage'.
513 """
515 pass
518class HeinzingerPNCMaxCurrentExceededException(HeinzingerPNCError):
519 """
520 Error indicating that program attempted to set the current
521 to a value exceeding 'max_current'.
522 """
524 pass
527class HeinzingerPNCDeviceNotRecognizedException(HeinzingerPNCError):
528 """
529 Error indicating that the serial number of the device
530 is not recognized.
531 """
533 pass