Hide keyboard shortcuts

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. 

5 

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/ 

9 

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""" 

17 

18import logging 

19import re 

20from abc import ABC, abstractmethod 

21from enum import IntEnum 

22from time import sleep 

23from typing import Union 

24 

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 

35 

36 

37@configdataclass 

38class HeinzingerSerialCommunicationConfig(SerialCommunicationConfig): 

39 #: Baudrate for Heinzinger power supplies is 9600 baud 

40 baudrate: int = 9600 

41 

42 #: Heinzinger does not use parity 

43 parity: Union[str, SerialCommunicationParity] = SerialCommunicationParity.NONE 

44 

45 #: Heinzinger uses one stop bit 

46 stopbits: Union[int, SerialCommunicationStopbits] = SerialCommunicationStopbits.ONE 

47 

48 #: One byte is eight bits long 

49 bytesize: Union[ 

50 int, SerialCommunicationBytesize 

51 ] = SerialCommunicationBytesize.EIGHTBITS 

52 

53 #: The terminator is LF 

54 terminator: bytes = b"\n" 

55 

56 #: use 3 seconds timeout as default 

57 timeout: Number = 3 

58 

59 #: default time to wait between attempts of reading a non-empty text 

60 wait_sec_read_text_nonempty: Number = 0.5 

61 

62 #: increased to 40 default number of attempts to read a non-empty text 

63 default_n_attempts_read_text_nonempty: int = 40 

64 

65 

66class HeinzingerSerialCommunication(SerialCommunication): 

67 """ 

68 Specific communication protocol implementation for 

69 Heinzinger power supplies. 

70 Already predefines device-specific protocol parameters in config. 

71 """ 

72 

73 @staticmethod 

74 def config_cls(): 

75 return HeinzingerSerialCommunicationConfig 

76 

77 

78@configdataclass 

79class HeinzingerConfig: 

80 """ 

81 Device configuration dataclass for Heinzinger power supplies. 

82 """ 

83 

84 class RecordingsEnum(IntEnum): 

85 ONE = 1 

86 TWO = 2 

87 FOUR = 4 

88 EIGHT = 8 

89 SIXTEEN = 16 

90 

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 

94 

95 #: number of decimals sent for setting the current limit or the voltage, between 1 

96 # and 10 

97 number_of_decimals: int = 6 

98 

99 #: Time to wait after subsequent commands during stop (in seconds) 

100 wait_sec_stop_commands: Number = 0.5 

101 

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 ) 

108 

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 ) 

113 

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 ) 

119 

120 

121class HeinzingerDI(SingleCommDevice, ABC): 

122 """ 

123 Heinzinger Digital Interface I/II device class 

124 

125 Sends basic SCPI commands and reads the answer. 

126 Only the standard instruction set from the manual is implemented. 

127 """ 

128 

129 class OutputStatus(IntEnum): 

130 """ 

131 Status of the voltage output 

132 """ 

133 

134 UNKNOWN = -1 

135 OFF = 0 

136 ON = 1 

137 

138 def __init__(self, com, dev_config=None): 

139 

140 # Call superclass constructor 

141 super().__init__(com, dev_config) 

142 

143 # Version of the interface (will be retrieved after com is opened) 

144 self._interface_version = "" 

145 

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 

149 

150 def __repr__(self): 

151 return f"HeinzingerDI({self._interface_version})" 

152 

153 @property 

154 def output_status(self) -> OutputStatus: 

155 return self._output_status 

156 

157 @staticmethod 

158 def default_com_cls(): 

159 return HeinzingerSerialCommunication 

160 

161 @staticmethod 

162 def config_cls(): 

163 return HeinzingerConfig 

164 

165 @abstractmethod 

166 def start(self): 

167 """ 

168 Opens the communication protocol. 

169 

170 :raises SerialCommunicationIOError: when communication port cannot be opened. 

171 """ 

172 

173 logging.info("Starting device " + str(self)) 

174 super().start() 

175 

176 self._interface_version = self.get_interface_version() 

177 

178 def stop(self) -> None: 

179 """ 

180 Stop the device. Closes also the communication protocol. 

181 """ 

182 

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() 

194 

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) 

199 

200 :raises SerialCommunicationIOError: when communication port is not opened 

201 """ 

202 self.com.write_text("*RST") 

203 

204 def get_interface_version(self) -> str: 

205 """ 

206 Queries the version number of the digital interface. 

207 

208 :raises SerialCommunicationIOError: when communication port is not opened 

209 """ 

210 self.com.write_text("VERS?") 

211 return self.com.read_text_nonempty() 

212 

213 def get_serial_number(self) -> str: 

214 """ 

215 Ask the device for its serial number and returns the answer as a string. 

216 

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() 

222 

223 def output_on(self) -> None: 

224 """ 

225 Switch DC voltage output on and updates the output status. 

226 

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 

231 

232 def output_off(self) -> None: 

233 """ 

234 Switch DC voltage output off and updates the output status. 

235 

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 

240 

241 def get_number_of_recordings(self) -> int: 

242 """ 

243 Queries the number of recordings the device is using for average value 

244 calculation. 

245 

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) 

252 

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. 

259 

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}") 

264 

265 def measure_voltage(self) -> float: 

266 """ 

267 Ask the Device to measure its output voltage and return the measurement result. 

268 

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) 

275 

276 def set_voltage(self, value: Union[int, float]) -> None: 

277 """ 

278 Sets the output voltage of the Heinzinger PNC to the given value. 

279 

280 :param value: voltage expressed in `self.unit_voltage` 

281 :raises SerialCommunicationIOError: when communication port is not opened 

282 """ 

283 

284 self.com.write_text( 

285 f"VOLT {{:.{self.config.number_of_decimals}f}}".format(value) 

286 ) 

287 

288 def get_voltage(self) -> float: 

289 """ 

290 Queries the set voltage of the Heinzinger PNC (not the measured voltage!). 

291 

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) 

297 

298 def measure_current(self) -> float: 

299 """ 

300 Ask the Device to measure its output current and return the measurement result. 

301 

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) 

308 

309 def set_current(self, value: Union[int, float]) -> None: 

310 """ 

311 Sets the output current of the Heinzinger PNC to the given value. 

312 

313 :param value: current expressed in `self.unit_current` 

314 :raises SerialCommunicationIOError: when communication port is not opened 

315 """ 

316 

317 self.com.write_text( 

318 f"CURR {{:.{self.config.number_of_decimals}f}}".format(value) 

319 ) 

320 

321 def get_current(self) -> float: 

322 """ 

323 Queries the set current of the Heinzinger PNC (not the measured current!). 

324 

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) 

330 

331 

332class HeinzingerPNC(HeinzingerDI): 

333 """ 

334 Heinzinger PNC power supply device class. 

335 

336 The power supply is controlled over a Heinzinger Digital Interface I/II 

337 """ 

338 

339 class UnitCurrent(AutoNumberNameEnum): 

340 UNKNOWN = () 

341 mA = () 

342 A = () 

343 

344 class UnitVoltage(AutoNumberNameEnum): 

345 UNKNOWN = () 

346 V = () 

347 kV = () 

348 

349 def __init__(self, com, dev_config=None): 

350 

351 # Call superclass constructor 

352 super().__init__(com, dev_config) 

353 

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 

370 

371 def __repr__(self): 

372 return ( 

373 f"HeinzingerPNC({self._serial_number}), with " 

374 f"HeinzingerDI({self._interface_version})" 

375 ) 

376 

377 @property 

378 def max_current_hardware(self) -> Union[int, float]: 

379 return self._max_current_hardware 

380 

381 @property 

382 def max_voltage_hardware(self) -> Union[int, float]: 

383 return self._max_voltage_hardware 

384 

385 @property 

386 def unit_voltage(self) -> UnitVoltage: # noqa: F821 

387 return self._unit_voltage 

388 

389 @property 

390 def unit_current(self) -> UnitCurrent: # noqa: F821 

391 return self._unit_current 

392 

393 @property 

394 def max_current(self) -> Union[int, float]: 

395 return self._max_current 

396 

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 

404 

405 @property 

406 def max_voltage(self) -> Union[int, float]: 

407 return self._max_voltage 

408 

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 

416 

417 def start(self) -> None: 

418 """ 

419 Opens the communication protocol and configures the device. 

420 """ 

421 

422 # starting Heinzinger Digital Interface 

423 super().start() 

424 

425 logging.info("Starting device " + str(self)) 

426 

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) 

430 

431 def identify_device(self) -> None: 

432 """ 

433 Identify the device nominal voltage and current based on its serial number. 

434 

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) 

471 

472 def set_voltage(self, value: Union[int, float]) -> None: 

473 """ 

474 Sets the output voltage of the Heinzinger PNC to the given value. 

475 

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") 

483 

484 super().set_voltage(value) 

485 

486 def set_current(self, value: Union[int, float]) -> None: 

487 """ 

488 Sets the output current of the Heinzinger PNC to the given value. 

489 

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") 

497 

498 super().set_current(value) 

499 

500 

501class HeinzingerPNCError(Exception): 

502 """ 

503 General error with the Heinzinger PNC voltage source. 

504 """ 

505 

506 pass 

507 

508 

509class HeinzingerPNCMaxVoltageExceededException(HeinzingerPNCError): 

510 """ 

511 Error indicating that program attempted to set the voltage 

512 to a value exceeding 'max_voltage'. 

513 """ 

514 

515 pass 

516 

517 

518class HeinzingerPNCMaxCurrentExceededException(HeinzingerPNCError): 

519 """ 

520 Error indicating that program attempted to set the current 

521 to a value exceeding 'max_current'. 

522 """ 

523 

524 pass 

525 

526 

527class HeinzingerPNCDeviceNotRecognizedException(HeinzingerPNCError): 

528 """ 

529 Error indicating that the serial number of the device 

530 is not recognized. 

531 """ 

532 

533 pass