diff --git a/services/taubpm/current/app/pyproject.toml b/services/taubpm/current/app/pyproject.toml index 74ee422..8cf3e30 100644 --- a/services/taubpm/current/app/pyproject.toml +++ b/services/taubpm/current/app/pyproject.toml @@ -5,6 +5,7 @@ description = "AGEBD-TAUBPM Service" requires-python = "==3.10.*" dependencies = [ "agebd", + "numpy>=2.0.2", ] [dependency-groups] diff --git a/services/taubpm/current/app/src/agebd_taubpm/service.py b/services/taubpm/current/app/src/agebd_taubpm/service.py index c41ad54..cda786a 100644 --- a/services/taubpm/current/app/src/agebd_taubpm/service.py +++ b/services/taubpm/current/app/src/agebd_taubpm/service.py @@ -1,49 +1,40 @@ -import time +from time import sleep +import numpy as np from epics import dbr -from agebd.pv import LocalPVLink +from agebd.pv import get_pv_class, get_pv_external_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__) PV = get_pv_class() +PV_EXTERNAL = get_pv_external_class() class PVs(BasePVs): - # Option 1: - ## Service specific PVs from dedicated IOC - my_pv1 = PV("AGEBD-TAUBPM:AO") # TODO: is this a good PV to use? + # Service specific PVs from dedicated IOC + NsamplesTAU = PV("AGEBD-TAUBPM:CONTROL-NSAMPLES-FIT-TAU.VAL") + NsamplesROC = PV("AGEBD-TAUBPM:CONTROL-NSAMPLES-FIT-ROC.VAL") + MinCurr = PV("AGEBD-TAUBPM:CONTROL-MIN-CURR.VAL") - ## Service specific PVs from other IOCs - # ... + # Service specific PVs from other IOCs + inj_active = PV_EXTERNAL("AGEBD-PARAMS:INJECTION-ACTIVE.VAL") + dump_valve = PV_EXTERNAL("ARS01-VVPG-0210:PLC_STATUS.VAL") + + # Output PVs + inj_tau = PV("AGEBD-TAUBPM:TAU.VAL") + inj_tau_err = PV("AGEBD-TAUBPM:TAU-ERR.VAL") + inj_rate = PV("AGEBD-TAUBPM:INJRATE.VAL") + inj_rate_err = PV("AGEBD-TAUBPM:INJRATE-ERR.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-TAUBPM: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-TAUBPM:CALLBACK", auto_monitor=dbr.DBE_VALUE - ) # TODO: ok? + self.CallbackPV = PV("AGEBD-DBPM3CURR:CURRENT-AVG.VAL", auto_monitor=dbr.DBE_VALUE) class Service(BaseService[PVs]): @@ -56,17 +47,102 @@ class Service(BaseService[PVs]): ): super().__init__(name, pvs, version, sleep_interval) - # TODO: What is this for? - self.path = "sls/bd/bin" + self.reset() + + def reset(self): + self.IterSinceReset = 0 + self.NsamplesTAU = int(self.pvs.NsamplesTAU.get()) + self.NsamplesROC = int(self.pvs.NsamplesROC.get()) + NsamplesMAX = int(np.nanmax([self.NsamplesTAU, self.NsamplesROC])) + self.MinCurr = self.pvs.MinCurr.get() + self.t = np.empty(NsamplesMAX) + self.I = np.empty(NsamplesMAX) + self.inj_active = self.pvs.inj_active.get() 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) + + conditions_cb_invalid = ( + not self.inj_active == pvs.inj_active.get() + or self.CallbackValue < self.MinCurr + or not 0 <= self.CallbackValue <= 500 + ) + + # set values to zero and wait without beam + if not pvs.dump_valve.get() == 1: + pvs.inj_tau.put(np.nan) + pvs.inj_tau_err.put(0.0) + pvs.inj_rate.put(0.0) + pvs.inj_rate_err.put(0.0) + sleep(0.5) + + elif conditions_cb_invalid: + self.reset() + + else: + self.t[:-1] = self.t[1:] + self.I[:-1] = self.I[1:] + self.t[-1] = self.CallbackTimestamp + self.I[-1] = self.CallbackValue + + self.IterSinceReset += 1 + + if ( + self.IterSinceReset > self.NsamplesROC + and self.IterSinceReset > self.NsamplesTAU + ): + t = self.t - self.t[0] + + p, pcov = np.polyfit(t, self.I, 1, cov=True) + perr = np.sqrt(np.diag(pcov)) + + injrate = p[0] * 60 + injrate_err = perr[0] * 60 + + pvs.inj_rate.put(injrate) + pvs.inj_rate_err.put(injrate_err) + + # injection rate in mA/hour to minimize risk of log(x) with x < 0: + injrate = injrate * 60 + injrate_err = injrate_err * 60 + + # if injrate/p[1] < 1: + if not self.inj_active and self.IterSinceReset > self.NsamplesTAU: + tau = 1 / np.log(1 - injrate / p[1]) # (-dt in hours) / ln(1 - dI/I0) + tau_err = injrate_err * (-1) / ( + (injrate - p[1]) * np.log(1 - injrate / p[1]) ** 2 + ) + perr[1] * (injrate_err) / ( + p[1] * (injrate - p[1]) * np.log(1 - injrate / p[1]) ** 2 + ) + + pvs.inj_tau.put(tau) + pvs.inj_tau_err.put(tau_err) + + elif self.IterSinceReset > self.NsamplesROC: + t = self.t[-self.NsamplesROC :] + I = self.I[-self.NsamplesROC :] + + t = t - t[0] + + p, pcov = np.polyfit(t, I, 1, cov=True) + perr = np.sqrt(np.diag(pcov)) + + injrate = p[0] * 60 + injrate_err = perr[0] * 60 + + pvs.inj_rate.put(injrate) + pvs.inj_rate_err.put(injrate_err) + + # injection rate in mA/hour to minimize risk of log(x) with x < 0: + injrate = injrate * 60 + injrate_err = injrate_err * 60 ####################################################################################### ####################################################################################### @@ -77,17 +153,15 @@ 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() - + conditions_reset = ( + not self.NsamplesROC == pvs.NsamplesROC.get() + or not self.NsamplesTAU == pvs.NsamplesTAU.get() + or not self.MinCurr == pvs.MinCurr.get() + ) + if conditions_reset: + self.reset() # maybe sleep to limit the update loop execution rate - time.sleep(1) + # sleep(1) ####################################################################################### ####################################################################################### diff --git a/services/taubpm/current/app/uv.lock b/services/taubpm/current/app/uv.lock index 9d272fd..9f28c51 100644 --- a/services/taubpm/current/app/uv.lock +++ b/services/taubpm/current/app/uv.lock @@ -26,6 +26,7 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "agebd" }, + { name = "numpy" }, ] [package.dev-dependencies] @@ -36,7 +37,10 @@ dev = [ ] [package.metadata] -requires-dist = [{ name = "agebd", editable = "../../../../packages/agebd" }] +requires-dist = [ + { name = "agebd", editable = "../../../../packages/agebd" }, + { name = "numpy", specifier = ">=2.0.2" }, +] [package.metadata.requires-dev] dev = [