feat: migrate tunefbx python service
This commit is contained in:
@@ -5,6 +5,7 @@ description = "AGEBD-TUNEFBX Service"
|
||||
requires-python = "==3.10.*"
|
||||
dependencies = [
|
||||
"agebd",
|
||||
"numpy>=2.2.6",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import time
|
||||
from time import perf_counter
|
||||
|
||||
import numpy as np
|
||||
from epics import dbr
|
||||
|
||||
from agebd.pv import LocalPVLink
|
||||
from agebd.pv import get_pv_class
|
||||
from agebd.service.base import BaseService
|
||||
from agebd.service.pvs import BasePVs
|
||||
from agebd.utils import printgetversion
|
||||
from agebd.pv import get_pv_class
|
||||
|
||||
__version__ = printgetversion(__file__)
|
||||
|
||||
@@ -14,36 +14,86 @@ PV = get_pv_class()
|
||||
|
||||
|
||||
class PVs(BasePVs):
|
||||
# Option 1:
|
||||
## Service specific PVs from dedicated IOC
|
||||
my_pv1 = PV("AGEBD-TUNEFBX:AO") # TODO: is this a good PV to use?
|
||||
kp = PV("AGEBD-TUNEFBX:CONTROL-KP.VAL")
|
||||
ki = PV("AGEBD-TUNEFBX:CONTROL-KI.VAL")
|
||||
kd = PV("AGEBD-TUNEFBX:CONTROL-KD.VAL")
|
||||
qx_ref = PV("AGEBD-TUNEFBX:CONTROL-QX.VAL")
|
||||
qx_okrng = PV("AGEBD-TUNEFBX:CONTROL-QX-OKRNG.VAL")
|
||||
qx_drvh = PV("AGEBD-TUNEFBX:CONTROL-QXMAX.VAL")
|
||||
qx_drvl = PV("AGEBD-TUNEFBX:CONTROL-QXMIN.VAL")
|
||||
mincur = PV("AGEBD-TUNEFBX:CONTROL-MIN-CURR.VAL")
|
||||
dq = PV("AGEBD-TUNEFBX:DELTAQ.VAL")
|
||||
dq_ulim = PV("AGEBD-TUNEFBX:DELTAQ-ULIM.VAL")
|
||||
dq_llim = PV("AGEBD-TUNEFBX:DELTAQ-LLIM.VAL")
|
||||
check1 = PV("AGEBD-TUNEFBX:CHECK1.VAL")
|
||||
check2 = PV("AGEBD-TUNEFBX:CHECK2.VAL")
|
||||
check3 = PV("AGEBD-TUNEFBX:CHECK3.VAL")
|
||||
check4 = PV("AGEBD-TUNEFBX:CHECK4.VAL")
|
||||
check5 = PV("AGEBD-TUNEFBX:CHECK5.VAL")
|
||||
check6 = PV("AGEBD-TUNEFBX:CHECK6.VAL")
|
||||
check7 = PV("AGEBD-TUNEFBX:CHECK7.VAL")
|
||||
check8 = PV("AGEBD-TUNEFBX:CHECK8.VAL")
|
||||
|
||||
# Output PVs
|
||||
qx_stepdec = PV("AGEBD-TUNEBUMP:QX-STEPDOWN.VAL")
|
||||
qx_stepinc = PV("AGEBD-TUNEBUMP:QX-STEPUP.VAL")
|
||||
qx_stepsiz = PV("AGEBD-TUNEBUMP:QX-STEPSIZE.VAL")
|
||||
|
||||
## Service specific PVs from other IOCs
|
||||
# ...
|
||||
inj_active = PV("AGEBD-PARAMS:INJECTION-ACTIVE.VAL")
|
||||
dump_valve = PV("ARS01-VVPG-0210:PLC_STATUS.VAL")
|
||||
hor_pinger_trig = PV("AGETI-CVME-MASTER-TMA:Evt-SR-Pinger-H-Ena-Sel.VAL")
|
||||
ver_pinger_trig = PV("AGETI-CVME-MASTER-TMA:Evt-SR-Pinger-V-Ena-Sel.VAL")
|
||||
bpm_qx_pilot_int = PV("ARS07-DBPM-0840:PILOT-INT-SWITCH.VAL") # must be: 0 No Pilot
|
||||
bpm_qx_pilot_ext = PV("ARS07-DBPM-0840:PILOT-EXT-SWITCH.VAL") # must be: 0 No Pilot
|
||||
bpm_qx_chopper = PV("ARS07-DBPM-0840:CHOPPER-MODE.VAL") # must be: 0 Periodic
|
||||
cur = PV("ARS07-DPCT-0000:CURR.VAL")
|
||||
|
||||
def __init__(self, service_name: str, pv_factory=PV):
|
||||
super().__init__(service_name, pv_factory)
|
||||
PV = pv_factory
|
||||
|
||||
# TODO: can we do this cleaner? Maybe mechanism for
|
||||
# settings initial values in dev mode (e.g. ini file)
|
||||
if isinstance(self.onoff, LocalPVLink):
|
||||
self.onoff.value = True
|
||||
|
||||
# Option 2:
|
||||
## Service specific PVs from dedicated IOC
|
||||
self.my_pv2 = PV(
|
||||
"AGEBD-TUNEFBX:BO"
|
||||
) # TODO: is this a good PV to use?
|
||||
|
||||
## Service specific PVs from other IOCs
|
||||
# ...
|
||||
|
||||
# TODO: class attribute names usually lowercase
|
||||
# usually it is advisable to trigger the mainloop of a service on changes of certain PVs:
|
||||
self.CallbackPV = PV(
|
||||
"AGEBD-TUNEFBX:CALLBACK", auto_monitor=dbr.DBE_VALUE
|
||||
) # TODO: ok?
|
||||
self.CallbackPV = PV("AGEBD-TUNEBUMP:QX.VAL", auto_monitor=dbr.DBE_VALUE)
|
||||
|
||||
|
||||
class PID:
|
||||
# Proportional-Integral-Derivative (PID) controller class
|
||||
# PID setup procedure:
|
||||
# 1. set integral and derivative gains ki, kd to 0
|
||||
# 2. set proportional gain kp to low, stable value
|
||||
# 3. increase kp and create a disturbance, e.g., by changing the setpoint
|
||||
# 4. increase kp until feedback parameter oscillates
|
||||
# 5. set kp to 40 % of the critical value triggering oscillation
|
||||
# 6. slowly increase ki to eliminate the steady state error
|
||||
# 7. adjust it until setpoint is reached accurately without overshoot
|
||||
# 8. tune kd to minimize oscillations and overshoots (caution: high kd can amplify noise)
|
||||
|
||||
def __init__(self, kp, ki, kd, setpoint):
|
||||
|
||||
# Proportional Gain >> The "present" term. It provides a response proportional to the current error. Increasing kp makes the system respond faster, but if pushed too high, it will cause violent overshoot and oscillation.
|
||||
self.kp = kp
|
||||
# Integral Gain >> The "past" term. It accumulates the errors over time to eliminate steady-state error. However, high ki values can cause sluggishness or instability (e.g., integral windup).
|
||||
self.ki = ki
|
||||
# Derivative Gain >> The "future" term. It measures the rate of change of the error, acting as a damping factor to reduce overshoot and smooth out rapid, unwanted movements
|
||||
self.kd = kd
|
||||
self.setpoint = setpoint
|
||||
self.integral = 0
|
||||
self.prev_error = 0
|
||||
|
||||
def update(self, measurement, dt):
|
||||
|
||||
error = self.setpoint - measurement
|
||||
self.integral += error * dt
|
||||
derivative = (error - self.prev_error) / dt if dt > 0 else 0
|
||||
|
||||
self.prev_error = error
|
||||
|
||||
# Calculate the final output using Kp, Ki, and Kd which is written to actuator
|
||||
output = (self.kp * error) + (self.ki * self.integral) + (self.kd * derivative)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
class Service(BaseService[PVs]):
|
||||
@@ -56,17 +106,103 @@ class Service(BaseService[PVs]):
|
||||
):
|
||||
super().__init__(name, pvs, version, sleep_interval)
|
||||
|
||||
# TODO: What is this for?
|
||||
self.path = "sls/bd/bin"
|
||||
self.control = 0.0
|
||||
self.dq_llim = 0.0
|
||||
self.dq_ulim = 0.0
|
||||
|
||||
self.qx_okrng = pvs.qx_okrng.get()
|
||||
|
||||
self.PID = PID(pvs.kp.get(), pvs.ki.get(), pvs.kd.get(), pvs.qx_ref.get())
|
||||
# start stop watch
|
||||
self.t0 = perf_counter()
|
||||
|
||||
def update(self):
|
||||
pvs = self.pvs
|
||||
# when callback is fired, this code will be executed
|
||||
if self.CallbackFired:
|
||||
#######################################################################################
|
||||
#######################################################################################
|
||||
# here goes all the code that your service
|
||||
# is supposed to be running when the callback is fired
|
||||
time.sleep(1)
|
||||
# here goes all the code that your service is supposed to be
|
||||
# running every time when the callback is fired
|
||||
|
||||
# update PID settings
|
||||
self.PID.setpoint = pvs.qx_ref.get()
|
||||
self.PID.kp = pvs.kp.get()
|
||||
self.PID.ki = pvs.ki.get()
|
||||
self.PID.kd = pvs.kd.get()
|
||||
|
||||
self.qx_okrng = pvs.qx_okrng.get()
|
||||
|
||||
############
|
||||
## CHECKS ##
|
||||
############
|
||||
# 1. measured tune within drivelimits
|
||||
self.check1 = (
|
||||
pvs.qx_drvl.get() < self.CallbackValue and self.CallbackValue < pvs.qx_drvh.get()
|
||||
)
|
||||
pvs.check1.put(self.check1)
|
||||
|
||||
# 2. measured tune outside ok range limits
|
||||
self.check2 = self.CallbackValue < (
|
||||
self.PID.setpoint - self.qx_okrng
|
||||
) or self.CallbackValue > (self.PID.setpoint + self.qx_okrng)
|
||||
pvs.check2.put(self.check2)
|
||||
|
||||
# 3. injection not active
|
||||
self.check3 = not pvs.inj_active.get()
|
||||
pvs.check3.put(self.check3)
|
||||
|
||||
# 4. stored beam current above threshold
|
||||
self.check4 = pvs.cur.get() > pvs.mincur.get()
|
||||
pvs.check4.put(self.check4)
|
||||
|
||||
# 5. bpm setup correctly for tune measurement
|
||||
self.check5 = (
|
||||
pvs.bpm_qx_pilot_int.get() == 0
|
||||
and pvs.bpm_qx_pilot_ext.get() == 0
|
||||
and pvs.bpm_qx_chopper.get() == 1
|
||||
)
|
||||
pvs.check5.put(self.check5)
|
||||
|
||||
# 6. pingers are on
|
||||
self.check6 = pvs.hor_pinger_trig.get() and pvs.ver_pinger_trig.get()
|
||||
pvs.check6.put(self.check6)
|
||||
|
||||
# 7. dt corresponds to 3.125 Hz, i.e., min. 2 consecutive valid measurements
|
||||
t1 = perf_counter()
|
||||
self.dt = t1 - self.t0
|
||||
self.t0 = t1
|
||||
self.check7 = self.dt < 1.1 / 3.125
|
||||
pvs.check7.put(self.check7)
|
||||
|
||||
# check all checks before PID call
|
||||
self.checkall = (
|
||||
self.check1
|
||||
and self.check2
|
||||
and self.check3
|
||||
and self.check4
|
||||
and self.check5
|
||||
and self.check6
|
||||
and self.check7
|
||||
)
|
||||
|
||||
if self.checkall:
|
||||
# 8. PID calculated step within limits
|
||||
self.control = self.PID.update(self.CallbackValue, self.dt)
|
||||
self.dq_llim = pvs.dq_llim.get()
|
||||
self.dq_ulim = pvs.dq_ulim.get()
|
||||
|
||||
self.check8 = self.control > self.dq_llim and self.control < self.dq_ulim
|
||||
pvs.check8.put(self.check8)
|
||||
|
||||
if self.check8:
|
||||
pvs.dq.put(self.control)
|
||||
pvs.qx_stepsiz.put(np.abs(self.control))
|
||||
if self.control < 0:
|
||||
pvs.qx_stepdec.put(1)
|
||||
else:
|
||||
pvs.qx_stepinc.put(1)
|
||||
|
||||
#######################################################################################
|
||||
#######################################################################################
|
||||
|
||||
@@ -77,17 +213,9 @@ class Service(BaseService[PVs]):
|
||||
#######################################################################################
|
||||
# here goes all the code that your service
|
||||
# is supposed to be running in any case, e.g.,
|
||||
|
||||
# update some pvs/vals
|
||||
my_pv1_random_val = str(time.clock_gettime_ns(0))[-1]
|
||||
self.pvs.my_pv1.put(my_pv1_random_val)
|
||||
# self.pvs.my_pv2.put("MY_PV2 value")
|
||||
|
||||
# get some pvs/vals
|
||||
self.val1 = self.pvs.my_pv1.get()
|
||||
self.val2 = self.pvs.my_pv2.get()
|
||||
|
||||
# maybe sleep to limit the update loop execution rate
|
||||
time.sleep(1)
|
||||
# sleep(1)
|
||||
#######################################################################################
|
||||
#######################################################################################
|
||||
|
||||
Generated
+5
-1
@@ -30,6 +30,7 @@ version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "agebd" },
|
||||
{ name = "numpy" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
@@ -40,7 +41,10 @@ dev = [
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "agebd", editable = "../../../../packages/agebd" }]
|
||||
requires-dist = [
|
||||
{ name = "agebd", editable = "../../../../packages/agebd" },
|
||||
{ name = "numpy", specifier = ">=2.2.6" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
|
||||
Reference in New Issue
Block a user