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

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

## 223 statements

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

4 Device classes for Heinzinger Digital Interface I/II and Heinzinger PNC power supply.

5

6 The Heinzinger Digital Interface I/II is used for many Heinzinger power units.

7 Manufacturer homepage:

8 https://www.heinzinger.com/products/accessories-and-more/digital-interfaces/

9

10 The Heinzinger PNC series is a series of high voltage direct current power supplies.

11 The class HeinzingerPNC is tested with two PNChp 60000-1neg and a PNChp 1500-1neg.

12 Check the code carefully before using it with other PNC devices, especially PNC3p

13 or PNCcap.

14 Manufacturer homepage:

15 https://www.heinzinger.com/products/high-voltage/universal-high-voltage-power-supplies/

16"""

17

18 import logging

19 import re

20 from abc import ABC, abstractmethod

21 from enum import IntEnum

22 from time import sleep

23 from typing import Union

24

25 from .base import SingleCommDevice

26 from ..comm import SerialCommunication, SerialCommunicationConfig

27 from ..comm.serial import (

28 SerialCommunicationParity,

29 SerialCommunicationStopbits,

30 SerialCommunicationBytesize,

31)

32 from ..configuration import configdataclass

33 from ..utils.enum import AutoNumberNameEnum

34 from ..utils.typing import Number

35

36

37@configdataclass

38 class 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

66 class 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

79 class 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

121 class 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

332 class 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

501 class HeinzingerPNCError(Exception):

502 """

503 General error with the Heinzinger PNC voltage source.

504 """

505

506 pass

507

508

509 class 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

518 class 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

527 class HeinzingerPNCDeviceNotRecognizedException(HeinzingerPNCError):

528 """

529 Error indicating that the serial number of the device

530 is not recognized.

531 """

532

533 pass