From 4e27b6fe7a31464942834756c4810df51ba8b0da Mon Sep 17 00:00:00 2001 From: Benjamin Labrecque Date: Thu, 16 Jul 2026 16:02:16 +0200 Subject: [PATCH] feat: migrate scrubbing python service -- as much as possible :D --- services/scrubbing/current/app/pyproject.toml | 1 + .../current/app/src/agebd_scrubbing/main.py | 12 +- .../current/app/src/agebd_scrubbing/runner.py | 280 +++++++++ .../app/src/agebd_scrubbing/service.py | 546 +++++++++++++++--- .../scrubbing/current/app/tests/test_app.py | 3 +- services/scrubbing/current/app/uv.lock | 6 +- 6 files changed, 775 insertions(+), 73 deletions(-) create mode 100644 services/scrubbing/current/app/src/agebd_scrubbing/runner.py diff --git a/services/scrubbing/current/app/pyproject.toml b/services/scrubbing/current/app/pyproject.toml index f984959..b81a822 100644 --- a/services/scrubbing/current/app/pyproject.toml +++ b/services/scrubbing/current/app/pyproject.toml @@ -5,6 +5,7 @@ description = "AGEBD-SCRUBBING Service" requires-python = "==3.10.*" dependencies = [ "agebd", + "numpy>=2.2.6", ] [dependency-groups] diff --git a/services/scrubbing/current/app/src/agebd_scrubbing/main.py b/services/scrubbing/current/app/src/agebd_scrubbing/main.py index b92c9fc..73085b9 100644 --- a/services/scrubbing/current/app/src/agebd_scrubbing/main.py +++ b/services/scrubbing/current/app/src/agebd_scrubbing/main.py @@ -1,13 +1,13 @@ import typer from agebd.enums import LogLevel -from agebd.runner import CallbackRunner from agebd.utils import init_logging from agebd_scrubbing import PVs, Service - +from agebd_scrubbing.runner import Runner # TODO: remove: cicd test 10 + def main( log_level: LogLevel = typer.Option( LogLevel.INFO, @@ -19,11 +19,15 @@ def main( case_sensitive=False, ), ): + """ + Uses custom/old Runner because migrating to the agebd Runner would be quite + complex. + """ init_logging(log_level) service_name = "SCRUBBING" pvs = PVs(service_name=service_name) - service = Service(name=service_name, pvs=pvs) - runner = CallbackRunner(service=service) + service = Service(pvs=pvs) + runner = Runner(svc=service) runner.start() diff --git a/services/scrubbing/current/app/src/agebd_scrubbing/runner.py b/services/scrubbing/current/app/src/agebd_scrubbing/runner.py new file mode 100644 index 0000000..2ad2058 --- /dev/null +++ b/services/scrubbing/current/app/src/agebd_scrubbing/runner.py @@ -0,0 +1,280 @@ +import traceback +from time import perf_counter, sleep + +import numpy as np +from epics import poll + +from agebd_scrubbing.service import Service + + +class Runner: + def __init__(self, svc: Service) -> None: + self.svc = svc + + def start(self): + svc = self.svc + pvs = self.svc.pvs + + while not pvs.abort.get(): + if pvs.onoff.get() == 1: + # add callback for triggering the loop + if not svc.CallbackActive: + pvs.status.put("running") + pvs.CallbackPV.add_callback(callback=svc.InjectionCallback, index=1, svc=svc) + svc.CallbackActive = 1 + + if svc.ChangeTrue or perf_counter() > svc.tl + 0.1: + # start time for next loop firing, if no vacuum changes + svc.tl = perf_counter() + + # reload control PVs + svc.update() + + # check stop/abort conitions if injection is active + if svc.injection: + # ensure disabled scrubbing during injection + svc.pause_scrubbing() + + # ensure correct injector settings + if svc.cur > pvs.current_threshold.get(): + pvs.gun_grid.put(svc.grid_fpfb) + else: + pvs.gun_grid.put(svc.grid_accum) + + # abort injection when limits exceeded + if svc.conditions_abort_injection: + # disable injection when upper limit reached + svc.stop_injection() + print("injection aborted") + + if not svc.abort: + svc.abort = 1 + svc.abort_t0 = perf_counter() + + svc.set_status(svc.states["abort"], wait=60) + + elif svc.conditions_stop_injection: + # disable injection when upper limit reached + svc.stop_injection() + print("injection stopped") + + # start scrubbing if activated + if svc.scrubbi > 0: + sleep(0.1) + svc.start_scrubbing() + + svc.set_status(svc.states["decay + scrub"]) + + else: + svc.set_status(svc.states["decay - scrub"]) + + elif svc.info != svc.states["inject"]: + svc.set_status(svc.states["inject"]) + + # stop injection and info if master trigger missing + elif not svc.trig_master: + svc.stop_injection() + svc.set_status(svc.states["wait 4 master"]) + + # stop injection and info if master mode is not set to manual + elif not svc.master_mode == 2: + svc.stop_injection() + svc.set_status(svc.states["wait 4 manual"]) + + # stop injection and wait for 60s if injection was aborted + elif svc.abort: + svc.stop_injection() + dt = perf_counter() - svc.abort_t0 + + if dt > 60: + svc.abort = 0 + + else: + svc.set_status(svc.states["abort"], wait=60 - dt) + + sleep(0.1) + + # start warmup cycling if conditions for starting injection are fulfilled + elif svc.conditions_start_injection and svc.info not in [ + svc.states["inject"], + svc.states["warmup"], + ]: + svc.set_status(svc.states["warmup"]) + + # disable pingers for injection + svc.pause_scrubbing() + + # make sure that injection bump and pinger not both fired + sleep(0.1) + + # check setpoints for required amount of warmup cycles + svc.cycles_BOIN_sp = pvs.cycles_BOIN_sp.get() + svc.cycles_BOMB_sp = pvs.cycles_BOMB_sp.get() + svc.cycles_BOME_sp = pvs.cycles_BOME_sp.get() + svc.cycles_BOEX_sp = pvs.cycles_BOEX_sp.get() + svc.cycles_SRIN_sp = pvs.cycles_SRIN_sp.get() + + # set readback of warmup cycle counters to setpoint + pvs.cycles_BOIN_rb.put(svc.cycles_BOIN_sp) + pvs.cycles_BOMB_rb.put(svc.cycles_BOMB_sp) + pvs.cycles_BOME_rb.put(svc.cycles_BOME_sp) + pvs.cycles_BOEX_rb.put(svc.cycles_BOEX_sp) + pvs.cycles_SRIN_rb.put(svc.cycles_SRIN_sp) + svc.cycles_BOIN_rb = svc.cycles_BOIN_sp + svc.cycles_BOMB_rb = svc.cycles_BOMB_sp + svc.cycles_BOME_rb = svc.cycles_BOME_sp + svc.cycles_BOEX_rb = svc.cycles_BOEX_sp + svc.cycles_SRIN_rb = svc.cycles_SRIN_sp + + # calculate highest amount of total required warmup cycles + svc.cycles_max = np.nanmax( + [ + svc.cycles_BOIN_sp, + svc.cycles_BOMB_sp, + svc.cycles_BOME_sp, + svc.cycles_BOEX_sp, + svc.cycles_SRIN_sp, + ] + ) + + # calculate highest amount of remaining warmup cycles + svc.cycles_rem = svc.cycles_max + + # remove default callback for warmup (secretly since svc.CallbackActive = 0) + pvs.CallbackPV.remove_callback(index=1) + + # activate warmup callbacks + pvs.cycles_BOIN_cnt.add_callback( + callback=svc.WarmupCallback, index=2, svc=svc + ) + pvs.cycles_BOMB_cnt.add_callback( + callback=svc.WarmupCallback, index=3, svc=svc + ) + pvs.cycles_BOME_cnt.add_callback( + callback=svc.WarmupCallback, index=4, svc=svc + ) + pvs.cycles_BOEX_cnt.add_callback( + callback=svc.WarmupCallback, index=5, svc=svc + ) + pvs.cycles_SRIN_cnt.add_callback( + callback=svc.WarmupCallback, index=6, svc=svc + ) + # pvs.cycles_ALL_cnt.add_callback(callback=InjectionCallback, index=7, svc=svc) + + # set callback warmup cycle counters to 0 + svc.wait4warm = { + "AGETI-CVME-MASTER-TMA:Evt-BO-Inj-EVRCount-I.VAL": 0, + "AGETI-CVME-MASTER-TMA:Evt-BO-MA-B-EVRCount-I.VAL": 0, + "AGETI-CVME-MASTER-TMA:Evt-BO-Ramp-EVRCount-I.VAL": 0, + "AGETI-CVME-MASTER-TMA:Evt-BO-Ext-EVRCount-I.VAL": 0, + "AGETI-CVME-MASTER-TMA:Evt-SR-Inj-EVRCount-I.VAL": 0, + } + + svc.tp = perf_counter() + + # do top up analysis + svc.topup_dt = (svc.tp - svc.topup_t0) / 60 + svc.topup_dI = svc.topup_I0 - svc.cur + pvs.topup_dt.put(svc.topup_dt) + pvs.topup_dI.put(svc.topup_dI) + if svc.topup_I0 > 0: + if svc.topup_dI / svc.topup_I0 < 1: + pvs.topup_tau.put( + (-svc.topup_dt / 60) / np.log(1 - svc.topup_dI / svc.topup_I0) + ) + + elif svc.info == svc.states["warmup"]: + if svc.cycles_rem < 1: + # start injection + svc.start_injection() + + # reactivate default callback by notifying mainloop of its deactivation + svc.CallbackActive = 0 + + # disable warmup callbacks + pvs.cycles_BOIN_cnt.remove_callback(index=2) + pvs.cycles_BOMB_cnt.remove_callback(index=3) + pvs.cycles_BOME_cnt.remove_callback(index=4) + pvs.cycles_BOEX_cnt.remove_callback(index=5) + pvs.cycles_SRIN_cnt.remove_callback(index=6) + pvs.cycles_ALL_cnt.remove_callback(index=7) + + svc.set_status(svc.states["inject"]) + + else: + svc.prepare_injection() + + elif svc.info in [svc.states["pause"], svc.states["wait 4 master"]]: + if svc.injection: + svc.set_status(svc.states["inject"]) + else: + if pvs.trig_pinger_h.get() and pvs.trig_pinger_v.get(): + svc.set_status(svc.states["decay + scrub"]) + else: + svc.set_status(svc.states["decay - scrub"]) + + # stop scrubbing when deactivated + elif svc.scrubbi == 0 and svc.scrubbilast > 0: + svc.stop_scrubbing() + svc.set_status(svc.states["decay - scrub"]) + + # if scrubbing activated and lifetime drops below 1 hour --> stop scrubbing + elif svc.scrubbi > 0 and svc.tau < 1: + svc.pause_scrubbing() + svc.set_status(svc.states["decay - scrub"]) + print("Scrubbing aborted, tau < 1") + + svc.ChangeTrue = 0 + + # inform watchdog(s) + pvs.running.put(0) + + # wait for callback to fire + poll(evt=1.0e-5, iot=0.1) + + else: + # WHILE PAUSED REBOOT OF THE SCRUBBING IOC SHOULD NOT DISTURB THE SERVICE + + if svc.CallbackActive: + # remove callback + pvs.CallbackPV.remove_callback(index=1) + svc.CallbackActive = 0 + + # reload control PVs + svc.update() + + # disable injection and pingers when first paused + if svc.info != svc.states["pause"]: + svc.stop_injection() + svc.pause_scrubbing() + + svc.set_status(svc.states["pause"]) + pvs.status.put("paused") + + sleep(0.1) + + # sleep and inform watchdog(s) + pvs.alive.put(0) + + if pvs.abort.get(): + # confirm clean abort + pvs.status.put("killed") + print("Got Killed by Master Service!") + pvs.abortreq.put(0) + + # elif svc.crashed: + + # # confirm crash + # pvs.status.put('crashed') + # print('Service crashed on exception!') + # print('----- svc.exception.__traceback__ -----') + # print(svc.exception.__traceback__) + # print() + # print('----- traceback.format_exc() -----') + # print(traceback.format_exc()) + + else: + # confirm unknown exit + pvs.status.put("unknown crash") + print("Service crashed without exception!") + print(traceback.format_exc()) diff --git a/services/scrubbing/current/app/src/agebd_scrubbing/service.py b/services/scrubbing/current/app/src/agebd_scrubbing/service.py index 06d6f6d..3be0bc5 100644 --- a/services/scrubbing/current/app/src/agebd_scrubbing/service.py +++ b/services/scrubbing/current/app/src/agebd_scrubbing/service.py @@ -1,12 +1,12 @@ -import time +from datetime import datetime +from time import perf_counter, sleep -from epics import dbr +import numpy as np +from epics import PV -from agebd.pv import LocalPVLink -from agebd.service.base import BaseService +from agebd.pv import get_pv_class from agebd.service.pvs import BasePVs from agebd.utils import printgetversion -from agebd.pv import get_pv_class __version__ = printgetversion(__file__) @@ -14,80 +14,494 @@ PV = get_pv_class() class PVs(BasePVs): - # Option 1: - ## Service specific PVs from dedicated IOC - my_pv1 = PV("AGEBD-SCRUBBING:AO") # TODO: is this a good PV to use? + cycles_BOIN_sp = PV("AGEBD-SCRUBBING:WARMUPCYCLES-BOIN-SP.VAL") + cycles_BOIN_rb = PV("AGEBD-SCRUBBING:WARMUPCYCLES-BOIN-RB.VAL") + cycles_BOIN_cnt = PV("AGETI-CVME-MASTER-TMA:Evt-BO-Inj-EVRCount-I.VAL", auto_monitor=True) + cycles_BOMB_sp = PV("AGEBD-SCRUBBING:WARMUPCYCLES-BOMB-SP.VAL") + cycles_BOMB_rb = PV("AGEBD-SCRUBBING:WARMUPCYCLES-BOMB-RB.VAL") + cycles_BOMB_cnt = PV("AGETI-CVME-MASTER-TMA:Evt-BO-MA-B-EVRCount-I.VAL", auto_monitor=True) + cycles_BOME_sp = PV("AGEBD-SCRUBBING:WARMUPCYCLES-BOME-SP.VAL") + cycles_BOME_rb = PV("AGEBD-SCRUBBING:WARMUPCYCLES-BOME-RB.VAL") + cycles_BOME_cnt = PV("AGETI-CVME-MASTER-TMA:Evt-BO-Ramp-EVRCount-I.VAL", auto_monitor=True) + cycles_BOEX_sp = PV("AGEBD-SCRUBBING:WARMUPCYCLES-BOEX-SP.VAL") + cycles_BOEX_rb = PV("AGEBD-SCRUBBING:WARMUPCYCLES-BOEX-RB.VAL") + cycles_BOEX_cnt = PV("AGETI-CVME-MASTER-TMA:Evt-BO-Ext-EVRCount-I.VAL", auto_monitor=True) + cycles_SRIN_sp = PV("AGEBD-SCRUBBING:WARMUPCYCLES-SRIN-SP.VAL") + cycles_SRIN_rb = PV("AGEBD-SCRUBBING:WARMUPCYCLES-SRIN-RB.VAL") + cycles_SRIN_cnt = PV("AGETI-CVME-MASTER-TMA:Evt-SR-Inj-EVRCount-I.VAL", auto_monitor=True) + cycles_ALL_cnt = PV("AGETI-CVME-MASTER-TMA:Event-Seq-Cnt-I.VAL", auto_monitor=True) - ## Service specific PVs from other IOCs - # ... + ## Control PVs + # top-up control mode + # topumod = PV('.VAL') + # top-up mode vacuum limits + vac_max = PV("AGEBD-SCRUBBING:CONTROL-VACUUM-MAX.VAL") + vac_min = PV("AGEBD-SCRUBBING:CONTROL-VACUUM-MIN.VAL") + # top-up mode current limits + cur_max = PV("AGEBD-SCRUBBING:CONTROL-CURRENT-MAX.VAL") + cur_min = PV("AGEBD-SCRUBBING:CONTROL-CURRENT-MIN.VAL") + # top-up mode time limits for injection and decay (periodic refill) + tim_inj = PV("AGEBD-SCRUBBING:CONTROL-TIME-MAX.VAL") + tim_dec = PV("AGEBD-SCRUBBING:CONTROL-TIME-MIN.VAL") + # mbfb gain setpoints for scrubbing + mbfbx_gain_sp = PV("AGEBD-SCRUBBING:MBFBX-GAIN-SP.VAL") + mbfby_gain_sp = PV("AGEBD-SCRUBBING:MBFBY-GAIN-SP.VAL") + mbfbx_gain = PV("ARSGE-CECL-MBFB:MBFB-T-X-DDS-GAIN.VAL") + mbfby_gain = PV("ARSGE-CECL-MBFB:MBFB-T-Y-DDS-GAIN.VAL") + + # RDP Scan + # pause : set gain to 0 as normal, get current sweep freq, stop sweep, set initial freq of sweep to current freq + # start : set gain to sp (i.e., 100) as normal, start sweep + mbfby_freq_sp = PV("ARSGE-CECL-MBFB:MBFB-T-Y-DDS-FRQ.VAL") + mbfby_sweep_onoff = PV("ARSGE-CECL-MBFB:MBFB-T-Y-DDS-SWEEP-EN.VAL") + mbfby_sweepfreq_start = PV("ARSGE-CECL-MBFB:MBFB-T-Y-DDS-SWEEP-FREQ-START.VAL") + mbfby_sweepfreq_rb = PV("ARSGE-CECL-MBFB:MBFB-T-Y-DDS-SWEEP-FRQ-RB-REG.VAL") + + vac_dyn = PV("AGEBD-SCRUBBING:CONTROL-VACUUM-DYN.VAL") + vac_dyn_frac = PV("AGEBD-SCRUBBING:CONTROL-VACUUM-DYN-FRAC.VAL") + + # grd_max = PV('AGEBD-SCRUBBING:CONTROL-GUNGRID-MAX.VAL') + grid_accum = PV("AGEBD-SCRUBBING:CONTROL-GUNGRID-ACCUM.VAL") + grid_fpfb = PV("AGEBD-SCRUBBING:CONTROL-GUNGRID-FPFB.VAL") + # current above which the feedback mode is started + current_threshold = PV("AGEBD-TIMING:CURRENT-THRESHOLD.VAL") + + scrubbing = PV("AGEBD-SCRUBBING:CONTROL-SCRUB.VAL") + + # Input PVs + vacs = [ + # PV('ARIVA-VMAVE:PRESS-MAX.VAL'), # VCS gauges in the Arcs + Straights + # PV('ARIVA-VMAVE-FE:PRESS-MAX.VAL'), # VCS gauges in the ring to always the first one after the FE-Valve + # PV('ARIVA-VMAVE-ALL:PRESS-MAX.VAL'), # Max of the two above + # PV('ARIVA-VMAVE-PSH:PRESS-MAX.VAL'), # VCS gauges in the Photon Shutters/Beam Stoppers of the existing beamlines + PV("ARIVA-VMAVE-ALL:PRESS-MAX.VAL"), # Max of three signals: Ring/FE/PSH + PV("ARS07-VMCC-0120:PRESSURE.VAL"), + ] + vac_all = PV("ARIVA-VMAVE-ALL:PRESS-MAX.VAL") + vac_acc = [ + PV("ARIVA-VMAVE:PRESS-MAX.VAL"), + PV("ARS07-VMCC-0120:PRESSURE.VAL"), + ] + # vac_fe = PV('ARIVA-VMAVE-FE:PRESS-MAX.VAL') + # cur = PV('AGEBD-DBPM3CURR:CURRENT-AVG.VAL') + cur = PV("ARS07-DPCT-0000:CURR.VAL") + # cur1 = PV('ARS07-CECL-DPCT:BDC-DPCT100-SCALED') + # cur2 = PV('ARS07-CECL-DPCT2:BDC-DPCT200-SCALED') + # curuse = PV('ARS07-DPCT-0000:CHOICE-RB') + + injguard = PV("AGEBD-INJECTIONGUARD:CHECK-SUM.VAL") + + injactive = PV("AGEBD-PARAMS:INJECTION-ACTIVE") + + tau = PV("AGEBD-PARAMS:LIFETIME.VAL") + + # Regulated PVs + trig_firego = PV("AGEBD-TIMING:GO.VAL") + trig_master = PV("AGETI-CVME-MASTER-TMA:SR-Inj-Status-Sel.VAL") + master_mode = PV("AGETI-CVME-MASTER-TMA:FP-Mode-Sel.VAL") + trig_secret = PV("AGEBD-INJECTIONGUARD:STATUS-SECRET-MASTER.VAL") + # trig_guntxb = PV('ALIRF-VME-A-GUN:CH1-MODE.VAL') + # AGETI-CVME-MASTER-TMA:Evt-LI-Gun-Ena-Sel + # trig_gunevt = PV('AGETI-CVME-MASTER-TMA:Evt-LI-Gun-Ena-Sel.VAL') + # AGETI-CVME-MASTER-TMA:Evt-BO-Inj-Ena-Sel + trig_booinj = PV("AGETI-CVME-MASTER-TMA:Evt-BO-Inj-Ena-Sel.VAL") + # AGETI-CVME-MASTER-TMA:Evt-BO-MA-B-Ena-Sel + # trig_boormp = PV('AGETI-CVME-MASTER-TMA:Evt-Old-BO-HW-Ena-Sel.VAL') + trig_bormp1 = PV("AGETI-CVME-MASTER-TMA:Evt-BO-MA-B-Ena-Sel.VAL") + # AGETI-CVME-MASTER-TMA:Evt-BO-Ramp-Ena-Sel + trig_bormp2 = PV("AGETI-CVME-MASTER-TMA:Evt-BO-Ramp-Ena-Sel.VAL") + trig_booext = PV( + "AGETI-CVME-MASTER-TMA:Evt-BO-Ext-Ena-Sel.VAL" + ) # Alias AGETI-CVME-MASTER-TMA:Evt-10 + trig_rininj = PV( + "AGETI-CVME-MASTER-TMA:Evt-SR-Inj-Ena-Sel.VAL" + ) # Alias AGETI-CVME-MASTER-TMA:Evt-26 + trig_pinger_h = PV( + "AGETI-CVME-MASTER-TMA:Evt-SR-Pinger-H-Ena-Sel.VAL" + ) # Alias AGETI-CVME-MASTER-TMA:Evt-14-Ena-Sel + trig_pinger_v = PV( + "AGETI-CVME-MASTER-TMA:Evt-SR-Pinger-V-Ena-Sel.VAL" + ) # Alias AGETI-CVME-MASTER-TMA:Evt-14-Ena-Sel + + # # injection follow bucket workaround for linac (https://elog-gfa.psi.ch/SLS/64299) + # # Before making injection, disable the LLRF phase feedback ... + # linac_llrf_phaseFB1 = PV('ALIRF-A1-LLE-DSP:PHASE-FB-SWITCH.VAL') + # linac_llrf_phaseFB2 = PV('ALIRF-A2-LLE-DSP:PHASE-FB-SWITCH.VAL') + # # ... and disable the phase alarm by setting the phase limits + # linac_llrf_phaseFB_PBU_HIHI = PV('ALIRF-PBU:Ph-COMP.HIHI') + # linac_llrf_phaseFB_FBU_HIHI = PV('ALIRF-FBU:Ph-COMP.HIHI') + # linac_llrf_phaseFB_MOD1_HIHI = PV('ALIRF-AS-1:Ph-COMP.HIHI') + # linac_llrf_phaseFB_MOD2_HIHI = PV('ALIRF-AS-2:Ph-COMP.HIHI') + # linac_llrf_phaseFB_PBU_LOLO = PV('ALIRF-PBU:Ph-COMP.LOLO') + # linac_llrf_phaseFB_FBU_LOLO = PV('ALIRF-FBU:Ph-COMP.LOLO') + # linac_llrf_phaseFB_MOD1_LOLO = PV('ALIRF-AS-1:Ph-COMP.LOLO') + # linac_llrf_phaseFB_MOD2_LOLO = PV('ALIRF-AS-2:Ph-COMP.LOLO') + + gun_grid = PV("ALIRF-GUN-GRID:V-SET.VAL") + # pinger_v = PV('ARS01-MKIK-0320:CTRL-ON-OFF.VAL') + # pinger_h = PV('ARS01-MKIK-0290:CTRL-ON-OFF.VAL') + + topup_dt = PV("AGEBD-SCRUBBING:TOPUP-DELTAT.VAL") + topup_dI = PV("AGEBD-SCRUBBING:TOPUP-DELTAI.VAL") + topup_tau = PV("AGEBD-SCRUBBING:TOPUP-TAU.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-SCRUBBING: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-SCRUBBING:CALLBACK", auto_monitor=dbr.DBE_VALUE - ) # TODO: ok? + # Service Callback PVs + self.CallbackPV = PV("AGETI-CVME-MASTER-TMA:Evt-Old-BO-SYN.VALA", auto_monitor=True) -class Service(BaseService[PVs]): - def __init__( - self, - name: str, - pvs: PVs, - version: str = __version__, - sleep_interval: float = 0.1, - ): - super().__init__(name, pvs, version, sleep_interval) +class Service: + def __init__(self, pvs: PVs): + self.pvs = pvs - # TODO: What is this for? - self.path = "sls/bd/bin" + self.pvs.started.put(str(datetime.now())[:-7]) + self.pvs.version.put(__version__) + self.pvs.info.put("initialized") + + self.cycles_BOIN_sp = pvs.cycles_BOIN_sp.get() + self.cycles_BOIN_rb = pvs.cycles_BOIN_rb.get() + self.cycles_BOIN_cnt = pvs.cycles_BOIN_cnt.get() + self.cycles_BOMB_sp = pvs.cycles_BOMB_sp.get() + self.cycles_BOMB_rb = pvs.cycles_BOMB_rb.get() + self.cycles_BOMB_cnt = pvs.cycles_BOMB_cnt.get() + self.cycles_BOME_sp = pvs.cycles_BOME_sp.get() + self.cycles_BOME_rb = pvs.cycles_BOME_rb.get() + self.cycles_BOME_cnt = pvs.cycles_BOME_cnt.get() + self.cycles_BOEX_sp = pvs.cycles_BOEX_sp.get() + self.cycles_BOEX_rb = pvs.cycles_BOEX_rb.get() + self.cycles_BOEX_cnt = pvs.cycles_BOEX_cnt.get() + self.cycles_SRIN_sp = pvs.cycles_SRIN_sp.get() + self.cycles_SRIN_rb = pvs.cycles_SRIN_rb.get() + self.cycles_SRIN_cnt = pvs.cycles_SRIN_cnt.get() + self.timerthreadready = 1 + + self.cycles_max = np.nanmax( + [ + self.cycles_BOIN_sp, + self.cycles_BOMB_sp, + self.cycles_BOME_sp, + self.cycles_BOEX_sp, + self.cycles_SRIN_sp, + ] + ) + + self.cycles_rem = self.cycles_max + + self.wait4warm = { + "AGETI-CVME-MASTER-TMA:Evt-BO-Inj-EVRCount-I.VAL": 0, + "AGETI-CVME-MASTER-TMA:Evt-BO-MA-B-EVRCount-I.VAL": 0, + "AGETI-CVME-MASTER-TMA:Evt-BO-Ramp-EVRCount-I.VAL": 0, + "AGETI-CVME-MASTER-TMA:Evt-BO-Ext-EVRCount-I.VAL": 0, + "AGETI-CVME-MASTER-TMA:Evt-SR-Inj-EVRCount-I.VAL": 0, + } + + self.vac = np.max([pv.get() for pv in pvs.vacs]) + # self.vac_acc = pvs.vac_acc.get() + self.vac_acc = np.max([pv.get() for pv in pvs.vac_acc]) + # self.vac = pvs.vac_fe.get() + self.vac_array = np.repeat(self.vac_acc, 10) + self.vac_dyn = self.vac * pvs.vac_dyn_frac.get() + pvs.vac_dyn.put(self.vac_dyn * 1e9) + self.cur = 420 + self.scrubbi = pvs.scrubbing.get() + self.scrubbilast = self.scrubbi + + self.mbfbx_sp = int(pvs.mbfbx_gain_sp.get()) + self.mbfby_sp = int(pvs.mbfby_gain_sp.get()) + self.mbfby_sweepfreq_rb = np.round(pvs.mbfby_sweepfreq_rb.get()) + + self.topup_t0 = perf_counter() + self.topup_I0 = pvs.cur_min.get() + self.topup_dI = 0.0 + self.topup_dt = 0.0 + + def getcurr(self): + pvs = self.pvs + + self.cur = pvs.cur.get() + + # def start_linac_phasefeedback(self): + + # # injection follow bucket workaround for linac (https://elog-gfa.psi.ch/SLS/64299) + # # Before making injection, disable the LLRF phase alarms and feedback ... + # pvs.linac_llrf_phaseFB1.put(1) + # pvs.linac_llrf_phaseFB2.put(1) + + # pvs.linac_llrf_phaseFB_PBU_HIHI.put(10) + # pvs.linac_llrf_phaseFB_FBU_HIHI.put(10) + # pvs.linac_llrf_phaseFB_MOD1_HIHI.put(10) + # pvs.linac_llrf_phaseFB_MOD2_HIHI.put(10) + # pvs.linac_llrf_phaseFB_PBU_LOLO.put(-10) + # pvs.linac_llrf_phaseFB_FBU_LOLO.put(-10) + # pvs.linac_llrf_phaseFB_MOD1_LOLO.put(-10) + # pvs.linac_llrf_phaseFB_MOD2_LOLO.put(-10) + # print('phaseFB ON') def update(self): - # 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) - ####################################################################################### - ####################################################################################### + pvs = self.pvs - # callback done - self.CallbackFired = 0 + # (re-)load input PVs + self.injection = pvs.injactive.get() + self.vac = np.max([pv.get() for pv in pvs.vacs]) + self.vac_acc = np.max([pv.get() for pv in pvs.vac_acc]) - ####################################################################################### - ####################################################################################### - # here goes all the code that your service - # is supposed to be running in any case, e.g., + self.getcurr() - # 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") + self.trig_master = pvs.trig_master.get() and pvs.trig_secret.get() + self.master_mode = pvs.master_mode.get() + self.injguard = pvs.injguard.get() + self.info = pvs.info.get().split(",")[0] + self.tau = pvs.tau.get() - # get some pvs/vals - self.val1 = self.pvs.my_pv1.get() - self.val2 = self.pvs.my_pv2.get() + # (re-)load control PVs + self.vac_max = pvs.vac_max.get() * 1e-9 + self.vac_dyn = np.max(self.vac_array[:5]) * pvs.vac_dyn_frac.get() + pvs.vac_dyn.put(self.vac_dyn * 1e9) - # maybe sleep to limit the update loop execution rate - time.sleep(1) - ####################################################################################### - ####################################################################################### + # roll oldest value arr[0] to end, i.e., arr[-1] + self.vac_array = np.roll(self.vac_array, -2) + # replace oldest value with new value + self.vac_array[-2:] = self.vac_acc + + self.mbfbx_sp = int(pvs.mbfbx_gain_sp.get()) + self.mbfby_sp = int(pvs.mbfby_gain_sp.get()) + + self.vac_min = pvs.vac_min.get() * 1e-9 + self.cur_max = pvs.cur_max.get() + self.cur_min = pvs.cur_min.get() + self.grid_accum = pvs.grid_accum.get() + self.grid_fpfb = pvs.grid_fpfb.get() + self.scrubbilast = self.scrubbi + self.scrubbi = pvs.scrubbing.get() + + self.conditions_start_injection = ( # is any lower limit reached? + self.vac <= self.vac_min or self.cur <= self.cur_min + ) and ( # is no upper limit exceeded ? + self.cur <= self.cur_max and self.vac <= self.vac_max and self.vac_acc <= self.vac_dyn + ) + + self.conditions_stop_injection = ( + # is upper current limit reached/exceeded? + self.cur >= self.cur_max or self.vac >= self.vac_max + ) + + self.conditions_abort_injection = ( + # is any upper limit reached/exceeded? + self.vac_acc >= self.vac_dyn or self.cur > self.cur_max * 1.02 + # or self.injguard != 1 + ) + + def stop_injection(self): + pvs = self.pvs + + # disable super duper master fire trigger + pvs.trig_firego.put(0) + + # stop injection events + pvs.trig_booinj.put(0) + pvs.trig_bormp1.put(0) + pvs.trig_bormp2.put(0) + pvs.trig_booext.put(0) + pvs.trig_rininj.put(0) + + self.injection = 0 + + # prepare top-up analysis + self.topup_I0 = self.cur + self.topup_t0 = perf_counter() + + # # create thread that restarts phase feedback after 2 seconds + # if self.timerthreadready: + + # tt = threading.Timer(10, self.start_linac_phasefeedback) + # tt.start() + # print('threadtimer started....') + # self.timerthreadready = 0 + + def start_injection(self): + pvs = self.pvs + + # # injection follow bucket workaround for linac (https://elog-gfa.psi.ch/SLS/64299) + # # Before making injection, disable the LLRF phase alarms and feedback ... + # pvs.linac_llrf_phaseFB_PBU_HIHI.put(360) + # pvs.linac_llrf_phaseFB_FBU_HIHI.put(360) + # pvs.linac_llrf_phaseFB_MOD1_HIHI.put(360) + # pvs.linac_llrf_phaseFB_MOD2_HIHI.put(360) + # pvs.linac_llrf_phaseFB_PBU_LOLO.put(-360) + # pvs.linac_llrf_phaseFB_FBU_LOLO.put(-360) + # pvs.linac_llrf_phaseFB_MOD1_LOLO.put(-360) + # pvs.linac_llrf_phaseFB_MOD2_LOLO.put(-360) + # pvs.linac_llrf_phaseFB1.put(0) + # pvs.linac_llrf_phaseFB2.put(0) + # print('phaseFB OFF') + # self.timerthreadready = 1 + # sleep(0.5) + + # enable super duper master fire trigger + pvs.trig_firego.put(1) + + def prepare_injection(self): + pvs = self.pvs + + # ensure correct injector settings + if self.cur > pvs.current_threshold.get(): + pvs.gun_grid.put(self.grid_fpfb) + else: + pvs.gun_grid.put(self.grid_accum) + + self.cycles_BOIN_rb = ( + self.cycles_BOIN_sp - self.wait4warm["AGETI-CVME-MASTER-TMA:Evt-BO-Inj-EVRCount-I.VAL"] + ) + self.cycles_BOMB_rb = ( + self.cycles_BOMB_sp - self.wait4warm["AGETI-CVME-MASTER-TMA:Evt-BO-MA-B-EVRCount-I.VAL"] + ) + self.cycles_BOME_rb = ( + self.cycles_BOME_sp - self.wait4warm["AGETI-CVME-MASTER-TMA:Evt-BO-Ramp-EVRCount-I.VAL"] + ) + self.cycles_BOEX_rb = ( + self.cycles_BOEX_sp - self.wait4warm["AGETI-CVME-MASTER-TMA:Evt-BO-Ext-EVRCount-I.VAL"] + ) + self.cycles_SRIN_rb = ( + self.cycles_SRIN_sp - self.wait4warm["AGETI-CVME-MASTER-TMA:Evt-SR-Inj-EVRCount-I.VAL"] + ) + + self.cycles_rem = np.nanmax( + [ + self.cycles_BOIN_rb, + self.cycles_BOMB_rb, + self.cycles_BOME_rb, + self.cycles_BOEX_rb, + self.cycles_SRIN_rb, + ] + ) + + if self.cycles_rem == self.cycles_BOIN_sp: + pvs.trig_booinj.put(1) + if self.cycles_rem == self.cycles_BOMB_sp: + pvs.trig_bormp1.put(1) + if self.cycles_rem == self.cycles_BOME_sp: + pvs.trig_bormp2.put(1) + if self.cycles_rem == self.cycles_BOEX_sp: + pvs.trig_booext.put(1) + if self.cycles_rem == self.cycles_SRIN_sp: + pvs.trig_rininj.put(1) + + pvs.cycles_BOIN_rb.put(self.cycles_BOIN_rb) + pvs.cycles_BOMB_rb.put(self.cycles_BOMB_rb) + pvs.cycles_BOME_rb.put(self.cycles_BOME_rb) + pvs.cycles_BOEX_rb.put(self.cycles_BOEX_rb) + pvs.cycles_SRIN_rb.put(self.cycles_SRIN_rb) + + def start_scrubbing(self): + pvs = self.pvs + + if self.scrubbi == 1: + pvs.trig_pinger_v.put(1) + elif self.scrubbi == 2: + pvs.trig_pinger_h.put(1) + elif self.scrubbi == 3: + pvs.trig_pinger_v.put(1) + pvs.trig_pinger_h.put(1) + elif self.scrubbi == 4: + for i in range(self.mbfby_sp): + pvs.mbfby_gain.put(i + 1) + sleep(0.05) + elif self.scrubbi == 5: + for i in range(self.mbfbx_sp): + pvs.mbfbx_gain.put(i + 1) + sleep(0.05) + elif self.scrubbi == 6: + for i in range(self.mbfby_sp): + pvs.mbfby_gain.put(i + 1) + sleep(0.05) + for i in range(self.mbfbx_sp): + pvs.mbfbx_gain.put(i + 1) + sleep(0.05) + elif self.scrubbi == 7: + for i in range(self.mbfby_sp): + pvs.mbfby_gain.put(i + 1) + sleep(0.05) + pvs.mbfby_sweep_onoff.put(1) + + print("scrubbing started...") + + def stop_scrubbing(self): + pvs = self.pvs + + pvs.mbfbx_gain.put(0) + pvs.mbfby_gain.put(0) + pvs.trig_pinger_h.put(0) + pvs.trig_pinger_v.put(0) + + if self.scrubbi == 7: + self.mbfby_sweepfreq_rb = np.round(pvs.mbfby_sweepfreq_rb.get()) + pvs.mbfby_sweep_onoff.put(0) + pvs.mbfby_sweepfreq_start.put(self.mbfby_sweepfreq_rb) + pvs.mbfby_freq_sp.put(self.mbfby_sweepfreq_rb) + + def pause_scrubbing(self): + pvs = self.pvs + + if self.scrubbi > 0: + pvs.mbfbx_gain.put(0) + pvs.mbfby_gain.put(0) + pvs.trig_pinger_h.put(0) + pvs.trig_pinger_v.put(0) + + if self.scrubbi == 7: + self.mbfby_sweepfreq_rb = np.round(pvs.mbfby_sweepfreq_rb.get()) + pvs.mbfby_sweep_onoff.put(0) + pvs.mbfby_sweepfreq_start.put(self.mbfby_sweepfreq_rb) + pvs.mbfby_freq_sp.put(self.mbfby_sweepfreq_rb) + + def set_status(self, status, wait=0): + pvs = self.pvs + + if self.info != status: + self.info = status + + if wait == 0: + pvs.info.put(status) + + if wait > 0: + pvs.info.put(status + ", Wait {:.0f}s".format(wait)) + + ## define some "static" class attributes, i.e., persistent over every class call v = vals() + # trigger for execution of loop (controlled by PV callback) + ChangeTrue = 0 + # current state of callback + CallbackActive = 0 + # start time for next loop firing, if no vacuum changes + tl = perf_counter() + # waittime before injection after preparation + tp = perf_counter() + # status for GUI message + status = "Service Restarted" + # injection abort status + abort = 0 + # injection abort timer + abort_t0 = perf_counter() + # states of state machine + states = { + "abort": "Injection Abort", + "decay + scrub": "Decaying Beam, Scrub ON", + "decay - scrub": "Decaying Beam, Scrub OFF", + "inject": "Injecting", + "wait 4 master": "Waiting for Master Trigger", + "wait 4 manual": "Master Manual Mode Required", + "warmup": "Top Up Save Warmup", + "pause": "Service Paused", + } + + # define callback function to trigger loop execution on change of corresponding pv change + def InjectionCallback(self, pvname=None, value=None, char_value=None, **kw): + # trigger execution of loop with actual callback since execution of code here not reliable + self.ChangeTrue += 1 + + def WarmupCallback(self, pvname=None, value=None, char_value=None, **kw): + # trigger execution of loop with actual callback since execution of code here not reliable + self.wait4warm[pvname] += 1 + self.ChangeTrue += 1 diff --git a/services/scrubbing/current/app/tests/test_app.py b/services/scrubbing/current/app/tests/test_app.py index 4e3b1a8..693f7c8 100644 --- a/services/scrubbing/current/app/tests/test_app.py +++ b/services/scrubbing/current/app/tests/test_app.py @@ -4,6 +4,5 @@ from agebd_scrubbing import PVs, Service def test_app(): service_name = "DUMMY" pvs = PVs(service_name=service_name) - service = Service(name=service_name, pvs=pvs) - service.CallbackFired = 1 + service = Service(pvs=pvs) service.update() diff --git a/services/scrubbing/current/app/uv.lock b/services/scrubbing/current/app/uv.lock index c2400b2..457a790 100644 --- a/services/scrubbing/current/app/uv.lock +++ b/services/scrubbing/current/app/uv.lock @@ -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 = [