Coverage for C:\Users\hjanssen\HOME\pyCharmProjects\ethz_hvl\hvl_ccb\hvl_ccb\dev\ea_psi9000.py: 28%

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

## 204 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 class for controlling a Elektro Automatik PSI 9000 power supply over VISA.

5

6 It is necessary that a backend for pyvisa is installed.

7 This can be NI-Visa oder pyvisa-py (up to know, all the testing was done with NI-Visa)

8"""

9

10 import dataclasses

11 import logging

12 import time

13 from typing import ClassVar, Iterable, Tuple, Union

14

15 from .visa import VisaDevice, VisaDeviceConfig

16 from ..comm import (

17 VisaCommunication,

18 VisaCommunicationConfig,

19)

20 from ..configuration import configdataclass

21 from ..utils.typing import Number

22

23

24 class PSI9000Error(Exception):

25 """

26 Base error class regarding problems with the PSI 9000 supply.

27 """

28

29 pass

30

31

32@configdataclass

33 class PSI9000VisaCommunicationConfig(VisaCommunicationConfig):

34 """

35 Visa communication protocol config dataclass with specification for the PSI 9000

36 power supply.

37 """

38

39 interface_type: Union[

40 str, VisaCommunicationConfig.InterfaceType

41 ] = VisaCommunicationConfig.InterfaceType.TCPIP_SOCKET # type: ignore

42

43

44 class PSI9000VisaCommunication(VisaCommunication):

45 """

46 Communication protocol used with the PSI 9000 power supply.

47 """

48

49 @staticmethod

50 def config_cls():

51 return PSI9000VisaCommunicationConfig

52

53

54@configdataclass

55 class PSI9000Config(VisaDeviceConfig):

56 """

57 Elektro Automatik PSI 9000 power supply device class.

58 The device is communicating over a VISA TCP socket.

59

60 Using this power supply, DC voltage and current can be supplied to a load with up

61 to 2040 A and 80 V (using all four available units in parallel). The maximum power

62 is limited by the grid, being at 43.5 kW available through the CEE63 power socket.

63 """

64

65 #: Power limit in W depending on the experimental setup. With

66 #: 3x63A, this is 43.5kW. Do not change this value, if you do not know what you

67 #: are doing. There is no lower power limit.

68 power_limit: Number = 43500

69

70 #: Lower voltage limit in V, depending on the experimental setup.

71 voltage_lower_limit: Number = 0.0

72

73 #: Upper voltage limit in V, depending on the experimental setup.

74 voltage_upper_limit: Number = 10.0

75

76 #: Lower current limit in A, depending on the experimental setup.

77 current_lower_limit: Number = 0.0

78

79 #: Upper current limit in A, depending on the experimental setup.

80 current_upper_limit: Number = 2040.0

81

82 wait_sec_system_lock: Number = 0.5

83 wait_sec_settings_effect: Number = 1

84 wait_sec_initialisation: Number = 2

85

86 # limit with 63 A grid connection, absolute limit, never ever change this value

87 _POWER_GRID_LIMIT: ClassVar = 43500

88

89 # do not touch this values, unless you really know what you are doing

90 _VOLTAGE_UPPER_LIMIT: ClassVar = 81.6 # nominal voltage + 2% (absolute max)

91 _CURRENT_UPPER_LIMIT: ClassVar = 2080.8 # nominal current + 2 % (absolute max)

92

93 def clean_values(self) -> None:

94

95 # check that power_limit is in range

96 if self.power_limit > self._POWER_GRID_LIMIT or self.power_limit < 0:

97 raise ValueError(

98 f"Power limit out of range. Must be in " f"0..{self._POWER_GRID_LIMIT}"

99 )

100

101 # check that lower voltage limits are in range

102 if (

103 self.voltage_lower_limit < 0

104 or self.voltage_lower_limit > self.voltage_upper_limit

105 or self.voltage_lower_limit > self._VOLTAGE_UPPER_LIMIT

106 ):

107 raise ValueError("Lower voltage limit out of range.")

108

109 # check that upper voltage limits are in range

110 if (

111 self.voltage_upper_limit < 0

112 or self.voltage_upper_limit < self.voltage_lower_limit

113 or self.voltage_upper_limit > self._VOLTAGE_UPPER_LIMIT

114 ):

115 raise ValueError("Upper voltage limit out of range.")

116

117 # check that lower current limits are in range

118 if (

119 self.current_lower_limit < 0

120 or self.current_lower_limit > self.current_upper_limit

121 or self.current_lower_limit > self._CURRENT_UPPER_LIMIT

122 ):

123 raise ValueError("Lower current limit out of range.")

124

125 # check that upper current limits are in range

126 if (

127 self.current_upper_limit < 0

128 or self.current_upper_limit < self.current_lower_limit

129 or self.current_upper_limit > self._CURRENT_UPPER_LIMIT

130 ):

131 raise ValueError("Upper current limit out of range.")

132 if self.wait_sec_system_lock <= 0:

133 raise ValueError(

134 "Wait time for system lock must be a positive value (in seconds)."

135 )

136 if self.wait_sec_settings_effect <= 0:

137 raise ValueError(

138 "Wait time for settings effect must be a positive value (in seconds)."

139 )

140 if self.wait_sec_initialisation <= 0:

141 raise ValueError(

142 "Wait time after initialisation must be a positive value (in seconds)."

143 )

144

145

146 class PSI9000(VisaDevice):

147 """

148 Elektro Automatik PSI 9000 power supply.

149 """

150

151 # unlock the device only if measured voltage and current are below these values

152 # user defined, not a technical requirement of the device

153 SHUTDOWN_VOLTAGE_LIMIT = 0.1

154 SHUTDOWN_CURRENT_LIMIT = 0.1

155

156 # Master slave nominal voltage and current, if 4 devices are in parallel

157 MS_NOMINAL_VOLTAGE = 80

158 MS_NOMINAL_CURRENT = 2040

159

160 def __init__(

161 self,

162 com: Union[PSI9000VisaCommunication, PSI9000VisaCommunicationConfig, dict],

163 dev_config: Union[PSI9000Config, dict, None] = None,

164 ):

165 super().__init__(com, dev_config)

166

167 @staticmethod

168 def config_cls():

169 return PSI9000Config

170

171 def start(self) -> None:

172 """

173 Start this device.

174 """

175

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

177 super().start()

178

179 def stop(self) -> None:

180 """

181 Stop this device. Turns off output and lock, if enabled.

182 """

183

184 logging.info("Stopping power supply.")

185

186 current_lock = self.get_system_lock()

187

188 if current_lock and self.get_output():

189 # locked and output on

190 self.set_voltage_current(0, 0)

191 time.sleep(self.config.wait_sec_settings_effect)

192 self.set_output(False)

193 time.sleep(self.config.wait_sec_settings_effect)

194

195 if current_lock:

196 # locked

197 self.set_system_lock(False)

198

199 super().stop()

200

201 @staticmethod

202 def default_com_cls():

203 return PSI9000VisaCommunication

204

205 def set_system_lock(self, lock: bool) -> None:

206 """

207 Lock / unlock the device, after locking the control is limited to this class

208 unlocking only possible when voltage and current are below the defined limits

209

210 :param lock: True: locking, False: unlocking

211 """

212

213 current_system_lock = self.get_system_lock()

214

215 if lock is current_system_lock:

216 logging.info(f"Lock already at desired state: {lock}")

217 return

218

219 if lock:

220 # we want to lock the system

221 self.com.write("SYSTem:LOCK ON")

222 time.sleep(self.config.wait_sec_system_lock)

223 new_system_lock = self.get_system_lock()

224

225 if not new_system_lock:

226 # locking was unsuccessful

227 raise PSI9000Error("Locking system unsuccessful.")

228

229 logging.info("Power supply is now locked and ready for access.")

230 return

231

232 if not lock:

233 # we want to unlock the system

234 voltage, current = self.measure_voltage_current()

235

236 if (

237 voltage > self.SHUTDOWN_VOLTAGE_LIMIT

238 or current > self.SHUTDOWN_CURRENT_LIMIT

239 ):

240 err_msg = (

241 f"Output voltage and current should be zero: "

242 f"V = {voltage} V, I = {current} A"

243 )

244 logging.error(err_msg)

245 raise PSI9000Error(err_msg)

246

247 self.com.write("SYSTem:LOCK OFF")

248 time.sleep(self.config.wait_sec_system_lock)

249 new_system_lock = self.get_system_lock()

250

251 if new_system_lock:

252 # locking was unsuccessful

253 raise PSI9000Error("Unlocking system was unsuccessful.")

254

255 logging.info("Power supply is now unlocked and ready for shutdown.")

256 return

257

258 def get_system_lock(self) -> bool:

259 """

260 Get the current lock state of the system. The lock state is true,

261 if the remote control is active and false, if not.

262

263 :return: the current lock state of the device

264 """

265

266 lock_string = self.com.query("SYSTem:LOCK:OWNer?")

267 if lock_string == "REMOTE":

268 return True

269 elif lock_string == "NONE" or lock_string == "LOCAL":

270 return False

271 else:

272 raise PSI9000Error(

273 f"Illegal answer to SYSTem:LOCK:OWNer? received: {lock_string}"

274 )

275

276 def set_output(self, target_onstate: bool) -> None:

277 """

278 Enables / disables the DC output.

279

280 :param target_onstate: enable or disable the output power

281 :raises PSI9000Error: if operation was not successful

282 """

283

284 if target_onstate:

285 self.com.write("OUTPut ON")

286 elif not target_onstate:

287 self.com.write("OUTPut OFF")

288

289 output_state = self.get_output()

290

291 if target_onstate and output_state:

292 logging.info("Output successfully switched on.")

293 elif target_onstate and not output_state:

294 logging.error("Output was not switched on.")

295 raise PSI9000Error("Output was not switched on.")

296 elif not target_onstate and not output_state:

297 logging.info("Output successfully switched off.")

298 elif not target_onstate and output_state:

299 logging.error("Output was not switched off.")

300 raise PSI9000Error("Output was not switched off.")

301

302 def get_output(self) -> bool:

303 """

304 Reads the current state of the DC output of the source. Returns True,

305 if it is enabled, false otherwise.

306

307 :return: the state of the DC output

308 """

309

310 on_off_string = self.com.query("OUTPut?")

311 if on_off_string == "ON":

312 return True

313 elif on_off_string == "OFF":

314 return False

315 else:

316 raise PSI9000Error(f"Query of OUTPut? is not ON or OFF: {on_off_string}")

317

318 def measure_voltage_current(self) -> Tuple[float, float]:

319 """

320 Measure the DC output voltage and current

321

322 :return: Umeas in V, Imeas in A

323 """

324

325 list_ret = self.com.query("MEASure:VOLTage?", "MEASure:CURRent?")

326 assert len(list_ret) == 2

327 ret = self._remove_units(list_ret)

328 return ret[0], ret[1]

329

330 def set_voltage_current(self, volt: float, current: float) -> None:

331 """

332 Set voltage and current setpoints.

333

334 After setting voltage and current, a check is performed if writing was

335 successful.

336

337 :param volt: is the setpoint voltage: 0..81.6 V (1.02 * 0-80 V)

338 (absolute max, can be smaller if limits are set)

339 :param current: is the setpoint current: 0..2080.8 A (1.02 * 0 - 2040 A)

340 (absolute max, can be smaller if limits are set)

341 :raises PSI9000Error: if the desired setpoint is out of limits

342 """

343

344 self.com.write(f"SOURce:VOLTage {volt:f}", f"SOURce:CURRent {current:f}")

345

346 read_voltage, read_current = self.get_voltage_current_setpoint()

347

348 if read_voltage == volt and read_current == current:

349 logging.info(

350 f"Setting voltage to {volt:.2f} V and current to {current:.2f} A was "

351 f"successful."

352 )

353

354 else:

355 v_lower, i_lower = self.get_ui_lower_limits()

356 v_upper, i_upper, p_upper = self.get_uip_upper_limits()

357

358 err_msg = (

359 f"Setting U = {volt:f} V and I = {current:f} A was not successful: "

360 f"voltage has to be between {v_lower} V and {v_upper} V, "

361 f"current between {i_lower} A and {i_upper} A. "

362 f"Actual settings are: U = {read_voltage} V, I = {read_current} A"

363 )

364 logging.error(err_msg)

365 raise PSI9000Error(err_msg)

366

367 def get_voltage_current_setpoint(self) -> Tuple[float, float]:

368 """

369 Get the voltage and current setpoint of the current source.

370

371 :return: Uset in V, Iset in A

372 """

373

374 list_ret = self.com.query("SOURce:VOLTage?", "SOURce:CURRent?")

375 assert len(list_ret) == 2

376 ret = self._remove_units(list_ret)

377 return ret[0], ret[1]

378

379 def set_upper_limits(

380 self,

381 voltage_limit: float = None,

382 current_limit: float = None,

383 power_limit: float = None,

384 ) -> None:

385 """

386 Set the upper limits for voltage, current and power.

387 After writing the values a check is performed if the values are set.

388 If a parameter is left blank, the maximum configurable limit is set.

389

390 :param voltage_limit: is the voltage limit in V

391 :param current_limit: is the current limit in A

392 :param power_limit: is the power limit in W

393 :raises PSI9000Error: if limits are out of range

394 """

395

396 voltage_limit = (

397 voltage_limit

398 if voltage_limit is not None

399 else self.config.voltage_upper_limit

400 )

401 current_limit = (

402 current_limit

403 if current_limit is not None

404 else self.config.current_upper_limit

405 )

406 power_limit = (

407 power_limit if power_limit is not None else self.config.power_limit

408 )

409

410 v_lower, i_lower = self.get_ui_lower_limits()

411

412 # Creates a new configclass object just to check the limits; will raise

413 # ValueError if not within limits (see PSI9000Config.clean_values)

414 dataclasses.replace(

415 self.config,

416 voltage_lower_limit=v_lower,

417 current_lower_limit=i_lower,

418 voltage_upper_limit=voltage_limit,

419 current_upper_limit=current_limit,

420 power_limit=power_limit,

421 )

422

423 self.com.write(

424 f"SOURce:VOLTage:LIMit:HIGH {voltage_limit}",

425 f"SOURce:CURRent:LIMit:HIGH {current_limit}",

426 f"SOURce:POWer:LIMit:HIGH {power_limit}",

427 )

428

429 # wait until settings are made

430 time.sleep(self.config.wait_sec_settings_effect)

431

432 v_higher, i_higher, p_higher = self.get_uip_upper_limits()

433

434 if (

435 v_higher == voltage_limit

436 and i_higher == current_limit

437 and p_higher == power_limit

438 ):

439 logging.info(

440 f"New upper voltage, current, power limits set: Umax = {voltage_limit} "

441 f"V, Imax = {current_limit} A, Pmax = {power_limit} W"

442 )

443

444 else:

445 raise PSI9000Error("Setting upper limits was not successful.")

446

447 def set_lower_limits(

448 self, voltage_limit: float = None, current_limit: float = None

449 ) -> None:

450 """

451 Set the lower limits for voltage and current.

452 After writing the values a check is performed if the values are set correctly.

453

454 :param voltage_limit: is the lower voltage limit in V

455 :param current_limit: is the lower current limit in A

456 :raises PSI9000Error: if the limits are out of range

457 """

458

459 voltage_limit = (

460 voltage_limit

461 if voltage_limit is not None

462 else self.config.voltage_lower_limit

463 )

464 current_limit = (

465 current_limit

466 if current_limit is not None

467 else self.config.current_lower_limit

468 )

469

470 v_upper, i_upper, p_upper = self.get_uip_upper_limits()

471

472 # Creates a new configclass object just to check the limits; will raise

473 # ValueError if not within limits (see PSI9000Config.clean_values)

474 dataclasses.replace(

475 self.config,

476 voltage_upper_limit=v_upper,

477 current_upper_limit=i_upper,

478 power_limit=p_upper,

479 voltage_lower_limit=voltage_limit,

480 current_lower_limit=current_limit,

481 )

482

483 self.com.write(

484 f"SOURce:VOLTage:LIMit:LOW {voltage_limit}",

485 f"SOURce:CURRent:LIMit:LOW {current_limit}",

486 )

487

488 v_lower, i_lower = self.get_ui_lower_limits()

489

490 if v_lower == voltage_limit and i_lower == current_limit:

491 logging.info(

492 f"New lower voltage and current limits set: Umin = {voltage_limit} V, "

493 f"Imin = {current_limit} A"

494 )

495

496 else:

497 raise PSI9000Error("Setting lower limits was unsuccessful.")

498

499 def get_ui_lower_limits(self) -> Tuple[float, float]:

500 """

501 Get the lower voltage and current limits. A lower power limit does not exist.

502

503 :return: Umin in V, Imin in A

504 """

505

506 list_ret = self.com.query(

507 "SOURce:VOLTage:LIMit:LOW?", "SOURce:CURRent:LIMit:LOW?"

508 )

509 assert len(list_ret) == 2

510 ret = self._remove_units(list_ret)

511 return ret[0], ret[1]

512

513 def get_uip_upper_limits(self) -> Tuple[float, float, float]:

514 """

515 Get the upper voltage, current and power limits.

516

517 :return: Umax in V, Imax in A, Pmax in W

518 """

519

520 list_ret = self.com.query(

521 "SOURce:VOLTage:LIMit:HIGH?",

522 "SOURce:CURRent:LIMit:HIGH?",

523 "SOURce:POWer:LIMit:HIGH?",

524 )

525 assert len(list_ret) == 3

526 ret = self._remove_units(list_ret)

527 return ret[0], ret[1], ret[2]

528

529 def check_master_slave_config(self) -> None:

530 """

531 Checks if the master / slave configuration and initializes if successful

532

533 :raises PSI9000Error: if master-slave configuration failed

534 """

535

536 # verify that MS is enabled

537 if self.com.query("SYSTem:MS:ENABle?") != "ON":

538 logging.error("Master-slave-mode not enabled.")

539 raise PSI9000Error("Master-slave-mode not enabled.")

540

541 # verify that this device ist MASTER

542 if self.com.query("SYSTem:MS:LINK?") != "MASTER":

543 logging.error("Device is not Master.")

544 raise PSI9000Error("Device is not Master.")

545

546 # begin initialization

547 self.com.write("SYSTem:MS:INITialisation")

548 time.sleep(self.config.wait_sec_initialisation)

549

550 # check for correct init

551 if self.com.query("SYSTem:MS:CONDition?") != "INIT":

552 logging.error("Master-slave initialisation failed.")

553 raise PSI9000Error("Master-slave initialisation failed.")

554

555 # read resulting master-slave nominal voltage and current

556 ms_voltage, ms_current = self._remove_units(

557 self.com.query("SYSTem:MS:NOMinal:VOLTage?", "SYSTem:MS:NOMinal:CURRent?")

558 )

559

560 # check for correct total voltage and current

561 if (

562 ms_current != self.MS_NOMINAL_CURRENT

563 or ms_voltage != self.MS_NOMINAL_VOLTAGE

564 ):

565 err_msg = (

566 f"Nominal voltage and current should be {self.MS_NOMINAL_VOLTAGE} V, "

567 f"{self.MS_NOMINAL_CURRENT} A; but are {ms_voltage} V, {ms_current} A."

568 )

569 logging.error(err_msg)

570 raise PSI9000Error(err_msg)

571

572 # here the initialization is successful

573 logging.info("Initialization of Master/Slave successful")

574

575 @staticmethod

576 def _remove_units(list_with_strings: Iterable[str]) -> Tuple[float, ...]:

577 """

578 Removes the last two characters of each string in the list and

579 convert it to float (is only working for units with one character e.g. V, A, W)

580

581 :param list_with_strings: list with return strings containing value and unit

582 :return: list of floats without units

583 """

584

585 return tuple(float(value[:-2]) for value in list_with_strings)