Files
REQubit-control/PLE/PowerSupplyCaylarLib.py
2026-06-03 12:14:02 +02:00

584 lines
22 KiB
Python

#!/usr/bin/env python
# coding: utf-8
"""
This library is provided with Caylar power supplies to allow remote control
via Ethernet connection.
This library contains an example code that can be run directly from this file
(see the code at the end of the file).
This library does not cover all available Ethernet commands.
Please refer to the Ethernet interface notice for a complete list of available commands.
For any questions or custom development please contact our team at www.caylar.net
"""
__version__ = "1.0"
__author__ = "Louis Bernot"
__email__ = "caylar@caylar.net"
__status__ = "Beta"
__license__ = "MIT"
from ast import Try
import socket
from threading import Lock
from threading import Thread
import time
import numpy as np
import pandas as pd
from scipy import interpolate
class CaylarPowerSupply(Thread):
def __init__(self, time_out=5, gap="large"):
super().__init__()
"""
create and init the power supply object with the specified time_out.
:param time_out: time out is the maximum response time from the socket in second.
:type ip: int
"""
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.time_out = time_out
self.sock_mutex = Lock() #to make library thread safe, i.e. to be sure that
#not two thread modifiy the resource at the same time
self.calibration = None
def import_calibration(self, name, caylar_path=""):
if name=="Caylar_large_holder":
data = pd.read_excel(caylar_path+"Caylar_calibration.xlsx", sheet_name="field_vs_current_gap80D10mm_V1_", usecols=range(0, 7))
B= np.array(data["Column4"][3:], dtype=float)
I = np.array(data["Column2"][3:], dtype=float)
elif name=="Caylar_large_center":
data = pd.read_excel(caylar_path+"Caylar_calibration.xlsx", sheet_name="field_vs_current_gap80D10mm_V1_", usecols=range(0, 7))
B = np.array(data["Column6"][3:], dtype=float)
I = np.array(data["Column2"][3:], dtype=float)
elif name=="Caylar_small_holder":
data = pd.read_excel(caylar_path+"Caylar_calibration.xlsx", usecols=range(0, 7))
I = np.array(data["Column2"][3:], dtype=float)
B = np.array(data["Column4"][3:], dtype=float)
elif name=="Caylar_small_center":
data = pd.read_excel(caylar_path+"Caylar_calibration.xlsx", usecols=range(0, 7))
B = np.array(data["Column6"][3:], dtype=float)
I = np.array(data["Column2"][3:], dtype=float)
else:
A = np.loadtxt(name)
I = A[:,0]
B = A[:,1]
self.calibrationB = interpolate.interp1d(B, I)
self.calibrationI = interpolate.interp1d(I, B)
def run_calibration(self, calibration_name):
power_state = int(self.getPowerState().split(" ")[1])
if (power_state == 0):
self.setPowerOn()
folder = "../../Code/caylar_class/"
tmp_rs = self.getRampSpeed_float()
self.setRampSpeed(10)
self.setCurrent_wait(-100)
sI = np.linspace(-100, 100, 41)
B = np.zeros_like(sI)
for i, I in enumerate(sI):
self.setCurrent_wait(I)
time.sleep(0.6)
B[i] =self.getField_float()
A = np.zeros((len(sI),2))
A[:, 0] = sI
A[:, 1] = B
np.savetxt(folder+calibration_name+"low.txt", A)
sI = np.linspace(100, -100, 41)
B = np.zeros_like(sI)
for i, I in enumerate(sI):
self.setCurrent_wait(I)
time.sleep(0.6)
B[i] =self.getField_float()
A = np.zeros((len(sI),2))
A[:, 0] = sI
A[:, 1] = B
np.savetxt(folder+calibration_name+"high.txt", A)
self.setCurrent_wait(0)
self.setRampSpeed(tmp_rs)
return sI, B
def connect(self, ip, port):
"""
Connect function try to create and open a socket to connect to the power supply
from the given IP on port 1234 wich is the default port of caylar power supply.
:param ip: the power supply ip
:type ip: string
"""
with self.sock_mutex:
# try:
# self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# self.sock.settimeout(self.time_out)
# self.sock.connect((ip, port))
# except:
# return 1
# else:
# return 0
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(self.time_out)
self.sock.connect((ip, port))
return 0
def disconnect(self):
"""
Close and destroy the socket connected (or not) to the power supply.
"""
with self.sock_mutex:
self.sock.close()
def getIDN(self):
"""
Returns the power supply factory id.
:return: Power supply idn.
Return format is 'CAYLAR_MPUXXXX_XXX' where XXXX_XXX is the id.
:rtype: string.
"""
return self.__sendCmd("*IDN?")
def clearDefault(self):
"""
Reset the power supply defaults.
If the present default has not been fixed, the command has no effect and the
default status remains active.
:return: Power supply clearDefault acknowledgment.
Return format is 'CLEAR_DEFAULT_OK'.
:rtype: string.
"""
return self.__sendCmd("CLEAR_DEFAULT")
def getRackTemp(self):
"""
Returns the last measured temperature of the rack containing the power supply
control electronics.
:return: Rack temperature in degree Celsius.
Return format is 'RACK_TEMP= +XX.XX Deg'.
:rtype: string.
"""
return self.__sendCmd("GET_RACK_TEMP")
def getBoxTemp(self):
"""
Returns the last measured temperature of boxe containing the power supply precision
electronics wich can be thermostated according to the power supply options.
:return: Boxe temperature in degree Celsius.
Return format is 'BOX_TEMP= +XX.XX Deg'.
:rtype: string.
"""
return self.__sendCmd("GET_BOX_TEMP")
def getAdcDacTemp(self):
"""
Returns the last measured temperature of the pcb containing the DAC and ADS.
:return: PCB (DAC and ADC) temperature in degree Celsius.
Return format is 'ADC_DAC_TEMP= +XX.XX Deg'.
:rtype: string.
"""
return self.__sendCmd("GET_ADC_DAC_TEMP")
def getWaterTemp(self):
"""
Returns the last measured temperature of power supply water cooling
at the outlet of the circuit.
WARNING: water temperature measurement is an option.
:return: Water temperature in degree Celsius.
Return format is 'WATER_TEMP= +XX.XX Deg'.
:rtype: string.
"""
return self.__sendCmd("GET_WATER_TEMP")
def getWaterFlow(self):
"""
Returns the last measured coolant flow value of power supply at the
outlet of the circuit.
WARNING: water flow measurement is an option.
:return: Coolant flow in L/Min.
Return format is 'WATER_FLOW= +XX.X L/Min'.
:rtype: string.
"""
return self.__sendCmd("GET_WATER_FLOW")
def getField(self):
"""
Returns the last measurement of the field made by the power supply hall probe. The field is measured approximately every 0.6
second depending on options and settings. The field is returned in Gauss.
"""
return self.__sendCmd("GET_FIELD")
def getField_float(self):
return float(self.getField().split(" ")[1])
def getHallTemp(self):
return self.__sendCmd("GET_HALL_PROBE_TEMP")
def getHallTemp_float(self):
return float(self.getHallTemp().split(" ")[1])
def getCurrent(self):
"""
Returns the last current measurement of the power supply.
:return: Power supply current in ampere.
Return format is 'CURRENT= +XXX.XXXXXX A'.
:rtype: string.
"""
return self.__sendCmd("GET_CURRENT")
def getRampSpeed(self):
return self.__sendCmd("GET_DIGITAL_CURRENT_RAMP_SPEED")
def getRampSpeed_float(self):
return float(self.getRampSpeed().split(" ")[2])
def getVoltage(self):
"""
Returns the last voltage measurement of the power supply.
:return: Power supply out voltage in volts.
Return format is 'VOLTAGE= +XX.XXX V'.
:rtype: string.
"""
return self.__sendCmd("GET_VOLTAGE")
def getDefaultState(self):
"""
Returns the power security status of the power supply.
:return: Power supply security status.
Return format is 'DEFAULT_STATE= X'.
X is the security status: 1=error, 0=no_error.
:rtype: string.
"""
return self.__sendCmd("GET_DEFAULT_STATE")
def getDefaultName(self):
"""
Returns the last default name of the power supply.
:return: Last default name.
Return format is 'DEFAULT_NAME= XXXXXXXXXXX'.
XXXXXXXXXXX can take the following values :
"ALIMS AUX", "MAINS", "QUENCH", "INTERLOCK 3", "LIMIT POWER", "WATCH DOG",
"LIMIT I", "PC DEFAULT", "NEG BANK", "DCCT", "POS BANK", "BANK TEMP",
"CONDENSATION", "INTERLOCK 2", "BRIDGE TEMP", "INTERLOCK 1"
:rtype: string.
"""
return self.__sendCmd("GET_DEFAULT_NAME")
def getSelectorPos(self):
"""
Returns the position of the control switch on the front pannel of the power supply.
The switch selects the source of the current setpoint between the front potentiometer,
the DAC or the external voltage entry.
:return: Selector position code.
Return format is 'CMD_SELEC= X'.
X is the selec pos code : 0=potentiometer, 1=dac, 2=external.
:rtype: string
"""
return self.__sendCmd("GET_CMD_SELEC")
def getPowerState(self):
"""
Returns the power status of the power supply.
:return: Power status.
Return format is 'POWER_STATE= X'.
X is the power status of the power supply: 1=power_on, 0=power_off.
:rtype: string.
"""
return self.__sendCmd("GET_POWER_STATE")
def getMaintenanceState(self):
"""
Returns the maintenance state of the power supply.
:return: Maintenance state.
Return format is 'MAINTENANCE_STATE= X'.
X is the maintenance state of the power supply: 1=maintenance_on, 0=maintenance_off.
:rtype: string.
"""
return self.__sendCmd("GET_MAINTENANCE_STATE")
def setCurrent(self, cmd):
"""
Changes the power supply current setpoint in DAC mode.
:param cmd: cmd is the setpoint in ampere.
:type cmd: double, int or float - signed or not.
:return: power supply out current in Ampere.
Return format is 'SET_CURRENT_OK +XXX.XXXXXX A'.
XXX.XXXXXX is the feedback setpoint which corresponds to cmd.
Return 'SET_CURRENT_ERROR OVERRANGE' if cmd is to high.
Return 'SET_CURRENT_ERROR POWER_OFF' if power is of and
'CMD UPDATE' parameter in 'divers menu' of power supply = 'PON ONLY'
Return 'SET_CURRENT_ERROR MAINTENANCE_ON' if maintenance is active.
:rtype: string.
"""
return self.__sendCmd("SET_CURRENT " + str(cmd))
def setCurrent_wait(self, cmd):
cc = float(self.getCurrent().split(" ")[1])
rs = self.getRampSpeed_float()
ret = self.setCurrent(cmd)
#print(rs, ret, cmd, np.abs(cmd-cc)/rs*1.3)
time.sleep(np.abs(cmd-cc)/rs*1.3)
return ret
def setRampSpeed(self, cmd):
return self.__sendCmd("SET_DIGITAL_FIELD_RAMP_SPEED " + str(cmd))
def setField(self,cmd):
"""
Set the digital field setpoint in field regulation (from hall probe).
:param cmd: cmd is the setpoint in ampere.
:type cmd: double, int or float - signed or not.
:return: Response in case of unrecognized argument <setpoint> : SET_FIELD_ERROR BAD_ARG
Response in case of overrange in <setpoint> argument : SET_FIELD_ERROR OVERRANGE
Response in case of power off state with « CMD UPDATE : PON ONLY» (1): SET_FIELD_ERROR POWER_OFF
Response in case of current regulation mode : SET_FIELD_ERROR CURRENT_REG
Response if maintenance is on : SET_FIELD_ERROR MAINTENANCE_ON
:rtype: string.
"""
return self.setCurrent_wait(self.calibrationB(cmd))
def getModCurrent(self):
return self.__sendCmd("GET_MODUL_CURRENT_AMPLI")
def setModCurrent(self, cmd):
"""
Changes the Modulation current
:param cmd: cmd is the setpoint in ampere.
:type cmd: double, int or float - signed or not.
:return: modulation current in Ampere.
TODO: Return format is 'SET_CURRENT_OK +XXX.XXXXXX A'.
XXX.XXXXXX is the feedback setpoint which corresponds to cmd.
Return 'SET_CURRENT_ERROR OVERRANGE' if cmd is to high.
Return 'SET_CURRENT_ERROR POWER_OFF' if power is of and
'CMD UPDATE' parameter in 'divers menu' of power supply = 'PON ONLY'
Return 'SET_CURRENT_ERROR MAINTENANCE_ON' if maintenance is active.
:rtype: string.
"""
return self.__sendCmd("SET_MODUL_CURRENT_AMPLI " + str(cmd))
def getModFreq(self):
return self.__sendCmd("GET_MODUL_FREQ")
def setModFreq(self, cmd):
"""
Changes the Modulation frequency
:param cmd: cmd is the setpoint in Hz.
:type cmd: double, int or float - signed or not.
:return: modulation frequency.
TODO: Return format is 'SET_CURRENT_OK +XXX.XXXXXX A'.
XXX.XXXXXX is the feedback setpoint which corresponds to cmd.
Return 'SET_CURRENT_ERROR OVERRANGE' if cmd is to high.
Return 'SET_CURRENT_ERROR POWER_OFF' if power is of and
'CMD UPDATE' parameter in 'divers menu' of power supply = 'PON ONLY'
Return 'SET_CURRENT_ERROR MAINTENANCE_ON' if maintenance is active.
:rtype: string.
"""
return self.__sendCmd("SET_MODUL_FREQ " + str(cmd))
def setModOn(self):
"""
Turn on the modulatoin coil
TODO:
:return: Modulation driver setPowerOn acknowledgment
TODO: Return format is 'SET_CURRENT_OK +XXX.XXXXXX A'.
XXX.XXXXXX is the feedback setpoint which corresponds to cmd.
Return 'SET_CURRENT_ERROR OVERRANGE' if cmd is to high.
Return 'SET_CURRENT_ERROR POWER_OFF' if power is of and
'CMD UPDATE' parameter in 'divers menu' of power supply = 'PON ONLY'
Return 'SET_CURRENT_ERROR MAINTENANCE_ON' if maintenance is active.
:rtype: string.
"""
return self.__sendCmd("SET_MODUL_ON")
def setsaveModOn(self):
return self.setModOn()
def setModOff(self):
"""
Turn off the modulatoin coil
TODO:
:return: Modulation driver setPowerOn acknowledgment
TODO: Return format is 'SET_CURRENT_OK +XXX.XXXXXX A'.
XXX.XXXXXX is the feedback setpoint which corresponds to cmd.
Return 'SET_CURRENT_ERROR OVERRANGE' if cmd is to high.
Return 'SET_CURRENT_ERROR POWER_OFF' if power is of and
'CMD UPDATE' parameter in 'divers menu' of power supply = 'PON ONLY'
Return 'SET_CURRENT_ERROR MAINTENANCE_ON' if maintenance is active.
:rtype: string.
"""
return self.__sendCmd("SET_MODUL_OFF")
def setMaintenanceOn(self):
"""
Switch power supply to maintenance mode to prevent SET Ethernet commands from being
able to drive power when an operator is physically working on the power supply.
See getMaintenanceState() to get the maintenance mode of the power supply.
WARNING: Maintenance mode can only be removed physically from the front face
of the power supply. The maintenance selector is available in the "SECURITIES"
menu by pressing the F3 key on the keyboard.
:return: Power supply setMaintenanceOn acknowledgment.
Return format is 'SET_MAINTENANCE_ON_OK'.
Return 'SET_MAINTENANCE_ON_ERROR MAINTENANCE_ON' if maintenance is already active.
:rtype: string.
"""
return self.__sendCmd("SET_MAINTENANCE_ON")
def setPowerOn(self):
"""
Turn the power on.
:return: Power supply setPowerOn acknowledgment.
Return format is 'SET_POWER_ON_OK'.
Return 'SET_POWER_ON_ERROR DEFAULT_ON' if default is active.
Return 'SET_POWER_ON_ERROR MAINTENANCE_ON' if maintenance is active.
:rtype: string - if there is no error the
"""
return self.__sendCmd("SET_POWER_ON")
def setPowerOff(self):
"""
Turn the power off.
WARNING: Turning off the power while current is circulating should be avoided
as much as possible. Check that the current is at zero before switching off the power.
:return: Power supply setPowerOff acknowledgment.
Return format is 'SET_POWER_OFF_OK'.
Return 'SET_POWER_OFF_ERROR MAINTENANCE_ON' if maintenance is active.
:rtype: string.
"""
return self.__sendCmd("SET_POWER_OFF")
def setUserPowerErrorOn(self):
"""
Create a user power default and prevent the power supply from restarting until the default
is removed (see setUserPowerErrorOff()).
WARNING: This function cuts the power to the power supply instantly.
Therefore, it must be used only in case of emergency.
:return: Power supply setUserPowerErrorOn acknowledgment.
Return format is 'SET_DEFAULT_ON_OK'.
Return 'SET_DEFAULT_ON_ERROR MAINTENANCE_ON' if maintenance is active.
:rtype: string.
"""
return self.__sendCmd("SET_DEFAULT_ON")
def setUserPowerErrorOff(self):
"""
Clear the user power default.
WARNING: This function don't clear the global power default state of the power supply.
You can clear the global default by press the reset button in front pannel of power supply
or by the clearDefault() function.
:return: Power supply setUserPowerErrorOn acknowledgment.
Return format is 'SET_DEFAULT_OFF_OK'.
Return 'SET_DEFAULT_OFF_ERROR MAINTENANCE_ON' if maintenance is active.
:rtype: string.
"""
return self.__sendCmd("SET_DEFAULT_OFF")
def __sendCmd(self, cmd):
with self.sock_mutex:
self.sock.send(bytes(cmd + '\n', 'ascii'))
time.sleep(0.01)
rx_buff = self.sock.recv(50).decode('ascii').replace("\n", "").replace("\r", "")
return rx_buff
"""
Little demo of the librairie.
---
This program does not take control of the power.
It simply get some of power supply reading values.
"""
if __name__ == "__main__":
import traceback
USER_PSUPPLY_IP = "129.129.180.91" #CHANGE USER_PSUPPLY_IP WITH YOUR OWN POWER SUPPLY IP HERE
USER_PSUPPLY_PORT = 1234 #CHANGE USER_PSUPPLY_IP WITH YOUR OWN POWER SUPPLY IP HERE
print("==================================================")
print("Test Caylar power supply library start !\n")
powerSupply = CaylarPowerSupply(2)
try:
print("Try to connect to " + USER_PSUPPLY_IP + " (port 1234) ...")
powerSupply.connect(USER_PSUPPLY_IP,USER_PSUPPLY_PORT)
print("Successfully connected to the power supply!\n")
print("Power supply info:")
print("---------------------------")
print(" - IDN: " + powerSupply.getIDN())
print("\n - Températures infos:")
print(" - Rack temp = " + powerSupply.getRackTemp().split(" ")[1])
print(" - Box temp = " + powerSupply.getBoxTemp().split(" ")[1])
print(" - ADC/DAC temp = " + powerSupply.getAdcDacTemp().split(" ")[1])
print("\n - Power infos:")
power_state = int(powerSupply.getPowerState().split(" ")[1])
selector_pos = int(powerSupply.getSelectorPos().split(" ")[1])
print(" - Power is " + ("off", "on")[power_state])
print(" - Output tension = " + powerSupply.getVoltage().split(" ")[1])
print(" - Output current = " + powerSupply.getCurrent().split(" ")[1])
print(" - Current remote from " + ("front potentiometer", "internal DAC", "external voltage source")[selector_pos])
print("\n - Securities infos:")
default_state = int(powerSupply.getDefaultState().split(" ")[1])
maintenance_state = int(powerSupply.getMaintenanceState().split(" ")[1])
print(" - Power supply power securitie is " + ("off", "on")[default_state])
print(" - Last default name is " + powerSupply.getDefaultName().split(" ")[1])
print(" - Maintenance is " + ("off", "on")[maintenance_state])
except:
print("soft error ...")
traceback.print_exc()
finally:
powerSupply.disconnect()