From 80d91d89f216fdb45105ea117dcd2e2a2ae95649 Mon Sep 17 00:00:00 2001 From: reiche Date: Wed, 22 Feb 2023 09:14:40 +0100 Subject: [PATCH 01/13] Initial Commit --- app/__init__.py | 1 + app/adaptiveorbit.py | 53 +++++++++++++++++++++++++++++++++++++++++ ext/__init__.py | 1 + ext/reichebscombined.py | 12 ++++++++++ util/__init__.py | 0 5 files changed, 67 insertions(+) create mode 100644 app/__init__.py create mode 100644 app/adaptiveorbit.py create mode 100644 ext/__init__.py create mode 100644 ext/reichebscombined.py create mode 100644 util/__init__.py diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..b2338f3 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1 @@ +from .adaptiveorbit import AdaptiveOrbit diff --git a/app/adaptiveorbit.py b/app/adaptiveorbit.py new file mode 100644 index 0000000..48ee0d7 --- /dev/null +++ b/app/adaptiveorbit.py @@ -0,0 +1,53 @@ +import time +import numpy as np + +from sfbd.ext.reichebscombined import ReicheBSCombined +from slic.core.adjustable import PVAdjustable + +class AdaptiveOrbit: + """ + Wrapper class to bundle all daq/io needed for adaptive orbit feedback. + """ + def __init__(self,beamline='Aramis'): + self.beamline=None + self.sensor = None + self.actuator = None + self.getBeamline(beamline) + + def read(self): + if not self.sensor or not self.beamline: + return None + data=self.sensor.get_current_value() + if not data: + return None + + retval={'pid':data['pid'], + 'Signal':data[self.channel0[0]], + 'X':np.array([data[i] for i in self.channelX]), + 'Y':np.array([data[i] for i in self.channelY])} + return retval + + + def read_adj(self): + if not self.actuator or not self.beamline: + return None + return [pv.get_current_value() for pv in self.actuator] + + + def getBeamline(self,beamline='Aramis'): + if beamline.upper() == 'ARAMIS': + self.beamline='Aramis' + self.channelX=['SARUN%2.2d-DBPM070:X1' % id for id in range(1,17)] + self.channelY=['SARUN%2.2d-DBPM070:Y1' % id for id in range(1,17)] + self.channel0 = ['SARFE10-PBIG050-EVR0:CALCI'] + FB_bpms = [x.replace(':X1',':X-REF-FB') for x in self.channelX] + [x.replace(':Y1',':Y-REF-FB') for x in self.channelY] + elif beamline.upper() == 'ATHOS': + self.beamline=None + else: + self.beamline=None + return + channels=self.channel0+self.channelX+self.channelY + self.sensor = ReicheBSCombined('AdapiveOrbit',channels) + self.actuator = [PVAdjustable(pv) for pv in FB_bpms] + + diff --git a/ext/__init__.py b/ext/__init__.py new file mode 100644 index 0000000..ae07e6b --- /dev/null +++ b/ext/__init__.py @@ -0,0 +1 @@ +from .reichebscombined import ReicheBSCombined diff --git a/ext/reichebscombined.py b/ext/reichebscombined.py new file mode 100644 index 0000000..1541705 --- /dev/null +++ b/ext/reichebscombined.py @@ -0,0 +1,12 @@ +from slic.core.sensor.bsmonitor import BSMonitor + +class ReicheBSCombined(BSMonitor): + + # Du brauchst kein extra init. BSMonitor tut schon das richtige... + + def _unpack(self, data): + # data ist ein dict mit allen Deinen Kanälen + pid = data["pid"] # der effektive Channel-Name der Pulse ID + # hier dein Code + # am Ende sollte eine Zahl rauskommen: + return data diff --git a/util/__init__.py b/util/__init__.py new file mode 100644 index 0000000..e69de29 -- 2.49.1 From cb3a2ba50161ed31f64cfdfe00a1d81d1746bbcd Mon Sep 17 00:00:00 2001 From: reiche Date: Wed, 22 Feb 2023 09:17:36 +0100 Subject: [PATCH 02/13] Added root level __init__py --- __init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 __init__.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 -- 2.49.1 From b82b023f339bdfa9e7582aad3f15dd04bd8fb71a Mon Sep 17 00:00:00 2001 From: Sven Reiche Date: Tue, 23 May 2023 16:39:27 +0200 Subject: [PATCH 03/13] Filling template for the interface module (loading, saving, elog etc) --- app/adaptiveorbit.py | 149 ++++++++++++++++++------ interface/__init__.py | 0 interface/elog.py | 25 ++++ interface/load.py | 74 ++++++++++++ interface/save.py | 150 ++++++++++++++++++++++++ interface/snap.py | 261 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 622 insertions(+), 37 deletions(-) create mode 100644 interface/__init__.py create mode 100644 interface/elog.py create mode 100644 interface/load.py create mode 100644 interface/save.py create mode 100644 interface/snap.py diff --git a/app/adaptiveorbit.py b/app/adaptiveorbit.py index 48ee0d7..5c78766 100644 --- a/app/adaptiveorbit.py +++ b/app/adaptiveorbit.py @@ -1,53 +1,128 @@ import time import numpy as np -from sfbd.ext.reichebscombined import ReicheBSCombined -from slic.core.adjustable import PVAdjustable +from bstrd import BSCache +from bstrd.bscache import make_channel_config, is_available +from epics import PV class AdaptiveOrbit: """ Wrapper class to bundle all daq/io needed for adaptive orbit feedback. """ - def __init__(self,beamline='Aramis'): - self.beamline=None - self.sensor = None - self.actuator = None - self.getBeamline(beamline) + def __init__(self): + + # Aramis Channels + self.ARch0 = 'SARFE10-PBIG050-EVR0:CALCI' + self.ARchx = ['SARUN%2.2d-DBPM070:X1' % id for id in range(1,17)] + self.ARchy = ['SARUN%2.2d-DBPM070:Y1' % id for id in range(1,17)] + self.bsAR = self.initBSStream([self.ARch0]+self.ARchx+self.ARchy) + self.pvAR = self.initPV(self.ARchx) + self.kickerAR = self.initPV(['SARMA02-MCRX050:I-SET','SARMA02-MCRY050:I-SET','SARUN02-MCRX080:I-SET','SARUN02-MCRY080:I-SET','SFB_ORBIT_SAR:ONOFF1']) + + # Athos Channels + self.ATch0 = 'SATFE10-PEPG046-EVR0:CALCI' + self.ATchx=[] + self.ATchy=[] + for bpm in range(5,23): + idx = '070' + if bpm == 5 or bpm ==14: + idx='410' + self.ATchx.append('SATUN%2.2d-DBPM%s:X1' % (bpm,idx)) + self.ATchy.append('SATUN%2.2d-DBPM%s:Y1' % (bpm,idx)) + self.bsAT = self.initBSStream([self.ATch0]+self.ATchx+self.ATchy) + self.pvAT = self.initPV(self.ATchx) + self.kickerAR = self.initPV(['SATMA01-MCRX610:I-SET','SATMA01-MCRY610:I-SET','SATUN05-MCRX420:I-SET','SATUN05-MCRY420:I-SET','SFB_ORBIT_SAT:ONOFF1']) + + # select first beamline + self.isAramis = True + + + def initBSStream(self,channels): + print("Initializing BSstream") + bs = BSCache() + bs.stop() + for cnl in channels[1:]: + if not is_available(cnl): + raise ValueError(f"BS-Channel {cbl} is not available") + res = make_channel_config(cnl,None,None) + bs.channels[res]=res + bs.get_var(channels[0]) # this starts also the stream into the cache + return bs + + def initPV(self,chx): + print("Initializing EPICS Channels") + pvs = [] + for x in chx: + pvs.append(PV(x.replace(':X1',':X-REF-FB'))) + pvs.append(PV(x.replace(':X1',':Y-REF-FB'))) + con = [pv.wait_for_connection(timeout=0.2) for pv in pvs] + for i, val in enumerate(con): + if val is False: + name = pv[i].pvname + raise ValueError(f"PV-Channel {name} is not available") + return pvs + + def terminate(self): + print('Stopping BSStream Thread...') + self.bsAR.stop() + self.bsAR.pt.running.clear() # for some reason I have to + self.bsAT.stop() + self.bsAT.pt.running.clear() # for some reason I have to + + def getBeamline(self,beamline): + if beamline == 'Aramis': + self.isAramis = True + else: + self.isAthos = True + + # all routine for accessing the machine (read/write) def read(self): - if not self.sensor or not self.beamline: - return None - data=self.sensor.get_current_value() - if not data: - return None - - retval={'pid':data['pid'], - 'Signal':data[self.channel0[0]], - 'X':np.array([data[i] for i in self.channelX]), - 'Y':np.array([data[i] for i in self.channelY])} - return retval + if self.isAramis: + data=self.bsAR.__next__() + return data['pid'],data[self.ARch0],np.array([data[cnl] for cnl in self.ARchx]),np.array([data[cnl] for cnl in self.ARchy]) + data=self.bsAT.__next__() + return data['pid'],data[self.ATch0],np.array([data[cnl] for cnl in self.ATchx]),np.array([data[cnl] for cnl in self.ATchy]) - def read_adj(self): - if not self.actuator or not self.beamline: - return None - return [pv.get_current_value() for pv in self.actuator] + def readPV(self): + if self.isAramis: + return [pv.value for pv in self.pvAR] + return [pv.value for pv in self.pvAT] - def getBeamline(self,beamline='Aramis'): - if beamline.upper() == 'ARAMIS': - self.beamline='Aramis' - self.channelX=['SARUN%2.2d-DBPM070:X1' % id for id in range(1,17)] - self.channelY=['SARUN%2.2d-DBPM070:Y1' % id for id in range(1,17)] - self.channel0 = ['SARFE10-PBIG050-EVR0:CALCI'] - FB_bpms = [x.replace(':X1',':X-REF-FB') for x in self.channelX] + [x.replace(':Y1',':Y-REF-FB') for x in self.channelY] - elif beamline.upper() == 'ATHOS': - self.beamline=None - else: - self.beamline=None + def setPV(self,fbval): + if self.isAramis: + for i in range(len(fbval)): + self.pvAR[i].value = fbval[i] + return + for i in range(len(fbval)): + self.pvAT[i].value = fbval[i] + + def getPVNames(self): + if self.isAramis: + return [pv.pvname for pv in self.pvAR] + return [pv.pvname for pv in self.pvAT] + + def getDetectorName(self): + if self.isAramis: + return self.ARch0 + return self.ATch0 + + def getKicker(self): + if self.isAramis: + return [pv.value for pv in self.kickerAR] + return [pv.value for pv in self.kickerAT] + + def setKicker(self,vals): + return + if self.isAramis: + for i,val in enumerate(vals): + self.kickerAR[i].value = val return - channels=self.channel0+self.channelX+self.channelY - self.sensor = ReicheBSCombined('AdapiveOrbit',channels) - self.actuator = [PVAdjustable(pv) for pv in FB_bpms] + for i,val in enumerate(vals): + self.kickerAT[i].value = val + - + + diff --git a/interface/__init__.py b/interface/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/interface/elog.py b/interface/elog.py new file mode 100644 index 0000000..b8660df --- /dev/null +++ b/interface/elog.py @@ -0,0 +1,25 @@ +import elog + +def write(text, Title = 'Test', Application = 'SFBD-Module', Attachment = None): + """ + Generates an entry in the electronic logbook of SwissFEL Commisisoning Data + :param text: The text to be placed in the log book + :param Title: Title of the log book entry + :param Application: Name of application which generates the log book entry + :param Attachment: List of attachments to be added to the log book (mostly plots) + :return: Message ID of log book entry + """ + + # supplemental info + Author = 'sfop' + Category = 'Measurement' # Info or Measurement + System = 'Beamdynamics' # Beamdynamics, Operation, Controls + + dict_att = {'Author': Author, 'Application': Application, 'Category': Category, 'Title': Title, 'System': System} + print('\nLog book entry generated') + + logbook = elog.open('https://elog-gfa.psi.ch/SwissFEL+commissioning+data/', user='robot', password='robot') + return logbook.post(text, attributes=dict_att, attachments=Attachment) + + + diff --git a/interface/load.py b/interface/load.py new file mode 100644 index 0000000..01d939f --- /dev/null +++ b/interface/load.py @@ -0,0 +1,74 @@ +import sys +import os +import datetime +import h5py + +# add other classes +#sys.path.append('/sf/bd/packages/SFBD/src') + +class Load: + def __init__(self, logger = None, program = 'SFBD', version = 'v1.0.0'): + + self.program = program + self.version = version + self.author ='S. Reiche' + self.file = None + + + if logger == None: + logging.basicConfig(level=logging.INFO, + format='%(levelname)-8s %(message)s') + self.logger = logging.getLogger(self.program) + self.logger.info('Load class started at %s' % datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + self.logger.info('Version: %s ' % self.version) + self.logger.info('Host: %s' % socket.gethostname()) + else: + self.logger = logger + + + def open(self,filename): + self.file = h5py.File(filename, "r") + + def close(self): + if self.file is not None: + self.file.close() + self.file = None + + def loadSnap(self): + snap={} + if not 'experiment' in self.file.keys(): + return snap + for key1 in self.file['experiment'].keys(): + for key2 in self.file['experiment'][key1].keys(): + val = self.file['experiment'][key1][key2][()] + snap[key1+':'+key2]={'val':val} + return snap + + def loadData(self,scanrun=1): + run='scan_%d' % scanrun + data = {} + for key1 in self.file[run]['data'].keys(): + for key2 in self.file[run]['data'][key1].keys(): + val = self.file[run]['data'][key1][key2][()] + data[key1+':'+key2]=val + return data + + def loadActuator(self,scanrun=1): + run='scan_%d' % scanrun + data = {} + if 'actuators' in self.file[run]['method'].keys(): + for key1 in self.file[run]['method']['actuators'].keys(): + for key2 in self.file[run]['method']['actuators'][key1].keys(): + val = self.file[run]['method']['actuators'][key1][key2][()] + data[key1+':'+key2]={'val':val} + return data + + + + + + + + + + diff --git a/interface/save.py b/interface/save.py new file mode 100644 index 0000000..9517fd0 --- /dev/null +++ b/interface/save.py @@ -0,0 +1,150 @@ +import sys +import os +import datetime +import h5py +import logging +import socket + +# add other classes +#sys.path.append('/sf/bd/packages/SFBD/src') + +class Save: + def __init__(self, logger = None, program = 'SFBD', version = 'v1.0.0'): + + self.program = program + self.version = version + self.author ='S. Reiche' + self.filename=None + self.file = None + + + if logger == None: + logging.basicConfig(level=logging.INFO, + format='%(levelname)-8s %(message)s') + self.logger = logging.getLogger(self.program) + self.logger.info('Save class started at %s' % datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + self.logger.info('Version: %s ' % self.version) + self.logger.info('Host: %s' % socket.gethostname()) + else: + self.logger = logger + + + + def open(self): + year = datetime.datetime.now().strftime('%Y') + month = datetime.datetime.now().strftime('%m') + day = datetime.datetime.now().strftime('%d') + + path = '/sf/data/measurements/%s' % year + if not os.path.exists(path): + os.makedirs(path) + path = '%s/%s' % (path,month) + if not os.path.exists(path): + os.makedirs(path) + path = '%s/%s' % (path,day) + if not os.path.exists(path): + os.makedirs(path) + datetag = datetime.datetime.now().strftime('%Y_%m_%d_%H_%M_%S_%f') + self.filename=('%s/%s_%s' % (path, self.program.replace(' ','_'), datetag)) + self.file = h5py.File(self.filename+'.h5', "w") + + # meta data header + dt = h5py.special_dtype(vlen=bytes) + dset=self.file.create_dataset('general/user',(1,),dtype=dt) + dset[0]=os.getlogin() + dset=self.file.create_dataset('general/application',(1,),dtype=dt) + dset[0]=self.program + dset=self.file.create_dataset('general/author',(1,),dtype=dt) + dset[0]=self.author + dset=self.file.create_dataset('general/version',(1,),dtype=dt) + dset[0]=self.version + dset=self.file.create_dataset('general/created',(1,),dtype=dt) + dset[0]=str(datetime.datetime.now()) + + + def close(self): + if self.file is not None: + self.file.close() + self.file = None + + + def writeSnap(self,val): + for key in val.keys(): + name=key.split(':') + if 'value' in val[key].keys(): + data=val[key]['value'] + elif 'val' in val[key].keys(): + data=val[key]['val'] + else: + continue + dset=self.file.create_dataset('experiment/%s/%s' % (name[0],name[1]),data=[data]) + dset.attrs['system']=self.getSystem(name[0]) + dset.attrs['units']='unknown' + + def writeAnalysis(self,data,scanrun=1): + for key1 in data.keys(): + for key2 in data[key1].keys(): + dset=self.file.create_dataset('scan_%d/analysis/%s/%s' % (scanrun, key1, key2), + data=data[key1][key2]) + + def writeData(self, data, scanrun=1): + if not 'Shot:ID' in data.keys(): + return + shape = data['Shot:ID'].shape + ndim = len(shape) + nsam = shape[-1] + nrec = 0 + if ndim > 1: + nrec = shape[:-1][0] + self.file.create_dataset("scan_%d/method/records" % scanrun,data=[nrec]) + self.file.create_dataset("scan_%d/method/samples" % scanrun,data=[nsam]) + self.file.create_dataset("scan_%d/method/dimension" % scanrun,data=[ndim]) + self.file.create_dataset("scan_%d/method/reducedData" % scanrun,data=[0]) # indicating that there is at least a 2D array for scalar data + # write the sensor raw value + for ele in data.keys(): + name=ele.split(':') + dset=self.file.create_dataset('scan_%d/data/%s/%s' % (scanrun, name[0], name[1]), data=data[ele]) + dset.attrs['system'] = self.getSystem(name[0]) + dset.attrs['units'] = 'unknown' + + + def writeActuator(self,act,scanrun=1): + dt = h5py.special_dtype(vlen=bytes) + dset=self.file.create_dataset("scan_%d/method/type" % scanrun,(1,),dtype=dt) + if act.isActuator: + dset[0]='Scan' + else: + dset[0]='Time Recording' + for ele in act.actuators.keys(): + name=ele.split(':') + dset=self.file.create_dataset("scan_%d/method/actuators/%s/%s" % (scanrun,name[0],name[1]),data=act.actuators[ele]['val']) + dset.attrs['system']=self.getSystem(name[0]) + dset.attrs['units']='unknown' + + + def getSystem(self,name): + if len(name) > 11: + tag=name[8:9] + fulltag=name[8:12] + else: + tag='' + fulltag='' + sys='Unknown' + if tag =='P': + sys='Photonics' + if tag =='D': + sys='Diagnostics' + if fulltag =='DSCR': + sys='Camera' + if tag == 'R': + sys='RF' + if tag == 'M': + sys='Magnets' + if tag == 'U': + sys='Undulator' + return sys + + + + + diff --git a/interface/snap.py b/interface/snap.py new file mode 100644 index 0000000..4bbe1b6 --- /dev/null +++ b/interface/snap.py @@ -0,0 +1,261 @@ +import datetime +import re +import numpy as np +import yaml +import os +import json + +import copy + +from threading import Thread + +import epics + +class Snap: + def __init__(self,filename='/sf/data/applications/snapshot/req/op/SF_settings.yaml'): + + self.pvs = self.parseYAML(filename) + self.doAbort = False + + def my_caget_many(self): + # this skips quite some channels - don't know why? + + pvdata = {} + pvchids = [] + # create, don't connect or create callbacks + for name in self.pvs: + chid = epics.ca.create_channel(name, connect=False, auto_cb=False) # note 1 + pvchids.append(chid) + + # connect + for chid in pvchids: + print(epics.ca.name(chid)) + epics.ca.connect_channel(chid) + + # request get, but do not wait for result + epics.ca.poll() + for chid in pvchids: + print(epics.ca.name(chid)) + epics.ca.get(chid, wait=False) # note 2 + + # now wait for get() to complete + epics.ca.poll() + for chid in pvchids: + print(epics.ca.name(chid)) + val = epics.ca.get_complete(chid,timeout=0.5) + if not val: + pvdata[epics.ca.name(chid)] = np.array([val]) + epics.ca.clear_cache() + return pvdata + + def abort(self): + self.doAbort=True + print('Aborting Snap') + + def getSnapValues(self,force=True): + self.doAbort=False + ret={} + if self.pvs: +# ret=self.my_caget_many() + val = epics.caget_many(self.pvs) + for i,pv in enumerate(self.pvs): + if val[i]: # filter out None values + ret[pv]={'val':float(val[i])} + epics.ca.clear_cache() + return ret,{} + + #------------- + # routines to parse the OP YAML file + + def applyMacro(self,pvs_in,macros): + pvs = [] + for macro in macros: + for key in macro: + tag='$('+key+')' + for pv in pvs_in: + if tag in pv: + pvs.append(pv.replace(tag,macro[key])) + for pv in pvs_in: # copy the ones without macro + if not '$(' in pv: + pvs.append(pv) + return pvs + + def parseYAML(self,filename='/sf/data/applications/snapshot/req/op/SF_settings.yaml'): + pvs = [] + path = os.path.dirname(filename) + with open(filename) as f: + try: + content = yaml.load(f, Loader=yaml.SafeLoader) + if 'include' in content.keys(): + if len(content['include']) > 0: + for cont in content['include']: + retpv = self.parseYAML(path+'/'+cont['name']) + if 'macros' in cont.keys(): + retpv = self.applyMacro(retpv,cont['macros']) + pvs = pvs + retpv + if 'pvs' in content.keys(): + if 'list' in content['pvs']: + for pv in content['pvs']['list']: + pvs.append(pv['name']) + return pvs + return None + + except yaml.YAMLError as e: + print(e) + return None + return None + + + + +class Old: + + def __init__(self): + self.filename = filename + print('Estbalishing snapshot with request file:', filename,flush=True) + self.savepath = savepath + self.tolerance = 0.0005 + self.pvnames = [] + self.pvs = [] + self.mppvnames = [] + self.mppvs = [] + self.machinepar = [] + self.message = '' + if self.filename: + self.openRequestFile(self.filename) + + def openRequestFile(self, filename): + self.filename = filename + self.rootname = self.filename.split('/')[-1] + + isReq = True + if '.yaml' in filename: + isReq = False +# req_file = SnapshotReqFile(path=str(self.filename)) + + if newVersion: + if '.yaml' in filename: + req_file = SnapshotJsonFile(path=str(self.filename)) + else: + req_file = SnapshotReqFile(path=str(self.filename)) + pvs_list= req_file.read()[0] + print('PV List:-------------------------') + for i in range(len(pvs_list)): + print(pvs_list[i]) + print(req_file.read()[1]) + else: + if '.yaml' in filename: + self.filename=None + self.rootname = None + print('YAML files not supported') + return + req_file = SnapshotReqFile(str(self.filename)) + pvs_list = req_file.read() + + + self.pvnames.clear() + self.machinepar.clear() + for ele in pvs_list: + if isinstance(ele, list): + self.pvnames = ele + elif isinstance(ele, dict): + if 'machine_params' in ele.keys(): + self.machinepar = ele['machine_params'] + Thread(target=self.connectPVs).start() + + def connectPVs(self): + self.pvs = [PV(pv, auto_monitor=False) for pv in self.pvnames] + con = [pv.wait_for_connection(timeout=0.2) for pv in self.pvs] + pvscon=[] + for i, val in enumerate(con): + if val is False: + print('Cannot connect to PV:', self.pvs[i].pvname,flush=True) + else: + pvscon.append(self.pvs[i]) + self.pvs = copy.deepcopy(pvscon) + if isinstance(self.machinepar,list): + self.mppvs = [PV(self.machinepar[key], auto_monitor=False) for key in self.machinepar] + else: + self.mppvs = [PV(self.machinepar[key], auto_monitor=False) for key in self.machinepar.keys()] + con = [pv.wait_for_connection(timeout=0.2) for pv in self.mppvs] + pvscon.clear() + for i, val in enumerate(con): + if val is False: + print('Cannot connect to mPV:', self.mppvs[i].pvname,flush=True) + else: + pvscon.append(self.mppvs[i]) + self.mppvs=copy.deepcopy(pvscon) + + def getSnapValues(self, force=True): + values = {} + val = [pv.get(timeout=0.6, use_monitor=False) for pv in self.pvs] + for i, pv in enumerate(self.pvs): + if val[i] is None: + if force: + continue + else: + return False + else: + values[pv.pvname] = { + "raw_name": pv.pvname, "val": val[i], "EGU": pv.units, "prec": pv.precision} + mvalues = {} + val = [pv.get(timeout=0.6, use_monitor=False) for pv in self.mppvs] + for i, pv in enumerate(self.mppvs): + if val[i] is None: + if force: + continue + else: + return False + else: + mvalues[pv.pvname] = {"value": val[i], + "units": pv.units, "precision": pv.precision} + return values, mvalues + + def save(self, labels=[], comment="Generated by SFBD-Package", force=True): + if self.filename is None: + self.message = 'No Request File Loaded' + return False + + val, mval = self.getSnapValues(force) + if isinstance(val, bool) and val == False: + self.message = 'Unsuccesful reading of PV channels (unforced access)' + return False + + # construct file name + datetag = datetime.datetime.now().strftime('%Y%m%d_%H%M%S') + root = self.rootname.split('.req')[0] + files = self.savepath+root+'_'+datetag+'.snap' + filel = self.savepath+root+'_latest.snap' + + # reshuffle from mval to keyword based machine values + mmval = {} + for key in self.machinepar.keys(): + if self.machinepar[key] in mval.keys(): + mmval[key] = mval[self.machinepar[key]] + # save file + parse_to_save_file( + val, files, macros=None, symlink_path=filel, comment=comment, + labels=[], + req_file_name=self.rootname, machine_params=mmval) + self.message = 'Snapshot saved to '+files + return True + + def restore(self,filename,refilter='',force=True): + filepath=self.savepath+filename + prog=re.compile(refilter) + save_pvs=parse_from_save_file(filepath) + res={} + for ele in save_pvs: + if isinstance(ele,dict): + for key in ele.keys(): + if prog.match(key): + res[key]=ele[key]['value'] + + for pv in self.pvs: + if pv.pvname in res.keys(): + val=pv.get() + if val is None or np.abs(val-res[pv.pvname]) > self.tolerance: + pv.put(res[pv.pvname]) + self.message ='Snap restored' + + -- 2.49.1 From f22a17852c3502f99f5d2c0a81c06c1fe20105f5 Mon Sep 17 00:00:00 2001 From: Sven Reiche Date: Wed, 31 May 2023 11:57:13 +0200 Subject: [PATCH 04/13] Added support for adaptive orbit feedback and spectral analysis --- app/__init__.py | 1 + app/adaptiveorbit.py | 8 +- app/spectralanalysis.py | 51 +++++++ interface/__init__.py | 4 + interface/elog.py | 6 +- interface/load.py | 96 ++++++------- interface/save.py | 298 +++++++++++++++++++++------------------ interface/snap.py | 300 ++++++++-------------------------------- 8 files changed, 327 insertions(+), 437 deletions(-) create mode 100644 app/spectralanalysis.py diff --git a/app/__init__.py b/app/__init__.py index b2338f3..f986faa 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1 +1,2 @@ from .adaptiveorbit import AdaptiveOrbit +from .spectralanalysis import SpectralAnalysis diff --git a/app/adaptiveorbit.py b/app/adaptiveorbit.py index 5c78766..286d4b6 100644 --- a/app/adaptiveorbit.py +++ b/app/adaptiveorbit.py @@ -61,7 +61,13 @@ class AdaptiveOrbit: name = pv[i].pvname raise ValueError(f"PV-Channel {name} is not available") return pvs - + + def flush(self): + with self.bsAR.pt.queue.mutex: + self.bsAR.pt.queue.queue.clear() + with self.bsAT.pt.queue.mutex: + self.bsAT.pt.queue.queue.clear() + def terminate(self): print('Stopping BSStream Thread...') self.bsAR.stop() diff --git a/app/spectralanalysis.py b/app/spectralanalysis.py new file mode 100644 index 0000000..7d16478 --- /dev/null +++ b/app/spectralanalysis.py @@ -0,0 +1,51 @@ +import time +import numpy as np + +from bstrd import BSCache +from bstrd.bscache import make_channel_config, is_available +from epics import PV + +class SpectralAnalysis: + """ + Wrapper class to bundle all daq/io needed for adaptive orbit feedback. + """ + def __init__(self): + + self.bs = BSCache() + self.bs.stop() + + self.channel = '' + self.channels = ['SARFE10-PSSS059:SPECTRUM_Y', + 'SATOP21-PMOS127-2D:SPECTRUM_Y', + 'SATOP31-PMOS132-2D:SPECTRUM_Y'] + self.isConnected = False + + def connect(self,ich): + if ich < 0 or ich >= len(self.channels): + return False + self.channel = self.channels[ich] + print('Connecting to BS-Channel:',self.channel) + self.bs.channels.clear() + self.bs.get_var(self.channel) # this starts the stream into the cache + self.pv = PV(self.channel.replace('_Y','_X')) + + def terminate(self): + print('Stopping BSStream Thread...') + self.bs.stop() + self.bs.pt.running.clear() # for some reason I have to + + def flush(self): + with self.bs.pt.queue.mutex: + self.bs.pt.queue.queue.clear() + + def read(self): + data=self.bs.__next__() + return data['pid'],data[self.channel] + + def readPV(self): + return self.pv.value + + def getSpectrometerName(self): + return self.channel + + diff --git a/interface/__init__.py b/interface/__init__.py index e69de29..a439ca0 100644 --- a/interface/__init__.py +++ b/interface/__init__.py @@ -0,0 +1,4 @@ +from .snap import getSnap +from .save import saveDataset +from .load import loadDataset +from .elog import writeElog diff --git a/interface/elog.py b/interface/elog.py index b8660df..7dbd84c 100644 --- a/interface/elog.py +++ b/interface/elog.py @@ -1,6 +1,7 @@ +import os import elog -def write(text, Title = 'Test', Application = 'SFBD-Module', Attachment = None): +def writeElog(text, Title = 'Test', Application = 'SFBD-Module', Attachment = None): """ Generates an entry in the electronic logbook of SwissFEL Commisisoning Data :param text: The text to be placed in the log book @@ -11,12 +12,11 @@ def write(text, Title = 'Test', Application = 'SFBD-Module', Attachment = None): """ # supplemental info - Author = 'sfop' + Author = os.getlogin() Category = 'Measurement' # Info or Measurement System = 'Beamdynamics' # Beamdynamics, Operation, Controls dict_att = {'Author': Author, 'Application': Application, 'Category': Category, 'Title': Title, 'System': System} - print('\nLog book entry generated') logbook = elog.open('https://elog-gfa.psi.ch/SwissFEL+commissioning+data/', user='robot', password='robot') return logbook.post(text, attributes=dict_att, attachments=Attachment) diff --git a/interface/load.py b/interface/load.py index 01d939f..79c0601 100644 --- a/interface/load.py +++ b/interface/load.py @@ -1,67 +1,51 @@ -import sys -import os -import datetime import h5py -# add other classes -#sys.path.append('/sf/bd/packages/SFBD/src') -class Load: - def __init__(self, logger = None, program = 'SFBD', version = 'v1.0.0'): - - self.program = program - self.version = version - self.author ='S. Reiche' - self.file = None - - - if logger == None: - logging.basicConfig(level=logging.INFO, - format='%(levelname)-8s %(message)s') - self.logger = logging.getLogger(self.program) - self.logger.info('Load class started at %s' % datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')) - self.logger.info('Version: %s ' % self.version) - self.logger.info('Host: %s' % socket.gethostname()) - else: - self.logger = logger - - - def open(self,filename): - self.file = h5py.File(filename, "r") - - def close(self): - if self.file is not None: - self.file.close() - self.file = None +def loadDataset(filename): + hid = h5py.File(filename, "r") + snap = loadSnap(hid) + data = loadData(hid) + act = loadActuator(hid) + hid.close() + return data,act,snap - def loadSnap(self): - snap={} - if not 'experiment' in self.file.keys(): - return snap - for key1 in self.file['experiment'].keys(): - for key2 in self.file['experiment'][key1].keys(): - val = self.file['experiment'][key1][key2][()] - snap[key1+':'+key2]={'val':val} - return snap +def loadSnap(hid): + snap={} + if not 'experiment' in hid.keys(): + return None + for key1 in hid['experiment'].keys(): + if isinstance(hid['experiment'][key1],h5py.Group): + for key2 in hid['experiment'][key1].keys(): + val = hid['experiment'][key1][key2][()] + snap[key1+':'+key2]=val + else: + snap[key1]=hid['experiment'][key1][()] + return snap - def loadData(self,scanrun=1): - run='scan_%d' % scanrun - data = {} - for key1 in self.file[run]['data'].keys(): - for key2 in self.file[run]['data'][key1].keys(): - val = self.file[run]['data'][key1][key2][()] +def loadData(hid,scanrun=1): + run='scan_%d' % scanrun + data = {} + for key1 in hid[run]['data'].keys(): + if isinstance(hid[run]['data'][key1],h5py.Group): + for key2 in hid[run]['data'][key1].keys(): + val = hid[run]['data'][key1][key2][()] data[key1+':'+key2]=val - return data + else: + data[key1]=hid[run]['data'][key1][()] + return data - def loadActuator(self,scanrun=1): - run='scan_%d' % scanrun - data = {} - if 'actuators' in self.file[run]['method'].keys(): - for key1 in self.file[run]['method']['actuators'].keys(): - for key2 in self.file[run]['method']['actuators'][key1].keys(): - val = self.file[run]['method']['actuators'][key1][key2][()] +def loadActuator(hid,scanrun=1): + run='scan_%d' % scanrun + data = {} + if 'actuators' in hid[run]['method'].keys(): + for key1 in hid[run]['method']['actuators'].keys(): + if isinstance(hid[run]['method']['actuators'],h5py.Group): + for key2 in hid[run]['method']['actuators'][key1].keys(): + val = hid[run]['method']['actuators'][key1][key2][()] data[key1+':'+key2]={'val':val} - return data + else: + data[key1]=hid[run]['method']['actuators'][key1][()] + return data diff --git a/interface/save.py b/interface/save.py index 9517fd0..648a146 100644 --- a/interface/save.py +++ b/interface/save.py @@ -2,147 +2,181 @@ import sys import os import datetime import h5py -import logging import socket +from PIL import Image -# add other classes -#sys.path.append('/sf/bd/packages/SFBD/src') +def getDatasetFileName(program='Unknown'): + year = datetime.datetime.now().strftime('%Y') + month = datetime.datetime.now().strftime('%m') + day = datetime.datetime.now().strftime('%d') -class Save: - def __init__(self, logger = None, program = 'SFBD', version = 'v1.0.0'): + path = '/sf/data/measurements/%s' % year + if not os.path.exists(path): + os.makedirs(path) + path = '%s/%s' % (path,month) + if not os.path.exists(path): + os.makedirs(path) + path = '%s/%s' % (path,day) + if not os.path.exists(path): + os.makedirs(path) + datetag = datetime.datetime.now().strftime('%Y_%m_%d_%H_%M_%S_%f') + filename=('%s/%s_%s' % (path, program.replace(' ','_'), datetag)) + return filename - self.program = program - self.version = version - self.author ='S. Reiche' - self.filename=None - self.file = None - - - if logger == None: - logging.basicConfig(level=logging.INFO, - format='%(levelname)-8s %(message)s') - self.logger = logging.getLogger(self.program) - self.logger.info('Save class started at %s' % datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')) - self.logger.info('Version: %s ' % self.version) - self.logger.info('Host: %s' % socket.gethostname()) - else: - self.logger = logger +def saveDataset(program,data,actuator=None,snap=None,analysis=None,figures=None): + hid,filename = openDataset(program) + if not hid: + return None + # check if scan is multiple instances of a scan + if isinstance(data,list): + for iscan,singledata in enumerate(data): + writeData(hid,singledata,iscan) + else: + writeData(hid,data,1) + # same for actuator + if isinstance(actuator,list): + for iscan,singleactuator in enumerate(actuator): + writeActuator(hid,singleactuator,iscan) + else: + writeActuator(hid,actuator,1) + # and same for analysis + if isinstance(analysis,list): + for iscan,singleana in enumerate(analysis): + writeAnalysis(hid,singleana,iscan) + else: + writeAnalysis(hid,analysis,1) + # write aux data + writeSnap(hid,snap) + hid.close() - - def open(self): - year = datetime.datetime.now().strftime('%Y') - month = datetime.datetime.now().strftime('%m') - day = datetime.datetime.now().strftime('%d') - - path = '/sf/data/measurements/%s' % year - if not os.path.exists(path): - os.makedirs(path) - path = '%s/%s' % (path,month) - if not os.path.exists(path): - os.makedirs(path) - path = '%s/%s' % (path,day) - if not os.path.exists(path): - os.makedirs(path) - datetag = datetime.datetime.now().strftime('%Y_%m_%d_%H_%M_%S_%f') - self.filename=('%s/%s_%s' % (path, self.program.replace(' ','_'), datetag)) - self.file = h5py.File(self.filename+'.h5', "w") - - # meta data header - dt = h5py.special_dtype(vlen=bytes) - dset=self.file.create_dataset('general/user',(1,),dtype=dt) - dset[0]=os.getlogin() - dset=self.file.create_dataset('general/application',(1,),dtype=dt) - dset[0]=self.program - dset=self.file.create_dataset('general/author',(1,),dtype=dt) - dset[0]=self.author - dset=self.file.create_dataset('general/version',(1,),dtype=dt) - dset[0]=self.version - dset=self.file.create_dataset('general/created',(1,),dtype=dt) - dset[0]=str(datetime.datetime.now()) + writeFigure(filename,figures) + return filename - def close(self): - if self.file is not None: - self.file.close() - self.file = None +def openDataset(program): + if isinstance(program,str): + program={'Name':program,'Author':'Unknown','Version':'Unknown'} + if not isinstance(program,dict): + return None,None + if not 'Author' in program.keys(): + program['Author']='Unknown' + if not 'Version' in program.keys(): + program['Version']='Unknown' + filename=getDatasetFileName(program['Name']) + hid= h5py.File(filename+'.h5', "w") + + # meta data header + dt = h5py.special_dtype(vlen=bytes) + dset=hid.create_dataset('general/user',(1,),dtype=dt) + dset[0]=os.getlogin() + dset=hid.create_dataset('general/application',(1,),dtype=dt) + dset[0]=program['Name'] + dset=hid.create_dataset('general/author',(1,),dtype=dt) + dset[0]=program['Author'] + dset=hid.create_dataset('general/version',(1,),dtype=dt) + dset[0]=program['Version'] + dset=hid.create_dataset('general/created',(1,),dtype=dt) + dset[0]=str(datetime.datetime.now()) + return hid,filename + + + +def writeData(hid, data, scanrun=1): + if not 'pid' in data.keys(): + return + shape = data['pid'].shape + ndim = len(shape) + nsam = shape[-1] + nrec = 0 + if ndim > 1: + nrec = shape[:-1][0] + hid.create_dataset("scan_%d/method/records" % scanrun,data=[nrec]) + hid.create_dataset("scan_%d/method/samples" % scanrun,data=[nsam]) + hid.create_dataset("scan_%d/method/dimension" % scanrun,data=[ndim]) + hid.create_dataset("scan_%d/method/reducedData" % scanrun,data=[0]) # indicating that there is at least a 2D array for scalar data + # write the sensor raw value + for ele in data.keys(): + name=ele.split(':') + if len(name)>1: + dset=hid.create_dataset('scan_%d/data/%s/%s' % (scanrun, name[0], name[1]), data=data[ele]) + else: + dset=hid.create_dataset('scan_%d/data/%s' % (scanrun, name[0]), data=data[ele]) + dset.attrs['system'] = getDatasetSystem(name[0]) + dset.attrs['units'] = 'unknown' + +def writeActuator(hid,act,scanrun=1): + if not act: + return + dt = h5py.special_dtype(vlen=bytes) + dset=hid.create_dataset("scan_%d/method/type" % scanrun,(1,),dtype=dt) + nact = len(act.keys()) + if nact>0: + dset[0]='Scan' + else: + dset[0]='Time Recording' + for ele in act.keys(): + name=ele.split(':') + if len(name)>1: + dset=hid.create_dataset("scan_%d/method/actuators/%s/%s" % (scanrun,name[0],name[1]),data=act[ele]) + else: + dset=hid.create_dataset("scan_%d/method/actuators/%s" % (scanrun,name[0]),data=act[ele]) + dset.attrs['system']=getDatasetSystem(name[0]) + dset.attrs['units']='unknown' + +def writeSnap(hid,val): + if not val: + return + for key in val.keys(): + name=key.split(':') + if len(name)>1: + dset=hid.create_dataset('experiment/%s/%s' % (name[0],name[1]),data=val[key]) + else: + dset=hid.create_dataset('experiment/%s/%s' % (name[0]),data=val[key]) + dset.attrs['system']=getDatasetSystem(name[0]) + dset.attrs['units']='unknown' + + +def writeAnalysis(hid,data,scanrun=1): + if not data: + return + for key in data.keys(): + name=key.split(':') + if len(name)>1: + dset=hid.create_dataset('scan_%d/analysis/%s/%s' % (scanrun, name[0], name[1]), data=data[key]) + else: + dset=hid.create_dataset('scan_%d/analysis/%s/%s' % (scanrun, name[0]), data=data[key]) + dset.attrs['system']='analysis' + dset.attrs['units']='unknown' + +def writeFigure(filename,figs): + for i,ele in enumerate(figs): + plotname='%s_Fig%d.png' % (filename,(i+1)) + im = Image.open(ele) + im.save(plotname) + return None - - def writeSnap(self,val): - for key in val.keys(): - name=key.split(':') - if 'value' in val[key].keys(): - data=val[key]['value'] - elif 'val' in val[key].keys(): - data=val[key]['val'] - else: - continue - dset=self.file.create_dataset('experiment/%s/%s' % (name[0],name[1]),data=[data]) - dset.attrs['system']=self.getSystem(name[0]) - dset.attrs['units']='unknown' - - def writeAnalysis(self,data,scanrun=1): - for key1 in data.keys(): - for key2 in data[key1].keys(): - dset=self.file.create_dataset('scan_%d/analysis/%s/%s' % (scanrun, key1, key2), - data=data[key1][key2]) - - def writeData(self, data, scanrun=1): - if not 'Shot:ID' in data.keys(): - return - shape = data['Shot:ID'].shape - ndim = len(shape) - nsam = shape[-1] - nrec = 0 - if ndim > 1: - nrec = shape[:-1][0] - self.file.create_dataset("scan_%d/method/records" % scanrun,data=[nrec]) - self.file.create_dataset("scan_%d/method/samples" % scanrun,data=[nsam]) - self.file.create_dataset("scan_%d/method/dimension" % scanrun,data=[ndim]) - self.file.create_dataset("scan_%d/method/reducedData" % scanrun,data=[0]) # indicating that there is at least a 2D array for scalar data - # write the sensor raw value - for ele in data.keys(): - name=ele.split(':') - dset=self.file.create_dataset('scan_%d/data/%s/%s' % (scanrun, name[0], name[1]), data=data[ele]) - dset.attrs['system'] = self.getSystem(name[0]) - dset.attrs['units'] = 'unknown' - - - def writeActuator(self,act,scanrun=1): - dt = h5py.special_dtype(vlen=bytes) - dset=self.file.create_dataset("scan_%d/method/type" % scanrun,(1,),dtype=dt) - if act.isActuator: - dset[0]='Scan' - else: - dset[0]='Time Recording' - for ele in act.actuators.keys(): - name=ele.split(':') - dset=self.file.create_dataset("scan_%d/method/actuators/%s/%s" % (scanrun,name[0],name[1]),data=act.actuators[ele]['val']) - dset.attrs['system']=self.getSystem(name[0]) - dset.attrs['units']='unknown' - - - def getSystem(self,name): - if len(name) > 11: - tag=name[8:9] - fulltag=name[8:12] - else: - tag='' - fulltag='' - sys='Unknown' - if tag =='P': - sys='Photonics' - if tag =='D': - sys='Diagnostics' - if fulltag =='DSCR': - sys='Camera' - if tag == 'R': - sys='RF' - if tag == 'M': - sys='Magnets' - if tag == 'U': - sys='Undulator' - return sys +def getDatasetSystem(name): + if len(name) > 11: + tag=name[8:9] + fulltag=name[8:12] + else: + tag='' + fulltag='' + sys='Unknown' + if tag =='P': + sys='Photonics' + if tag =='D': + sys='Diagnostics' + if fulltag =='DSCR': + sys='Camera' + if tag == 'R': + sys='RF' + if tag == 'M': + sys='Magnets' + if tag == 'U': + sys='Undulator' + return sys diff --git a/interface/snap.py b/interface/snap.py index 4bbe1b6..4b14b90 100644 --- a/interface/snap.py +++ b/interface/snap.py @@ -1,261 +1,71 @@ -import datetime -import re import numpy as np import yaml import os -import json - -import copy - -from threading import Thread import epics -class Snap: - def __init__(self,filename='/sf/data/applications/snapshot/req/op/SF_settings.yaml'): +# things to do: +# 1. Read a snapshot file (not request file) +# 2. Save a snapshot file +# 3. add parameters and performance channels (e.g. AT photon energies) - self.pvs = self.parseYAML(filename) - self.doAbort = False +def parseSnapShotReqYAML(filename): +# read the snapshot request file +# returns a list of PV names + if not filename: + filename = '/sf/data/applications/snapshot/req/op/SF_settings.yaml' + pvs = [] + path = os.path.dirname(filename) + with open(filename) as f: + try: + content = yaml.load(f, Loader=yaml.SafeLoader) + if 'include' in content.keys(): + if len(content['include']) > 0: + for cont in content['include']: + retpv = parseSnapShotReqYAML(path+'/'+cont['name']) + if 'macros' in cont.keys(): + retpv = applyReqMacro(retpv,cont['macros']) + pvs = pvs + retpv + if 'pvs' in content.keys(): + if 'list' in content['pvs']: + for pv in content['pvs']['list']: + pvs.append(pv['name']) + return pvs + return None + except yaml.YAMLError as e: + print(e) + return None + return None - def my_caget_many(self): - # this skips quite some channels - don't know why? +def applyReqMacro(pvs_in,macros): + pvs = [] + for macro in macros: + for key in macro: + tag='$('+key+')' + for pv in pvs_in: + if tag in pv: + pvs.append(pv.replace(tag,macro[key])) + for pv in pvs_in: # copy the ones without macro + if not '$(' in pv: + pvs.append(pv) + return pvs - pvdata = {} - pvchids = [] - # create, don't connect or create callbacks - for name in self.pvs: - chid = epics.ca.create_channel(name, connect=False, auto_cb=False) # note 1 - pvchids.append(chid) - - # connect - for chid in pvchids: - print(epics.ca.name(chid)) - epics.ca.connect_channel(chid) - - # request get, but do not wait for result - epics.ca.poll() - for chid in pvchids: - print(epics.ca.name(chid)) - epics.ca.get(chid, wait=False) # note 2 - - # now wait for get() to complete - epics.ca.poll() - for chid in pvchids: - print(epics.ca.name(chid)) - val = epics.ca.get_complete(chid,timeout=0.5) - if not val: - pvdata[epics.ca.name(chid)] = np.array([val]) - epics.ca.clear_cache() - return pvdata - - def abort(self): - self.doAbort=True - print('Aborting Snap') - - def getSnapValues(self,force=True): - self.doAbort=False - ret={} - if self.pvs: -# ret=self.my_caget_many() - val = epics.caget_many(self.pvs) - for i,pv in enumerate(self.pvs): - if val[i]: # filter out None values - ret[pv]={'val':float(val[i])} - epics.ca.clear_cache() - return ret,{} - - #------------- - # routines to parse the OP YAML file - - def applyMacro(self,pvs_in,macros): - pvs = [] - for macro in macros: - for key in macro: - tag='$('+key+')' - for pv in pvs_in: - if tag in pv: - pvs.append(pv.replace(tag,macro[key])) - for pv in pvs_in: # copy the ones without macro - if not '$(' in pv: - pvs.append(pv) - return pvs - - def parseYAML(self,filename='/sf/data/applications/snapshot/req/op/SF_settings.yaml'): - pvs = [] - path = os.path.dirname(filename) - with open(filename) as f: - try: - content = yaml.load(f, Loader=yaml.SafeLoader) - if 'include' in content.keys(): - if len(content['include']) > 0: - for cont in content['include']: - retpv = self.parseYAML(path+'/'+cont['name']) - if 'macros' in cont.keys(): - retpv = self.applyMacro(retpv,cont['macros']) - pvs = pvs + retpv - if 'pvs' in content.keys(): - if 'list' in content['pvs']: - for pv in content['pvs']['list']: - pvs.append(pv['name']) - return pvs - return None - - except yaml.YAMLError as e: - print(e) - return None - return None +def getSnap(pvs=None): + if not isinstance(pvs,list): + pvs = parseSnapShotReqYAML(pvs) + if not pvs: + return + ret={} + val = epics.caget_many(pvs) + for i,pv in enumerate(pvs): + if val[i]: # filter out None values + ret[pv]=float(val[i]) + epics.ca.clear_cache() + return ret -class Old: - - def __init__(self): - self.filename = filename - print('Estbalishing snapshot with request file:', filename,flush=True) - self.savepath = savepath - self.tolerance = 0.0005 - self.pvnames = [] - self.pvs = [] - self.mppvnames = [] - self.mppvs = [] - self.machinepar = [] - self.message = '' - if self.filename: - self.openRequestFile(self.filename) - - def openRequestFile(self, filename): - self.filename = filename - self.rootname = self.filename.split('/')[-1] - - isReq = True - if '.yaml' in filename: - isReq = False -# req_file = SnapshotReqFile(path=str(self.filename)) - - if newVersion: - if '.yaml' in filename: - req_file = SnapshotJsonFile(path=str(self.filename)) - else: - req_file = SnapshotReqFile(path=str(self.filename)) - pvs_list= req_file.read()[0] - print('PV List:-------------------------') - for i in range(len(pvs_list)): - print(pvs_list[i]) - print(req_file.read()[1]) - else: - if '.yaml' in filename: - self.filename=None - self.rootname = None - print('YAML files not supported') - return - req_file = SnapshotReqFile(str(self.filename)) - pvs_list = req_file.read() - self.pvnames.clear() - self.machinepar.clear() - for ele in pvs_list: - if isinstance(ele, list): - self.pvnames = ele - elif isinstance(ele, dict): - if 'machine_params' in ele.keys(): - self.machinepar = ele['machine_params'] - Thread(target=self.connectPVs).start() - - def connectPVs(self): - self.pvs = [PV(pv, auto_monitor=False) for pv in self.pvnames] - con = [pv.wait_for_connection(timeout=0.2) for pv in self.pvs] - pvscon=[] - for i, val in enumerate(con): - if val is False: - print('Cannot connect to PV:', self.pvs[i].pvname,flush=True) - else: - pvscon.append(self.pvs[i]) - self.pvs = copy.deepcopy(pvscon) - if isinstance(self.machinepar,list): - self.mppvs = [PV(self.machinepar[key], auto_monitor=False) for key in self.machinepar] - else: - self.mppvs = [PV(self.machinepar[key], auto_monitor=False) for key in self.machinepar.keys()] - con = [pv.wait_for_connection(timeout=0.2) for pv in self.mppvs] - pvscon.clear() - for i, val in enumerate(con): - if val is False: - print('Cannot connect to mPV:', self.mppvs[i].pvname,flush=True) - else: - pvscon.append(self.mppvs[i]) - self.mppvs=copy.deepcopy(pvscon) - - def getSnapValues(self, force=True): - values = {} - val = [pv.get(timeout=0.6, use_monitor=False) for pv in self.pvs] - for i, pv in enumerate(self.pvs): - if val[i] is None: - if force: - continue - else: - return False - else: - values[pv.pvname] = { - "raw_name": pv.pvname, "val": val[i], "EGU": pv.units, "prec": pv.precision} - mvalues = {} - val = [pv.get(timeout=0.6, use_monitor=False) for pv in self.mppvs] - for i, pv in enumerate(self.mppvs): - if val[i] is None: - if force: - continue - else: - return False - else: - mvalues[pv.pvname] = {"value": val[i], - "units": pv.units, "precision": pv.precision} - return values, mvalues - - def save(self, labels=[], comment="Generated by SFBD-Package", force=True): - if self.filename is None: - self.message = 'No Request File Loaded' - return False - - val, mval = self.getSnapValues(force) - if isinstance(val, bool) and val == False: - self.message = 'Unsuccesful reading of PV channels (unforced access)' - return False - - # construct file name - datetag = datetime.datetime.now().strftime('%Y%m%d_%H%M%S') - root = self.rootname.split('.req')[0] - files = self.savepath+root+'_'+datetag+'.snap' - filel = self.savepath+root+'_latest.snap' - - # reshuffle from mval to keyword based machine values - mmval = {} - for key in self.machinepar.keys(): - if self.machinepar[key] in mval.keys(): - mmval[key] = mval[self.machinepar[key]] - # save file - parse_to_save_file( - val, files, macros=None, symlink_path=filel, comment=comment, - labels=[], - req_file_name=self.rootname, machine_params=mmval) - self.message = 'Snapshot saved to '+files - return True - - def restore(self,filename,refilter='',force=True): - filepath=self.savepath+filename - prog=re.compile(refilter) - save_pvs=parse_from_save_file(filepath) - res={} - for ele in save_pvs: - if isinstance(ele,dict): - for key in ele.keys(): - if prog.match(key): - res[key]=ele[key]['value'] - - for pv in self.pvs: - if pv.pvname in res.keys(): - val=pv.get() - if val is None or np.abs(val-res[pv.pvname]) > self.tolerance: - pv.put(res[pv.pvname]) - self.message ='Snap restored' - -- 2.49.1 From dc5df0ca2a6f76341b6d1ea20b29709b6ad76ffc Mon Sep 17 00:00:00 2001 From: reiche Date: Tue, 13 Jun 2023 14:18:09 +0200 Subject: [PATCH 05/13] Support for various app and slic wrapper --- app/__init__.py | 2 + app/adaptiveorbit.py | 8 +- app/dispersiontools.py | 175 +++++++++++++++++++++++++++++++++++++++ app/hero.py | 111 +++++++++++++++++++++++++ app/spectralanalysis.py | 5 +- ext/__init__.py | 4 +- ext/camacquisition.py | 41 +++++++++ ext/counteradjustable.py | 23 +++++ ext/magnet.py | 21 +++++ ext/reichebscombined.py | 12 --- interface/__init__.py | 1 + interface/save.py | 23 ++--- interface/slic.py | 93 +++++++++++++++++++++ 13 files changed, 488 insertions(+), 31 deletions(-) create mode 100644 app/dispersiontools.py create mode 100644 app/hero.py create mode 100644 ext/camacquisition.py create mode 100644 ext/counteradjustable.py create mode 100644 ext/magnet.py delete mode 100644 ext/reichebscombined.py create mode 100644 interface/slic.py diff --git a/app/__init__.py b/app/__init__.py index f986faa..ac9d226 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,2 +1,4 @@ from .adaptiveorbit import AdaptiveOrbit from .spectralanalysis import SpectralAnalysis +from .hero import LaserPower,EnergyModulation +from .dispersiontools import Dispersion diff --git a/app/adaptiveorbit.py b/app/adaptiveorbit.py index 286d4b6..0c74146 100644 --- a/app/adaptiveorbit.py +++ b/app/adaptiveorbit.py @@ -39,7 +39,7 @@ class AdaptiveOrbit: def initBSStream(self,channels): print("Initializing BSstream") - bs = BSCache() + bs = BSCache(100000,10000) # 1 second time out, capazity for 100 second. bs.stop() for cnl in channels[1:]: if not is_available(cnl): @@ -63,10 +63,8 @@ class AdaptiveOrbit: return pvs def flush(self): - with self.bsAR.pt.queue.mutex: - self.bsAR.pt.queue.queue.clear() - with self.bsAT.pt.queue.mutex: - self.bsAT.pt.queue.queue.clear() + self.bsAR.flush() + self.bsAT.flush() def terminate(self): print('Stopping BSStream Thread...') diff --git a/app/dispersiontools.py b/app/dispersiontools.py new file mode 100644 index 0000000..e6e4ef6 --- /dev/null +++ b/app/dispersiontools.py @@ -0,0 +1,175 @@ +import datetime +import re +import numpy as np + +from bsread import dispatcher +import epics + +from slic.core.adjustable import PVAdjustable +from slic.core.acquisition import BSAcquisition +from slic.core.scanner import Scanner + + +def getAux(pvs=None): + if not pvs: + return + ret={} + val = epics.caget_many(pvs) + for i,pv in enumerate(pvs): + if val[i]: # filter out None values + ret[pv]=float(val[i]) + epics.ca.clear_cache() + return ret + + +def getBSChannels(regexp): + prog = re.compile(regexp) + res = [] + for bs in dispatcher.get_current_channels(): + if prog.match(bs['name']): + res.append(bs['name']) + return res + + +class Dispersion: + def __init__(self, branch = 'Aramis'): + self.scanname = 'Dispersion' + self.branch = 'None' + dirpath= datetime.datetime.now().strftime('/sf/data/measurements/%Y/%m/%d/slic_sfbd') + self.scandir='%s/%s' % (dirpath,self.scanname) + + self.setBranch() + self.sc = None + self.Nsteps = 2 + self.Nsamples = 1 + + + def setBranch(self,branch = 'Aramis'): + if branch == 'Athos Dump': + self.setupAthosDump() + elif branch == 'Aramis': + self.setupAramis() + else: + self.branch = 'None' + + + def setupAramis(self): + # pre-scan item + self.pre = {} + self.pre['SFB_BEAM_DUMP_AR:ONOFF1']={'Val':0,'InitVal':0} + self.pre['SFB_BEAM_ENERGY_ECOL_AR:ONOFF1']={'Val':0,'InitVal':0} + self.pre['SFB_ORBIT_S30:ONOFF1']={'Val':0,'InitVal':0} + self.pre['SFB_ORBIT_SAR:ONOFF1']={'Val':0,'InitVal':0} + for pv in self.pre.keys(): + self.pre[pv]['adj']=PVAdjustable(pv) + # adjustable + self.adjSV = 'S30:SET-E-GAIN-OP' + self.adjRB = 'S30:GET-E-GAIN-OP' + self.adj = PVAdjustable(self.adjSV,pvname_readback = self.adjRB, accuracy = 0.1) + self.amp = 20 # the amplitude of the scan, which can be scaled + # acquisition + sensor1 = getBSChannels('SAR.*DBPM.*:[XY]1$') + sensor2 = getBSChannels('S[23].*-RLLE-DSP:.*-VS$') + self.sensor = sensor1+sensor2 + self.acq = [BSAcquisition("machine","sfbd", default_channels=self.sensor)] + # auxiliar data to be read one + self.aux=[] + for sen in sensor2: + if 'PHASE-VS' in sen: + self.aux.append(sen.replace('PHASE-VS','GET-VSUM-PHASE-OFFSET').replace('RLLE-DSP','RSYS')) + self.aux.append(sen.replace('PHASE-VS','GET-VSUM-AMPLT-SCALE').replace('RLLE-DSP','RSYS')) + self.aux.append(sen.replace('PHASE-VS','SM-SET').replace('RLLE-DSP','RMSM')) + self.aux.append('S10BC02-MBND100:ENERGY-OP') + + # scanner + self.branch='Aramis' + self.path = '%s-%s' % (self.scanname,self.branch) + self.scanner = Scanner(data_base_dir=self.path,scan_info_dir=self.path, + make_scan_sub_dir=True, + default_acquisitions=self.acq) + + def setupAthosDump(self): + # pre-scan item + self.pre = {} + self.pre['SFB_BEAM_DUMP_AT:ONOFF1']={'Val':0,'InitVal':0} + self.pre['SFB_ORBIT_SAT:ONOFF1']={'Val':0,'InitVal':0} + for i in range(1,5): + self.pre['SFB_ORBIT_SAT_%2.2d:ONOFF1' % i ]={'Val':0,'InitVal':0} + for pv in self.pre.keys(): + self.pre[pv]['adj']=PVAdjustable(pv) + # adjustable + self.adjSV = 'SATCB01-RSYS:SET-BEAM-PHASE' + self.adjRB = 'SATCB01-RSYS:GET-BEAM-PHASE' + self.adj = PVAdjustable(self.adjSV,pvname_readback = self.adjRB, accuracy = 0.1) + self.amp = 20 # the amplitude of the scan, which can be scaled + # acquisition + self.sensor = ['SATBD02-DBPM010:Y2','SATBD02-DBPM040:Y2'] + self.acq = [BSAcquisition("machine","sfbd", default_channels=self.sensor)] + # auxiliar data to be read one + aux = ['SATCL01-MBND100:ENERGY-OP'] + # scanner + self.branch='Athos_Dump' + self.path = '%s-%s' % (self.scanname,self.branch) + self.scanner = Scanner(data_base_dir=self.path,scan_info_dir=self.path, + make_scan_sub_dir=True, + default_acquisitions=self.acq) + + + + + def setup(self,scl = 1, Nsteps=5, Nsamples=5): + val = self.adj.get_current_value(readback=False) + dval = self.amp*scl + self.N = Nsteps + self.Ns= Nsamples + self.values=np.linspace(val-dval,val+dval,num=self.N) + + + def preaction(self): + for key in self.pre.keys(): + self.pre[key]['InitVal'] = self.pre[key]['adj'].get_current_value(readback = False) + self.pre[key]['adj'].set_target_value(self.pre[key]['Val']) + + def postaction(self): + for key in self.pre.keys(): + self.pre[key]['adj'].set_target_value(self.pre[key]['InitVal']) + + + def scan(self): + self.sc=self.scanner.ascan_list(self.adj,self.values, + filename=self.scanname,start_immediately = False, + n_pulses=self.Ns,return_to_initial_values=True) + self.preaction() + self.sc.run() + self.auxdata = getAux(self.aux) + self.postaction() + + + def stop(self): + if self.sc is None: + return + self.sc.stop() + + def running(self): + return self.sc.running + + def status(self): + si = self.sc.scan_info.to_dict() + steps = 0 + if 'scan_values' in si: + steps=len(si['scan_values']) + return steps,self.N + + def info(self): + return self.sc.scan_info.to_dict() + + + + + + + + + + + diff --git a/app/hero.py b/app/hero.py new file mode 100644 index 0000000..4f2ba5b --- /dev/null +++ b/app/hero.py @@ -0,0 +1,111 @@ +import datetime +import numpy as np + +from slic.core.acquisition import PVAcquisition +from slic.core.acquisition import BSAcquisition +from slic.devices.general import motor +from slic.core.scanner import Scanner +from sfbd.ext import CamAcquisition + +# some temporary wrapper +class PollingPVAcquisition(PVAcquisition): + def _acquire(self, *args, polling=True, **kwargs): + return super()._acquire(*args, polling=polling, **kwargs) + +class LaserScanBase: + def __init__(self): + print('Init Base Class') + self.SV= 'SSL-LMOT-M1104:MOT' + self.pol = motor.Motor(self.SV) + + def stop(self): + if self.sc is None: + return + self.sc.stop() + + def running(self): + return self.sc.running + + def status(self): + si = self.sc.scan_info.to_dict() + steps = 0 + if 'scan_values' in si: + steps=len(si['scan_values']) + return steps,self.N + + def info(self): + return self.sc.scan_info.to_dict() + + def setup(self,amax=21,Nsteps=5,Nsamples=5): + amin = 0 + self.N = Nsteps + self.Ns= Nsamples + self.values=np.linspace(19,21,num=self.N) # needs a change + +# measuring the pulse energy as a function of the controling PV. Note that the power should be limited to 300 uJ +# thus limiting the value of the actuaor defining the lase rpulse energy in the EnergyModulaiton class. + +class LaserPower(LaserScanBase): + def __init__(self): + super(LaserPower,self).__init__() + + self.scanname = 'HEROLaserEnergy' + dirpath= datetime.datetime.now().strftime('/sf/data/measurements/%Y/%m/%d/slic_sfbd') + self.scandir='%s/%s' % (dirpath,self.scanname) + + self.RB = 'SSL-LENG-SLNK1:VAL_GET' + self.erg = PollingPVAcquisition("machine","sfbd", default_channels=[self.RB]) + + self.scanner = Scanner(data_base_dir=self.scandir,scan_info_dir=self.scandir,make_scan_sub_dir=True, + default_acquisitions=[self.erg]) + + def scan(self): + self.sc=self.scanner.ascan_list(self.pol,self.values, + filename=self.scanname,start_immediately = False, + n_pulses=self.Ns,return_to_initial_values=True) + self.sc.run() + + + + +# measuring the coherent emission/space charge blow-up as a function of the hero energy modulation + +class EnergyModulation(LaserScanBase): + def __init__(self, acq = 0): + super(EnergyModulation,self).__init__() + self.scanname = 'HEROEnergyModulation' + dirpath= datetime.datetime.now().strftime('/sf/data/measurements/%Y/%m/%d/slic_sfbd') + self.scandir='%s/%s' % (dirpath,self.scanname) + + self.acq = acq + if self.acq == 0: + self.RB ='SATFE10-PEPG046-EVR0:CALCI' + self.erg = BSAcquisition("machine","sfbd", default_channels=[self.RB]) + elif self.acq == 1: + self.RB ='SATBD02-DBPM040:Y2' + self.erg = BSAcquisition("machine","sfbd", default_channels=[self.RB]) + elif self.acq == 2: + self.RB = 'SATBD01-DSCR210' + self.erg = CamAcquisition("machine","sfbd", default_channels=[self.RB]) + self.erg.getConnection(self.RB) + else: + self.RB = 'SATBD02-DSCR050' + self.erg = CamAcquisition("machine","sfbd", default_channels=[self.RB]) + self.erg.getConnection(self.RB) + + self.scanner = Scanner(data_base_dir=self.scandir,scan_info_dir=self.scandir,make_scan_sub_dir=True, + default_acquisitions=[self.erg]) + + def scan(self): + self.sc=self.scanner.ascan_list(self.pol,self.values, + filename=self.scanname,start_immediately = False, + n_pulses=self.Ns,return_to_initial_values=True) + self.sc.run() + + + + + + + + diff --git a/app/spectralanalysis.py b/app/spectralanalysis.py index 7d16478..4899cc7 100644 --- a/app/spectralanalysis.py +++ b/app/spectralanalysis.py @@ -11,7 +11,7 @@ class SpectralAnalysis: """ def __init__(self): - self.bs = BSCache() + self.bs = BSCache(100000,10000) # 100 second timeout, size for 100 second data taken self.bs.stop() self.channel = '' @@ -35,8 +35,7 @@ class SpectralAnalysis: self.bs.pt.running.clear() # for some reason I have to def flush(self): - with self.bs.pt.queue.mutex: - self.bs.pt.queue.queue.clear() + self.bs.flush() def read(self): data=self.bs.__next__() diff --git a/ext/__init__.py b/ext/__init__.py index ae07e6b..01b2db3 100644 --- a/ext/__init__.py +++ b/ext/__init__.py @@ -1 +1,3 @@ -from .reichebscombined import ReicheBSCombined +from .magnet import Magnet +from .camacquisition import CamAcquisition +from .counteradjustable import CounterAdjustable diff --git a/ext/camacquisition.py b/ext/camacquisition.py new file mode 100644 index 0000000..1f8d487 --- /dev/null +++ b/ext/camacquisition.py @@ -0,0 +1,41 @@ +from time import sleep +from tqdm import trange +import h5py + +from cam_server_client import PipelineClient +from cam_server_client.utils import get_host_port_from_stream_address +from bsread import source, SUB + + +from slic.core.acquisition.acquisition import Acquisition +class CamAcquisition(Acquisition): + + def getConnection(self,cam): + pipeline_client = PipelineClient() + cam_instance_name = str(cam) + "_sp1" + stream_address = pipeline_client.get_instance_stream(cam_instance_name) + self.host, self.port = get_host_port_from_stream_address(stream_address) + print(self.host,self.port) + + def _acquire(self, filename, channels=None, data_base_dir=None, scan_info=None, n_pulses=100, **kwargs): + print("my routine") + print("extra kwargs:", kwargs) + args = (filename, n_pulses, channels) + args = ", ".join(repr(i) for i in args) + print("acquire({})".format(args)) + print(f"dummy acquire to {filename}:") + +# stream_host,stream_port = getPipeLine(channels[0]) +# time.wait(1) + data= [] + with source(host=self.host, port=self.port, mode=SUB) as input_stream: + input_stream.connect() + for i in range(n_pulses): + print('Camera Images', i) + message = input_stream.receive() + data.append(message.data.data) + hid = h5py.File(filename,'w') + gid = hid.create_group(channels[0]) + for key in data[0].keys(): + gid.create_dataset(key, data = [rec[key].value for rec in data]) + hid.close() diff --git a/ext/counteradjustable.py b/ext/counteradjustable.py new file mode 100644 index 0000000..990a733 --- /dev/null +++ b/ext/counteradjustable.py @@ -0,0 +1,23 @@ +from slic.core.adjustable import Adjustable + +class CounterAdjustable(Adjustable): + def __init__(self, adjustable1, adjustable2): + self.adj1=adjustable1 + self.adj2=adjustable2 + self.ref_values() # implementation needs reference values to convert absolute scan to relative scan + + def ref_value(self): + self.val1 = self.adj1.get_current_value(readback = False) + self.val2 = self.adj2.get_current_value(readback = False) + + def set_target_value(self, value): + t1 = self.adj1.set_target_value(self.val1 + value) + t2 = self.adj2.set_target_value(self.val2 - value) + t1.wait() + t2.wait() + + def get_current_value(self): + return self.adj1.get_current_value() + + def is_moving(self): + return any([self.adj1.is_moving(),self.adj2.is_moving()]) diff --git a/ext/magnet.py b/ext/magnet.py new file mode 100644 index 0000000..2243b39 --- /dev/null +++ b/ext/magnet.py @@ -0,0 +1,21 @@ +from slic.core.adjustable import PVAdjustable +from slic.utils import typename + +class Magnet(PVAdjustable): + + def __init__(self,name): + self.name=name + pvsv='%s:I-SET' % name + pvrb='%s:I-READ' % name + tol = 0.075 + super().__init__(pvsv,pvname_readback=pvrb,accuracy=tol,internal=True) + + + + @property + def status(self): + return "Cycling" + + def __repr__(self): + tn = typename(self) + return f"{tn} \"{self.name}\" is {self.status}" diff --git a/ext/reichebscombined.py b/ext/reichebscombined.py deleted file mode 100644 index 1541705..0000000 --- a/ext/reichebscombined.py +++ /dev/null @@ -1,12 +0,0 @@ -from slic.core.sensor.bsmonitor import BSMonitor - -class ReicheBSCombined(BSMonitor): - - # Du brauchst kein extra init. BSMonitor tut schon das richtige... - - def _unpack(self, data): - # data ist ein dict mit allen Deinen Kanälen - pid = data["pid"] # der effektive Channel-Name der Pulse ID - # hier dein Code - # am Ende sollte eine Zahl rauskommen: - return data diff --git a/interface/__init__.py b/interface/__init__.py index a439ca0..3aa44be 100644 --- a/interface/__init__.py +++ b/interface/__init__.py @@ -2,3 +2,4 @@ from .snap import getSnap from .save import saveDataset from .load import loadDataset from .elog import writeElog +from .slic import SlicScan diff --git a/interface/save.py b/interface/save.py index 648a146..b6abdb0 100644 --- a/interface/save.py +++ b/interface/save.py @@ -50,7 +50,8 @@ def saveDataset(program,data,actuator=None,snap=None,analysis=None,figures=None) writeSnap(hid,snap) hid.close() - writeFigure(filename,figures) + if figures: + writeFigure(filename,figures) return filename @@ -83,6 +84,16 @@ def openDataset(program): def writeData(hid, data, scanrun=1): + # write the sensor raw value + for ele in data.keys(): + name=ele.split(':') + if len(name)>1: + dset=hid.create_dataset('scan_%d/data/%s/%s' % (scanrun, name[0], name[1]), data=data[ele]) + else: + dset=hid.create_dataset('scan_%d/data/%s' % (scanrun, name[0]), data=data[ele]) + dset.attrs['system'] = getDatasetSystem(name[0]) + dset.attrs['units'] = 'unknown' + # this part is obsolete - dimension should be given from the individual datasets if not 'pid' in data.keys(): return shape = data['pid'].shape @@ -95,15 +106,7 @@ def writeData(hid, data, scanrun=1): hid.create_dataset("scan_%d/method/samples" % scanrun,data=[nsam]) hid.create_dataset("scan_%d/method/dimension" % scanrun,data=[ndim]) hid.create_dataset("scan_%d/method/reducedData" % scanrun,data=[0]) # indicating that there is at least a 2D array for scalar data - # write the sensor raw value - for ele in data.keys(): - name=ele.split(':') - if len(name)>1: - dset=hid.create_dataset('scan_%d/data/%s/%s' % (scanrun, name[0], name[1]), data=data[ele]) - else: - dset=hid.create_dataset('scan_%d/data/%s' % (scanrun, name[0]), data=data[ele]) - dset.attrs['system'] = getDatasetSystem(name[0]) - dset.attrs['units'] = 'unknown' + def writeActuator(hid,act,scanrun=1): if not act: diff --git a/interface/slic.py b/interface/slic.py new file mode 100644 index 0000000..420f32e --- /dev/null +++ b/interface/slic.py @@ -0,0 +1,93 @@ +import h5py +import numpy as np +import time +from threading import Thread +from PyQt5.QtCore import QObject, pyqtSignal + +# to do: +# 1 - check if scan thread is running +# 2 - import of BSread data + +from sfbd.interface import getSnap + +class SlicScan(QObject): + + siginc = pyqtSignal(int, int) # signal for increment + sigterm = pyqtSignal(int) # signal for termination + sigsnap = pyqtSignal(bool) + + def __init__(self): + QObject.__init__(self) + self.clear() + + def clear(self): + self.daq = None + self.data = None + self.act = None + self.snap = None + + def start(self,daq,snap=False): + self.clear() + Thread(target=self.Tmonitor).start() + self.startSnap(snap) + + def startSnap(self,snap=False): + if not snap: + Thread(target=self.Tsnap).start() + + def Tsnap(self): + self.snap = getSnap() + self.sigsnap.emit(True) + + def Tmonitor(self): + mythread = Thread(target=self.Tscanner).start() + time.sleep(1) + ostep = -1 + while(self.daq.running()): + istep,nstep=self.daq.status() + if istep>ostep: + ostep=istep + self.siginc.emit(istep,nstep) + time.sleep(1) + if not mythread == None: # wait till scanning thread is done + mythread.join() + istep,nstep=self.daq.status() + self.siginc.emit(istep,nstep) + self.data,self.act = importSlicScan(self.daq.info()) + if hasattr(self.daq,'auxdata'): + self.data.update(self.daq.auxdata) + self.sigterm.emit(istep==nstep) + + def Tscanner(self): + self.daq.scan() + + def stop(self): + self.daq.stop() + + +def importSlicScan(scan_info): + if not isinstance(scan_info,dict): + return None,None + if not 'scan_files' in scan_info.keys(): + return None,None + sfiles = scan_info['scan_files'] + data = {} + for istep, sfile in enumerate(sfiles): + hid = h5py.File(sfile[0],'r') + for name, h5obj in hid.items(): + if isinstance(h5obj,h5py.Dataset): # pv channels + data[name] = addDatasetToData(data,name,h5obj) + elif isinstance(h5obj,h5py.Group): # bs read channels + if 'data' in h5obj: + data[name] = addDatasetToData(data,name,h5obj['data']) + actuator = {} + name = scan_info['scan_parameters']['name'][0] + actuator[name]=np.array(scan_info['scan_values']) + data[name]=np.array(scan_info['scan_readbacks']) + return data,actuator + +def addDatasetToData(data,name,h5obj): + if not name in data: + return np.array([h5obj[()]]) + else: + return np.append(data[name],np.array([h5obj[()]]),axis=0) -- 2.49.1 From a54abd383e1f23a49a53854d304d76467bcd12c3 Mon Sep 17 00:00:00 2001 From: reiche Date: Fri, 16 Jun 2023 16:58:56 +0200 Subject: [PATCH 06/13] Debug Dispersion measurement and added first support for XTCAV stabilizer --- app/adaptiveorbit.py | 15 ++--- app/dispersiontools.py | 120 ++++++++++++++++++++++++++-------------- app/hero.py | 12 ++-- app/spectralanalysis.py | 1 - app/xtcavstabilizer.py | 29 ++++++++++ interface/load.py | 6 +- interface/save.py | 2 + interface/slic.py | 3 +- 8 files changed, 132 insertions(+), 56 deletions(-) create mode 100644 app/xtcavstabilizer.py diff --git a/app/adaptiveorbit.py b/app/adaptiveorbit.py index 0c74146..3b847fe 100644 --- a/app/adaptiveorbit.py +++ b/app/adaptiveorbit.py @@ -40,13 +40,14 @@ class AdaptiveOrbit: def initBSStream(self,channels): print("Initializing BSstream") bs = BSCache(100000,10000) # 1 second time out, capazity for 100 second. - bs.stop() - for cnl in channels[1:]: - if not is_available(cnl): - raise ValueError(f"BS-Channel {cbl} is not available") - res = make_channel_config(cnl,None,None) - bs.channels[res]=res - bs.get_var(channels[0]) # this starts also the stream into the cache +# bs.stop() +# for cnl in channels[1:]: +# if not is_available(cnl): +# raise ValueError(f"BS-Channel {cbl} is not available") +# res = make_channel_config(cnl,None,None) +# bs.channels[res]=res +# bs.get_var(channels[0]) # this starts also the stream into the cache + bs.get_vars(channels) return bs def initPV(self,chx): diff --git a/app/dispersiontools.py b/app/dispersiontools.py index e6e4ef6..ff5689a 100644 --- a/app/dispersiontools.py +++ b/app/dispersiontools.py @@ -6,9 +6,9 @@ from bsread import dispatcher import epics from slic.core.adjustable import PVAdjustable -from slic.core.acquisition import BSAcquisition +from slic.core.acquisition import BSAcquisition, PVAcquisition from slic.core.scanner import Scanner - +from sfbd.ext import CounterAdjustable def getAux(pvs=None): if not pvs: @@ -34,26 +34,75 @@ def getBSChannels(regexp): class Dispersion: def __init__(self, branch = 'Aramis'): self.scanname = 'Dispersion' - self.branch = 'None' - dirpath= datetime.datetime.now().strftime('/sf/data/measurements/%Y/%m/%d/slic_sfbd') - self.scandir='%s/%s' % (dirpath,self.scanname) + self.branch = 'None' + dirpath= datetime.datetime.now().strftime('measurements/%Y/%m/%d/slic_sfbd') + self.basedir = '/sf/data/%s/%s' % (dirpath,self.scanname) - self.setBranch() + self.pgroup='%s/%s' % (dirpath,self.scanname) + self.scandir='/sf/data/'+self.pgroup + self.setBranch(branch) self.sc = None self.Nsteps = 2 self.Nsamples = 1 def setBranch(self,branch = 'Aramis'): + self.sc = None if branch == 'Athos Dump': self.setupAthosDump() elif branch == 'Aramis': self.setupAramis() + elif branch == 'Athos': + self.setupAthos() else: - self.branch = 'None' + self.scanner = None + return + # path is define in the various set-up procedures + self.scanner = Scanner(data_base_dir=self.basedir,scan_info_dir=self.basedir, + make_scan_sub_dir=True, + default_acquisitions=self.acq) + + + def getRFCalibrationChannels(self,sensor2,energy): + aux=[] + for sen in sensor2: + if 'PHASE-VS' in sen: + aux.append(sen.replace('PHASE-VS','GET-VSUM-PHASE-OFFSET').replace('RLLE-DSP','RSYS')) + aux.append(sen.replace('PHASE-VS','GET-VSUM-AMPLT-SCALE').replace('RLLE-DSP','RSYS')) + aux.append(sen.replace('PHASE-VS','SM-GET').replace('RLLE-DSP','RMSM')) + aux.append(energy) + return aux + + + def setupAthosDump(self): + self.branch='Athos_Dump' + pgroup = '%s-%s' % (self.pgroup,self.branch) + self.path = '/sf/data/'+pgroup + # pre-scan item - needs an adjustment of the dipole current of about 2.3e-3 - to be implemented later. + self.pre = {} + self.pre['SFB_BEAM_DUMP_AT:ONOFF1']={'Val':0,'InitVal':0} + self.pre['SFB_ORBIT_SAT:ONOFF1']={'Val':0,'InitVal':0} + for i in range(1,5): + self.pre['SFB_ORBIT_SAT_%2.2d:ONOFF1' % i ]={'Val':0,'InitVal':0} + for pv in self.pre.keys(): + self.pre[pv]['adj']=PVAdjustable(pv) + # adjustable + self.adjSV = 'SATCB01-RSYS:SET-BEAM-PHASE' + self.adjRB = 'SATCB01-RSYS:GET-BEAM-PHASE' + self.adj = PVAdjustable(self.adjSV,pvname_readback = self.adjRB, accuracy = 0.1) + self.amp = 30 # the amplitude of the scan, which can be scaled + # acquisition + sensor1 = getBSChannels('SATBD02-DBPM.*:Y2$') + sensor2 = getBSChannels('SATCB.*-RLLE-DSP:.*-VS$') + self.sensor = sensor1+sensor2 + self.acq = [BSAcquisition(".",pgroup, default_channels=self.sensor)] + # auxiliar data to be read one + self.aux = self.getRFCalibrationChannels(sensor2,'SATCL01-MBND100:ENERGY-OP') - def setupAramis(self): + self.branch='Aramis' + pgroup = '%s-%s' % (self.pgroup,self.branch) + self.path = '/sf/data/'+pgroup # pre-scan item self.pre = {} self.pre['SFB_BEAM_DUMP_AR:ONOFF1']={'Val':0,'InitVal':0} @@ -71,55 +120,46 @@ class Dispersion: sensor1 = getBSChannels('SAR.*DBPM.*:[XY]1$') sensor2 = getBSChannels('S[23].*-RLLE-DSP:.*-VS$') self.sensor = sensor1+sensor2 - self.acq = [BSAcquisition("machine","sfbd", default_channels=self.sensor)] + self.acq = [BSAcquisition(".",pgroup, default_channels=self.sensor)] # auxiliar data to be read one - self.aux=[] - for sen in sensor2: - if 'PHASE-VS' in sen: - self.aux.append(sen.replace('PHASE-VS','GET-VSUM-PHASE-OFFSET').replace('RLLE-DSP','RSYS')) - self.aux.append(sen.replace('PHASE-VS','GET-VSUM-AMPLT-SCALE').replace('RLLE-DSP','RSYS')) - self.aux.append(sen.replace('PHASE-VS','SM-SET').replace('RLLE-DSP','RMSM')) - self.aux.append('S10BC02-MBND100:ENERGY-OP') - - # scanner + self.aux = self.getRFCalibrationChannels(sensor2,'S10BC02-MBND100:ENERGY-OP') + + def setupAthos(self): self.branch='Aramis' - self.path = '%s-%s' % (self.scanname,self.branch) - self.scanner = Scanner(data_base_dir=self.path,scan_info_dir=self.path, - make_scan_sub_dir=True, - default_acquisitions=self.acq) - - def setupAthosDump(self): + pgroup = '%s-%s' % (self.pgroup,self.branch) + self.path = '/sf/data/'+pgroup # pre-scan item self.pre = {} self.pre['SFB_BEAM_DUMP_AT:ONOFF1']={'Val':0,'InitVal':0} + self.pre['SFB_BEAM_ENERGY_ECOL_AT:ONOFF1']={'Val':0,'InitVal':0} + self.pre['SFB_ORBIT_SWY:ONOFF1']={'Val':0,'InitVal':0} self.pre['SFB_ORBIT_SAT:ONOFF1']={'Val':0,'InitVal':0} for i in range(1,5): self.pre['SFB_ORBIT_SAT_%2.2d:ONOFF1' % i ]={'Val':0,'InitVal':0} for pv in self.pre.keys(): self.pre[pv]['adj']=PVAdjustable(pv) # adjustable - self.adjSV = 'SATCB01-RSYS:SET-BEAM-PHASE' - self.adjRB = 'SATCB01-RSYS:GET-BEAM-PHASE' + self.adjSV = 'S20:SET-E-GAIN-OP' + self.adjRB = 'S20:GET-E-GAIN-OP' self.adj = PVAdjustable(self.adjSV,pvname_readback = self.adjRB, accuracy = 0.1) - self.amp = 20 # the amplitude of the scan, which can be scaled +# self.adj2SV = 'S30:SET-E-GAIN-OP' +# self.adj2RB = 'S30:GET-E-GAIN-OP' +# self.adj2 = PVAdjustable(self.adj2SV,pvname_readback = self.adj2RB, accuracy = 0.1) +# self.adj = CounterAdjustable(self.adj1,self.adj2) # combine the two channels + self.amp = 10 # the amplitude of the scan, which can be scaled # acquisition - self.sensor = ['SATBD02-DBPM010:Y2','SATBD02-DBPM040:Y2'] - self.acq = [BSAcquisition("machine","sfbd", default_channels=self.sensor)] + sensor1 = getBSChannels('SAT[SDC].*DBPM.*:[XY]2$') + sensor2 = getBSChannels('S[2].*-RLLE-DSP:.*-VS$') + self.sensor = sensor1+sensor2 + self.acq = [BSAcquisition(".",pgroup, default_channels=self.sensor)] # auxiliar data to be read one - aux = ['SATCL01-MBND100:ENERGY-OP'] - # scanner - self.branch='Athos_Dump' - self.path = '%s-%s' % (self.scanname,self.branch) - self.scanner = Scanner(data_base_dir=self.path,scan_info_dir=self.path, - make_scan_sub_dir=True, - default_acquisitions=self.acq) - - - + self.aux = self.getRFCalibrationChannels(sensor2,'S10BC02-MBND100:ENERGY-OP') + def setup(self,scl = 1, Nsteps=5, Nsamples=5): val = self.adj.get_current_value(readback=False) dval = self.amp*scl + dval = 0 self.N = Nsteps self.Ns= Nsamples self.values=np.linspace(val-dval,val+dval,num=self.N) @@ -137,7 +177,7 @@ class Dispersion: def scan(self): self.sc=self.scanner.ascan_list(self.adj,self.values, - filename=self.scanname,start_immediately = False, + filename=self.branch,start_immediately = False, n_pulses=self.Ns,return_to_initial_values=True) self.preaction() self.sc.run() diff --git a/app/hero.py b/app/hero.py index 4f2ba5b..49451b3 100644 --- a/app/hero.py +++ b/app/hero.py @@ -3,6 +3,7 @@ import numpy as np from slic.core.acquisition import PVAcquisition from slic.core.acquisition import BSAcquisition +from slic.core.adjustable import PVAdjustable from slic.devices.general import motor from slic.core.scanner import Scanner from sfbd.ext import CamAcquisition @@ -17,7 +18,8 @@ class LaserScanBase: print('Init Base Class') self.SV= 'SSL-LMOT-M1104:MOT' self.pol = motor.Motor(self.SV) - +# self.pol = PVAdjustable(self.SV) + def stop(self): if self.sc is None: return @@ -36,11 +38,13 @@ class LaserScanBase: def info(self): return self.sc.scan_info.to_dict() - def setup(self,amax=21,Nsteps=5,Nsamples=5): + def setup(self,amax=45,Nsteps=5,Nsamples=5): amin = 0 self.N = Nsteps self.Ns= Nsamples - self.values=np.linspace(19,21,num=self.N) # needs a change + amin = 15 + amax = 22 + self.values=np.linspace(amin,amax,num=self.N) # needs a change # measuring the pulse energy as a function of the controling PV. Note that the power should be limited to 300 uJ # thus limiting the value of the actuaor defining the lase rpulse energy in the EnergyModulaiton class. @@ -54,7 +58,7 @@ class LaserPower(LaserScanBase): self.scandir='%s/%s' % (dirpath,self.scanname) self.RB = 'SSL-LENG-SLNK1:VAL_GET' - self.erg = PollingPVAcquisition("machine","sfbd", default_channels=[self.RB]) + self.erg = PVAcquisition("machine","sfbd", default_channels=[self.RB]) self.scanner = Scanner(data_base_dir=self.scandir,scan_info_dir=self.scandir,make_scan_sub_dir=True, default_acquisitions=[self.erg]) diff --git a/app/spectralanalysis.py b/app/spectralanalysis.py index 4899cc7..18ce27c 100644 --- a/app/spectralanalysis.py +++ b/app/spectralanalysis.py @@ -2,7 +2,6 @@ import time import numpy as np from bstrd import BSCache -from bstrd.bscache import make_channel_config, is_available from epics import PV class SpectralAnalysis: diff --git a/app/xtcavstabilizer.py b/app/xtcavstabilizer.py new file mode 100644 index 0000000..1e58ce1 --- /dev/null +++ b/app/xtcavstabilizer.py @@ -0,0 +1,29 @@ +import time +import numpy as np + +from bstrd import BSCache + +class XTCAVStabilizer: + """ + Wrapper class to bundle all daq/io needed for stabilizing the XTCAV + """ + def __init__(self): + self.bs = BSCache(100000,10000) # 100 second timeout, size for 100 second data taken + self.channels = ['SATBD02-DBPM040:X2','SATMA02-RLLE-DSP:PHASE-VS'] + self.bs.get_vars(self.channels) # this starts the stream into the cache + self.bs.stop() + + + def terminate(self): + print('Stopping BSStream Thread...') + self.bs.stop() + self.bs.pt.running.clear() # for some reason I have to + + def flush(self): + self.bs.flush() + + def read(self): + data=self.bs.__next__() + return data['pid'],data[self.channels[0]],data[self.channels[1]] # returns PID, BPM reading, TCAV Phase + + diff --git a/interface/load.py b/interface/load.py index 79c0601..748aecb 100644 --- a/interface/load.py +++ b/interface/load.py @@ -39,12 +39,12 @@ def loadActuator(hid,scanrun=1): data = {} if 'actuators' in hid[run]['method'].keys(): for key1 in hid[run]['method']['actuators'].keys(): - if isinstance(hid[run]['method']['actuators'],h5py.Group): + if isinstance(hid[run]['method']['actuators'][key1],h5py.Group): for key2 in hid[run]['method']['actuators'][key1].keys(): val = hid[run]['method']['actuators'][key1][key2][()] data[key1+':'+key2]={'val':val} - else: - data[key1]=hid[run]['method']['actuators'][key1][()] + else: + data[key1]=hid[run]['method']['actuators'][key1][()] return data diff --git a/interface/save.py b/interface/save.py index b6abdb0..934c036 100644 --- a/interface/save.py +++ b/interface/save.py @@ -154,6 +154,8 @@ def writeAnalysis(hid,data,scanrun=1): def writeFigure(filename,figs): for i,ele in enumerate(figs): + if ele == None: + continue plotname='%s_Fig%d.png' % (filename,(i+1)) im = Image.open(ele) im.save(plotname) diff --git a/interface/slic.py b/interface/slic.py index 420f32e..c2924c9 100644 --- a/interface/slic.py +++ b/interface/slic.py @@ -28,11 +28,12 @@ class SlicScan(QObject): def start(self,daq,snap=False): self.clear() + self.daq=daq Thread(target=self.Tmonitor).start() self.startSnap(snap) def startSnap(self,snap=False): - if not snap: + if snap: Thread(target=self.Tsnap).start() def Tsnap(self): -- 2.49.1 From 0c928a8bb134a199bc05817db3c351f5a4fdcb37 Mon Sep 17 00:00:00 2001 From: reiche Date: Wed, 5 Jul 2023 09:16:18 +0200 Subject: [PATCH 07/13] Improved snapshot interface with saving snapshot files in the OP configuration. Added app support for stabilizing XTCAV. Added a BSacquisition based on BSCache --- app/__init__.py | 1 + app/dispersiontools.py | 9 +++++---- app/xtcavstabilizer.py | 19 +++++++++++++++---- ext/__init__.py | 1 + ext/bscacquisition.py | 42 ++++++++++++++++++++++++++++++++++++++++++ interface/__init__.py | 1 + interface/snap.py | 17 ++++++++++++++--- 7 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 ext/bscacquisition.py diff --git a/app/__init__.py b/app/__init__.py index ac9d226..13ed080 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -2,3 +2,4 @@ from .adaptiveorbit import AdaptiveOrbit from .spectralanalysis import SpectralAnalysis from .hero import LaserPower,EnergyModulation from .dispersiontools import Dispersion +from .xtcavstabilizer import XTCAVStabilizer diff --git a/app/dispersiontools.py b/app/dispersiontools.py index ff5689a..ff2b6be 100644 --- a/app/dispersiontools.py +++ b/app/dispersiontools.py @@ -9,6 +9,7 @@ from slic.core.adjustable import PVAdjustable from slic.core.acquisition import BSAcquisition, PVAcquisition from slic.core.scanner import Scanner from sfbd.ext import CounterAdjustable +from sfbd.ext import BSCAcquisition def getAux(pvs=None): if not pvs: @@ -95,7 +96,7 @@ class Dispersion: sensor1 = getBSChannels('SATBD02-DBPM.*:Y2$') sensor2 = getBSChannels('SATCB.*-RLLE-DSP:.*-VS$') self.sensor = sensor1+sensor2 - self.acq = [BSAcquisition(".",pgroup, default_channels=self.sensor)] + self.acq = [BSCAcquisition(".",pgroup, default_channels=self.sensor)] # auxiliar data to be read one self.aux = self.getRFCalibrationChannels(sensor2,'SATCL01-MBND100:ENERGY-OP') @@ -159,7 +160,7 @@ class Dispersion: def setup(self,scl = 1, Nsteps=5, Nsamples=5): val = self.adj.get_current_value(readback=False) dval = self.amp*scl - dval = 0 + dval = 0 ######## edit this self.N = Nsteps self.Ns= Nsamples self.values=np.linspace(val-dval,val+dval,num=self.N) @@ -179,10 +180,10 @@ class Dispersion: self.sc=self.scanner.ascan_list(self.adj,self.values, filename=self.branch,start_immediately = False, n_pulses=self.Ns,return_to_initial_values=True) - self.preaction() +# self.preaction() ###### self.sc.run() self.auxdata = getAux(self.aux) - self.postaction() +# self.postaction() ####### def stop(self): diff --git a/app/xtcavstabilizer.py b/app/xtcavstabilizer.py index 1e58ce1..830085f 100644 --- a/app/xtcavstabilizer.py +++ b/app/xtcavstabilizer.py @@ -2,18 +2,29 @@ import time import numpy as np from bstrd import BSCache +from epics import PV + class XTCAVStabilizer: """ Wrapper class to bundle all daq/io needed for stabilizing the XTCAV """ def __init__(self): + + # the PV + self.PVTCAV = PV('SATMA02-RMSM:SM-GET',connection_timeout=0.9, callback=self.stateChanged) + self.state=self.PVTCAV.value == 9 + + # the BS channels self.bs = BSCache(100000,10000) # 100 second timeout, size for 100 second data taken - self.channels = ['SATBD02-DBPM040:X2','SATMA02-RLLE-DSP:PHASE-VS'] + self.channels = ['SATBD02-DBPM040:X2','SATMA02-RLLE-DSP:PHASE-VS','SATBD02-DBPM040:X2-VALID'] + self.validation = self.channels[2] self.bs.get_vars(self.channels) # this starts the stream into the cache self.bs.stop() - + def stateChanged(self,pvname=None, value=0, **kws): + self.state = value == 9 + def terminate(self): print('Stopping BSStream Thread...') self.bs.stop() @@ -23,7 +34,7 @@ class XTCAVStabilizer: self.bs.flush() def read(self): - data=self.bs.__next__() - return data['pid'],data[self.channels[0]],data[self.channels[1]] # returns PID, BPM reading, TCAV Phase + return self.bs.__next__() + diff --git a/ext/__init__.py b/ext/__init__.py index 01b2db3..976402d 100644 --- a/ext/__init__.py +++ b/ext/__init__.py @@ -1,3 +1,4 @@ from .magnet import Magnet from .camacquisition import CamAcquisition from .counteradjustable import CounterAdjustable +from .bscacquisition import BSCAcquisition diff --git a/ext/bscacquisition.py b/ext/bscacquisition.py new file mode 100644 index 0000000..21495ee --- /dev/null +++ b/ext/bscacquisition.py @@ -0,0 +1,42 @@ +import zmq +import h5py + + +from bstrd import BSCache +from slic.core.acquisition.acquisition import Acquisition +from slic.core.acquisition import BSAcquisition, PVAcquisition + +# class using the BSQueue to avoid to reestablish a stream for each step. + +class BSCAcquisition(Acquisition): + + def release(self): + if not self.queue == None: + del self.queue + self.queue = None + + def _acquire(self, filename, channels=None, data_base_dir=None, scan_info=None, n_pulses=100, **kwargs): + if not hasattr(self,'queue'): + self.queue = getStream(channels) + elif not self.queue: + self.queue = getStream(channels) + + self.queue.flush() + print('Acquiring',n_pulses,'samples') + data = [] + for i in range(n_pulses): + data.append(self.queue.__next__()) + hid = h5py.File(filename,'w') + gid = hid.create_group(channels[0]) + for key in data[0].keys(): + gid.create_dataset(key, data = [rec[key].value for rec in data]) + hid.close() + + +def getStream(channels): + print('Generating stream with channels:',channels) + bs = BSCache(100000,10000) # 1 second time out, capazity for 100 second. + bs.get_vars(channels) + return bs + + diff --git a/interface/__init__.py b/interface/__init__.py index 3aa44be..8f3efde 100644 --- a/interface/__init__.py +++ b/interface/__init__.py @@ -1,4 +1,5 @@ from .snap import getSnap +from .snap import saveSnap from .save import saveDataset from .load import loadDataset from .elog import writeElog diff --git a/interface/snap.py b/interface/snap.py index 4b14b90..ddd2841 100644 --- a/interface/snap.py +++ b/interface/snap.py @@ -1,13 +1,12 @@ import numpy as np import yaml import os - +import datetime import epics # things to do: # 1. Read a snapshot file (not request file) -# 2. Save a snapshot file -# 3. add parameters and performance channels (e.g. AT photon energies) +# 2. add parameters and performance channels (e.g. AT photon energies) def parseSnapShotReqYAML(filename): # read the snapshot request file @@ -63,6 +62,18 @@ def getSnap(pvs=None): epics.ca.clear_cache() return ret +def saveSnap(pvs={},label="", comment = "generated by application"): + filename = datetime.datetime.now().strftime('/sf/data/applications/snapshot/SF_settings_%Y%m%d_%H%M%S.snap') + with open(filename,'w') as fid: + fid.write('#{"labels":["%s"],"comment":"%s", "machine_parms":{}, "save_time": 0.0, "req_file_name": "SF_settings.yaml"}\n' % (label,comment)) + for key in pvs.keys(): + if isinstance(pvs[key],int): + fid.write('%s,{"val": %d}\n' % (key,pvs[key])) + elif isinstance(pvs[key],float): + fid.write('%s,{"val": %f}\n' % (key,pvs[key])) + elif isinstance(pvs[key],str): + fid.write('%s,{"val": %s}\n' % (key,pvs[key])) + -- 2.49.1 From 10c7b37b991113fbff7ce2804f35cad290809f9f Mon Sep 17 00:00:00 2001 From: reiche Date: Fri, 7 Jul 2023 11:59:55 +0200 Subject: [PATCH 08/13] Modified BSCAcquisition, so that the Cache is an argument of the function _acquire --- app/dispersiontools.py | 16 ++++++------- ext/bscacquisition.py | 54 ++++++++++++++++++++---------------------- 2 files changed, 34 insertions(+), 36 deletions(-) diff --git a/app/dispersiontools.py b/app/dispersiontools.py index ff2b6be..1ad4230 100644 --- a/app/dispersiontools.py +++ b/app/dispersiontools.py @@ -90,13 +90,13 @@ class Dispersion: # adjustable self.adjSV = 'SATCB01-RSYS:SET-BEAM-PHASE' self.adjRB = 'SATCB01-RSYS:GET-BEAM-PHASE' - self.adj = PVAdjustable(self.adjSV,pvname_readback = self.adjRB, accuracy = 0.1) + self.adj = PVAdjustable(self.adjSV,pvname_readback = self.adjRB, accuracy = 0.1,ID = self.adjSV, name = "SATCB01-Linac") self.amp = 30 # the amplitude of the scan, which can be scaled # acquisition sensor1 = getBSChannels('SATBD02-DBPM.*:Y2$') sensor2 = getBSChannels('SATCB.*-RLLE-DSP:.*-VS$') self.sensor = sensor1+sensor2 - self.acq = [BSCAcquisition(".",pgroup, default_channels=self.sensor)] + self.acq = [BSAcquisition(".",pgroup, default_channels=self.sensor)] # auxiliar data to be read one self.aux = self.getRFCalibrationChannels(sensor2,'SATCL01-MBND100:ENERGY-OP') @@ -115,7 +115,7 @@ class Dispersion: # adjustable self.adjSV = 'S30:SET-E-GAIN-OP' self.adjRB = 'S30:GET-E-GAIN-OP' - self.adj = PVAdjustable(self.adjSV,pvname_readback = self.adjRB, accuracy = 0.1) + self.adj = PVAdjustable(self.adjSV,pvname_readback = self.adjRB, accuracy = 0.1,ID = self.adjSV, name = "Linac3") self.amp = 20 # the amplitude of the scan, which can be scaled # acquisition sensor1 = getBSChannels('SAR.*DBPM.*:[XY]1$') @@ -142,7 +142,7 @@ class Dispersion: # adjustable self.adjSV = 'S20:SET-E-GAIN-OP' self.adjRB = 'S20:GET-E-GAIN-OP' - self.adj = PVAdjustable(self.adjSV,pvname_readback = self.adjRB, accuracy = 0.1) + self.adj = PVAdjustable(self.adjSV,pvname_readback = self.adjRB, accuracy = 0.1,ID = self.adjSV, name = "Linac 2 and 3") # self.adj2SV = 'S30:SET-E-GAIN-OP' # self.adj2RB = 'S30:GET-E-GAIN-OP' # self.adj2 = PVAdjustable(self.adj2SV,pvname_readback = self.adj2RB, accuracy = 0.1) @@ -159,7 +159,7 @@ class Dispersion: def setup(self,scl = 1, Nsteps=5, Nsamples=5): val = self.adj.get_current_value(readback=False) - dval = self.amp*scl +# dval = self.amp*scl dval = 0 ######## edit this self.N = Nsteps self.Ns= Nsamples @@ -180,10 +180,10 @@ class Dispersion: self.sc=self.scanner.ascan_list(self.adj,self.values, filename=self.branch,start_immediately = False, n_pulses=self.Ns,return_to_initial_values=True) -# self.preaction() ###### + # self.preaction() ###### self.sc.run() - self.auxdata = getAux(self.aux) -# self.postaction() ####### + # self.auxdata = getAux(self.aux) + # self.postaction() ####### def stop(self): diff --git a/ext/bscacquisition.py b/ext/bscacquisition.py index 21495ee..837ac55 100644 --- a/ext/bscacquisition.py +++ b/ext/bscacquisition.py @@ -1,42 +1,40 @@ -import zmq + import h5py +import numpy as np - -from bstrd import BSCache from slic.core.acquisition.acquisition import Acquisition -from slic.core.acquisition import BSAcquisition, PVAcquisition + # class using the BSQueue to avoid to reestablish a stream for each step. class BSCAcquisition(Acquisition): - def release(self): - if not self.queue == None: - del self.queue - self.queue = None - def _acquire(self, filename, channels=None, data_base_dir=None, scan_info=None, n_pulses=100, **kwargs): - if not hasattr(self,'queue'): - self.queue = getStream(channels) - elif not self.queue: - self.queue = getStream(channels) - - self.queue.flush() - print('Acquiring',n_pulses,'samples') - data = [] + + queue =channels[0] # abusing interface since BSAcquisition assume a list of channels + + # allocating space + data={} + chns = queue.channels + for chn in chns: + data[chn]={'data':np.zeros((n_pulses))} + data['pulse_id']=np.zeros((n_pulses)) + + # clear the queue + queue.flush() for i in range(n_pulses): - data.append(self.queue.__next__()) + msg = queue.__next__() # pull data from cache + for chn in chns: + data[chn]['data'][i] = msg[chn] + data['pulse_id'][i]=msg['pid'] + + # write out the data file hid = h5py.File(filename,'w') - gid = hid.create_group(channels[0]) - for key in data[0].keys(): - gid.create_dataset(key, data = [rec[key].value for rec in data]) + hid.create_dataset('pulse_id', data = data['pulse_id']) + for chn in chns: + gid = hid.create_group(chn) + for key in data[chn].keys(): + gid.create_dataset(key, data = data[chn][key]) hid.close() -def getStream(channels): - print('Generating stream with channels:',channels) - bs = BSCache(100000,10000) # 1 second time out, capazity for 100 second. - bs.get_vars(channels) - return bs - - -- 2.49.1 From 25e717e1328203cf9a6c4201d0d79718fc313aa7 Mon Sep 17 00:00:00 2001 From: reiche Date: Tue, 25 Jul 2023 09:20:43 +0200 Subject: [PATCH 09/13] Initial debug of slic scan for the dispersion tool --- app/dispersiontools.py | 138 +++++++++++++++++++++++++++-------------- interface/slic.py | 23 ++++--- interface/snap.py | 1 + 3 files changed, 106 insertions(+), 56 deletions(-) diff --git a/app/dispersiontools.py b/app/dispersiontools.py index 1ad4230..5b28259 100644 --- a/app/dispersiontools.py +++ b/app/dispersiontools.py @@ -10,6 +10,8 @@ from slic.core.acquisition import BSAcquisition, PVAcquisition from slic.core.scanner import Scanner from sfbd.ext import CounterAdjustable from sfbd.ext import BSCAcquisition +from sfbd.interface import SlicScan +from bstrd import BSCache def getAux(pvs=None): if not pvs: @@ -19,7 +21,6 @@ def getAux(pvs=None): for i,pv in enumerate(pvs): if val[i]: # filter out None values ret[pv]=float(val[i]) - epics.ca.clear_cache() return ret @@ -30,21 +31,32 @@ def getBSChannels(regexp): if prog.match(bs['name']): res.append(bs['name']) return res + +def getRFCalibrationChannels(sensors,energy): + aux=[] + for sen in sensors: + if 'PHASE-VS' in sen: + aux.append(sen.replace('PHASE-VS','GET-VSUM-PHASE-OFFSET').replace('RLLE-DSP','RSYS')) + aux.append(sen.replace('PHASE-VS','GET-VSUM-AMPLT-SCALE').replace('RLLE-DSP','RSYS')) + aux.append(sen.replace('PHASE-VS','SM-GET').replace('RLLE-DSP','RMSM')) + aux.append(energy) + return aux class Dispersion: def __init__(self, branch = 'Aramis'): self.scanname = 'Dispersion' - self.branch = 'None' + self.branch = None + dirpath= datetime.datetime.now().strftime('measurements/%Y/%m/%d/slic_sfbd') self.basedir = '/sf/data/%s/%s' % (dirpath,self.scanname) - self.pgroup='%s/%s' % (dirpath,self.scanname) self.scandir='/sf/data/'+self.pgroup - self.setBranch(branch) - self.sc = None + + self.setBranch('Athos Dump') # enfore Athos dump settings for now self.Nsteps = 2 self.Nsamples = 1 + def setBranch(self,branch = 'Aramis'): @@ -56,29 +68,61 @@ class Dispersion: elif branch == 'Athos': self.setupAthos() else: - self.scanner = None + self.branch = None + + def setup(self,scl = 1, Nsteps=5, Nsamples=10): + # the setup is done from the main thread. Therefore I have to define the BSC here + self.N = Nsteps + self.Ns= Nsamples + self.scale = scl + # define stream + self.bsc = BSCache(100000,10000) # 100 second timeout, size for 100 second data taken + self.bsc.get_vars(self.sensor) # this starts the stream into the cache + + def scan(self): + # core routine to do all action. Will be a thread of the slic scanner wrapper + if not self.branch: return - # path is define in the various set-up procedures - self.scanner = Scanner(data_base_dir=self.basedir,scan_info_dir=self.basedir, - make_scan_sub_dir=True, - default_acquisitions=self.acq) + + # define acquisition channels wrapper for BSQueue + pgroup = '%s-%s' % (self.pgroup,self.branch) + acq = [BSCAcquisition(".",pgroup, default_channels=[self.bsc])] + + # define the scanner + self.scanner = Scanner(data_base_dir=self.basedir, # root directory of data location e.g. /sf/data/2023/02/02/Dispersion + scan_info_dir=self.basedir, # write also scan info there + make_scan_sub_dir=True, # put each scan in its own directory + default_acquisitions=acq) # adjustable to use + + # define adjustable + self.adj = PVAdjustable(self.adjSV,pvname_readback = self.adjRB, accuracy = 0.1,ID = self.adjSV, name = self.name) + val = self.adj.get_current_value(readback=False) +# dval = self.amp*scl +# self.values=np.linspace(val-dval,val+dval,num=self.N) + dval = 0 + values = np.linspace(val-dval,val+dval, num = self.N) + + # create scanner backend + self.sc=self.scanner.ascan_list(self.adj, values, # list of adjustables and their values + filename=self.branch, # relative directory for data + start_immediately = False, # wait for execution to performe pre-action items + n_pulses=self.Ns, # steps + return_to_initial_values=True)# return to initial values + + # self.preaction() ###### + self.sc.run() # run scan + # self.auxdata = getAux(self.aux) + # self.postaction() ####### + self.bsc.stop() # stop the queue + - def getRFCalibrationChannels(self,sensor2,energy): - aux=[] - for sen in sensor2: - if 'PHASE-VS' in sen: - aux.append(sen.replace('PHASE-VS','GET-VSUM-PHASE-OFFSET').replace('RLLE-DSP','RSYS')) - aux.append(sen.replace('PHASE-VS','GET-VSUM-AMPLT-SCALE').replace('RLLE-DSP','RSYS')) - aux.append(sen.replace('PHASE-VS','SM-GET').replace('RLLE-DSP','RMSM')) - aux.append(energy) - return aux - - + def setupAthosDump(self): + # pathes and name tags self.branch='Athos_Dump' - pgroup = '%s-%s' % (self.pgroup,self.branch) - self.path = '/sf/data/'+pgroup + self.name ='SATCB01-Linac' + # pre-scan item - needs an adjustment of the dipole current of about 2.3e-3 - to be implemented later. self.pre = {} self.pre['SFB_BEAM_DUMP_AT:ONOFF1']={'Val':0,'InitVal':0} @@ -87,18 +131,22 @@ class Dispersion: self.pre['SFB_ORBIT_SAT_%2.2d:ONOFF1' % i ]={'Val':0,'InitVal':0} for pv in self.pre.keys(): self.pre[pv]['adj']=PVAdjustable(pv) + # adjustable self.adjSV = 'SATCB01-RSYS:SET-BEAM-PHASE' self.adjRB = 'SATCB01-RSYS:GET-BEAM-PHASE' - self.adj = PVAdjustable(self.adjSV,pvname_readback = self.adjRB, accuracy = 0.1,ID = self.adjSV, name = "SATCB01-Linac") self.amp = 30 # the amplitude of the scan, which can be scaled + # acquisition sensor1 = getBSChannels('SATBD02-DBPM.*:Y2$') sensor2 = getBSChannels('SATCB.*-RLLE-DSP:.*-VS$') self.sensor = sensor1+sensor2 - self.acq = [BSAcquisition(".",pgroup, default_channels=self.sensor)] + # auxiliar data to be read one - self.aux = self.getRFCalibrationChannels(sensor2,'SATCL01-MBND100:ENERGY-OP') + self.aux = getRFCalibrationChannels(sensor2,'SATCL01-MBND100:ENERGY-OP') + + + def setupAramis(self): self.branch='Aramis' @@ -157,13 +205,6 @@ class Dispersion: self.aux = self.getRFCalibrationChannels(sensor2,'S10BC02-MBND100:ENERGY-OP') - def setup(self,scl = 1, Nsteps=5, Nsamples=5): - val = self.adj.get_current_value(readback=False) -# dval = self.amp*scl - dval = 0 ######## edit this - self.N = Nsteps - self.Ns= Nsamples - self.values=np.linspace(val-dval,val+dval,num=self.N) def preaction(self): @@ -175,39 +216,44 @@ class Dispersion: for key in self.pre.keys(): self.pre[key]['adj'].set_target_value(self.pre[key]['InitVal']) - - def scan(self): - self.sc=self.scanner.ascan_list(self.adj,self.values, - filename=self.branch,start_immediately = False, - n_pulses=self.Ns,return_to_initial_values=True) - # self.preaction() ###### - self.sc.run() - # self.auxdata = getAux(self.aux) - # self.postaction() ####### - - + def stop(self): if self.sc is None: return self.sc.stop() def running(self): + if self.sc is None: + return False return self.sc.running def status(self): + if self.sc is None: + return 0,0 si = self.sc.scan_info.to_dict() steps = 0 if 'scan_values' in si: - steps=len(si['scan_values']) + steps=len(si['scan_values']) return steps,self.N def info(self): + if self.sc is None: + return None return self.sc.scan_info.to_dict() - +#-------------------- +# for debugging +if __name__ == '__main__': + daq = Dispersion() + daq.setup(1,10,100) +# threaded execution + scanner=SlicScan() + scanner.start(daq,True) + + diff --git a/interface/slic.py b/interface/slic.py index c2924c9..395eb04 100644 --- a/interface/slic.py +++ b/interface/slic.py @@ -25,22 +25,15 @@ class SlicScan(QObject): self.data = None self.act = None self.snap = None + self.doSnap = False def start(self,daq,snap=False): self.clear() + self.doSnap = snap self.daq=daq Thread(target=self.Tmonitor).start() - self.startSnap(snap) - def startSnap(self,snap=False): - if snap: - Thread(target=self.Tsnap).start() - - def Tsnap(self): - self.snap = getSnap() - self.sigsnap.emit(True) - - def Tmonitor(self): + def Tmonitor(self): mythread = Thread(target=self.Tscanner).start() time.sleep(1) ostep = -1 @@ -58,10 +51,20 @@ class SlicScan(QObject): if hasattr(self.daq,'auxdata'): self.data.update(self.daq.auxdata) self.sigterm.emit(istep==nstep) + # if requested add snapshot acquisition + if self.doSnap: + self.startSnap() def Tscanner(self): self.daq.scan() + def startSnap(self): + Thread(target=self.Tsnap).start() + + def Tsnap(self): + self.snap = getSnap() + self.sigsnap.emit(True) + def stop(self): self.daq.stop() diff --git a/interface/snap.py b/interface/snap.py index ddd2841..c1f7b17 100644 --- a/interface/snap.py +++ b/interface/snap.py @@ -60,6 +60,7 @@ def getSnap(pvs=None): if val[i]: # filter out None values ret[pv]=float(val[i]) epics.ca.clear_cache() + return ret def saveSnap(pvs={},label="", comment = "generated by application"): -- 2.49.1 From fff0c810700ea5f4c5e06ac3830bd25b7ef06741 Mon Sep 17 00:00:00 2001 From: reiche Date: Wed, 26 Jul 2023 13:32:43 +0200 Subject: [PATCH 10/13] Debugging dispersion and xtcav app plugins --- app/dispersiontools.py | 83 ++++++++++++++++++++++----------------- app/runDispersionTools.sh | 5 +++ app/xtcavstabilizer.py | 13 +++++- interface/slic.py | 3 +- 4 files changed, 66 insertions(+), 38 deletions(-) create mode 100755 app/runDispersionTools.sh diff --git a/app/dispersiontools.py b/app/dispersiontools.py index 5b28259..e13bbcc 100644 --- a/app/dispersiontools.py +++ b/app/dispersiontools.py @@ -57,8 +57,6 @@ class Dispersion: self.Nsteps = 2 self.Nsamples = 1 - - def setBranch(self,branch = 'Aramis'): self.sc = None if branch == 'Athos Dump': @@ -97,29 +95,37 @@ class Dispersion: # define adjustable self.adj = PVAdjustable(self.adjSV,pvname_readback = self.adjRB, accuracy = 0.1,ID = self.adjSV, name = self.name) val = self.adj.get_current_value(readback=False) -# dval = self.amp*scl -# self.values=np.linspace(val-dval,val+dval,num=self.N) - dval = 0 - values = np.linspace(val-dval,val+dval, num = self.N) + if self.adj2SV: + self.adj2 = PVAdjustable(self.adj2SV,pvname_readback = self.adj2RB, accuracy = 0.1,ID = self.adjSV, name = self.name) + val2 = self.adj.get_current_value(readback=False) + dval = self.amp*scl * 0 # create scanner backend - self.sc=self.scanner.ascan_list(self.adj, values, # list of adjustables and their values - filename=self.branch, # relative directory for data - start_immediately = False, # wait for execution to performe pre-action items - n_pulses=self.Ns, # steps - return_to_initial_values=True)# return to initial values + if self.adj2SV: + self.sc=self.scanner.a2scan(self.adj, val-dval, val+dval, + self.adj2, val+dval, val-dval, + self.Ns, # steps + filename=self.branch, # relative directory for data + start_immediately = False, # wait for execution to performe pre-action items + return_to_initial_values=True) # return to initial values + else: + self.sc=self.scanner.ascan(self.adj, val-dval, val+dval, + self.Ns, # steps + filename=self.branch, # relative directory for data + start_immediately = False, # wait for execution to performe pre-action items + return_to_initial_values=True) # return to initial values - # self.preaction() ###### - self.sc.run() # run scan - # self.auxdata = getAux(self.aux) - # self.postaction() ####### + self.preaction() + self.sc.run() + self.auxdata = getAux(self.aux) + self.postaction() self.bsc.stop() # stop the queue +################################# +# definition of the individual branches - - def setupAthosDump(self): - # pathes and name tags + # branch and name tag self.branch='Athos_Dump' self.name ='SATCB01-Linac' @@ -135,6 +141,8 @@ class Dispersion: # adjustable self.adjSV = 'SATCB01-RSYS:SET-BEAM-PHASE' self.adjRB = 'SATCB01-RSYS:GET-BEAM-PHASE' + self.adj2SV = None + self.adj2RB = None self.amp = 30 # the amplitude of the scan, which can be scaled # acquisition @@ -145,13 +153,11 @@ class Dispersion: # auxiliar data to be read one self.aux = getRFCalibrationChannels(sensor2,'SATCL01-MBND100:ENERGY-OP') - - - def setupAramis(self): + # branch and name tag self.branch='Aramis' - pgroup = '%s-%s' % (self.pgroup,self.branch) - self.path = '/sf/data/'+pgroup + self.name = 'Linac3' + # pre-scan item self.pre = {} self.pre['SFB_BEAM_DUMP_AR:ONOFF1']={'Val':0,'InitVal':0} @@ -160,23 +166,28 @@ class Dispersion: self.pre['SFB_ORBIT_SAR:ONOFF1']={'Val':0,'InitVal':0} for pv in self.pre.keys(): self.pre[pv]['adj']=PVAdjustable(pv) + # adjustable self.adjSV = 'S30:SET-E-GAIN-OP' self.adjRB = 'S30:GET-E-GAIN-OP' - self.adj = PVAdjustable(self.adjSV,pvname_readback = self.adjRB, accuracy = 0.1,ID = self.adjSV, name = "Linac3") + self.adj2SV = None + self.adj2RB = None self.amp = 20 # the amplitude of the scan, which can be scaled + # acquisition sensor1 = getBSChannels('SAR.*DBPM.*:[XY]1$') sensor2 = getBSChannels('S[23].*-RLLE-DSP:.*-VS$') self.sensor = sensor1+sensor2 - self.acq = [BSAcquisition(".",pgroup, default_channels=self.sensor)] + # auxiliar data to be read one self.aux = self.getRFCalibrationChannels(sensor2,'S10BC02-MBND100:ENERGY-OP') + def setupAthos(self): - self.branch='Aramis' - pgroup = '%s-%s' % (self.pgroup,self.branch) - self.path = '/sf/data/'+pgroup + # branch and name tag + self.branch='Athos' + self.name = 'Linac2+3' + # pre-scan item self.pre = {} self.pre['SFB_BEAM_DUMP_AT:ONOFF1']={'Val':0,'InitVal':0} @@ -187,24 +198,25 @@ class Dispersion: self.pre['SFB_ORBIT_SAT_%2.2d:ONOFF1' % i ]={'Val':0,'InitVal':0} for pv in self.pre.keys(): self.pre[pv]['adj']=PVAdjustable(pv) + # adjustable self.adjSV = 'S20:SET-E-GAIN-OP' self.adjRB = 'S20:GET-E-GAIN-OP' - self.adj = PVAdjustable(self.adjSV,pvname_readback = self.adjRB, accuracy = 0.1,ID = self.adjSV, name = "Linac 2 and 3") -# self.adj2SV = 'S30:SET-E-GAIN-OP' -# self.adj2RB = 'S30:GET-E-GAIN-OP' -# self.adj2 = PVAdjustable(self.adj2SV,pvname_readback = self.adj2RB, accuracy = 0.1) -# self.adj = CounterAdjustable(self.adj1,self.adj2) # combine the two channels + self.adj2SV = 'S30:SET-E-GAIN-OP' # counter adjustable + self.adj2RB = 'S30:GET-E-GAIN-OP' self.amp = 10 # the amplitude of the scan, which can be scaled + # acquisition sensor1 = getBSChannels('SAT[SDC].*DBPM.*:[XY]2$') sensor2 = getBSChannels('S[2].*-RLLE-DSP:.*-VS$') self.sensor = sensor1+sensor2 - self.acq = [BSAcquisition(".",pgroup, default_channels=self.sensor)] + # auxiliar data to be read one self.aux = self.getRFCalibrationChannels(sensor2,'S10BC02-MBND100:ENERGY-OP') +######################### +# some basic interaction with the scan functionality def preaction(self): @@ -215,7 +227,6 @@ class Dispersion: def postaction(self): for key in self.pre.keys(): self.pre[key]['adj'].set_target_value(self.pre[key]['InitVal']) - def stop(self): if self.sc is None: @@ -243,7 +254,7 @@ class Dispersion: #-------------------- -# for debugging +# main implementation for debugging if __name__ == '__main__': diff --git a/app/runDispersionTools.sh b/app/runDispersionTools.sh new file mode 100755 index 0000000..973f8ad --- /dev/null +++ b/app/runDispersionTools.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +export PYTHONPATH=/sf/bd/packages/slic:/sf/bd/packages/bstrd:$PYTHONPATH +python dispersiontools.py + diff --git a/app/xtcavstabilizer.py b/app/xtcavstabilizer.py index 830085f..f053977 100644 --- a/app/xtcavstabilizer.py +++ b/app/xtcavstabilizer.py @@ -14,7 +14,9 @@ class XTCAVStabilizer: # the PV self.PVTCAV = PV('SATMA02-RMSM:SM-GET',connection_timeout=0.9, callback=self.stateChanged) self.state=self.PVTCAV.value == 9 - + self.PVVolt = PV('SATMA02-RSYS:SET-ACC-VOLT') + self.PVPhase = PV('SATMA02-RSYS:SET-BEAM-PHASE') + # the BS channels self.bs = BSCache(100000,10000) # 100 second timeout, size for 100 second data taken self.channels = ['SATBD02-DBPM040:X2','SATMA02-RLLE-DSP:PHASE-VS','SATBD02-DBPM040:X2-VALID'] @@ -22,6 +24,15 @@ class XTCAVStabilizer: self.bs.get_vars(self.channels) # this starts the stream into the cache self.bs.stop() + def getPhase(self): + return self.PVPhase.value + + def setPhase(self,phi): + self.PVPhase.set(phi) + + def getVoltage(self): + return self.PVVolt.value + def stateChanged(self,pvname=None, value=0, **kws): self.state = value == 9 diff --git a/interface/slic.py b/interface/slic.py index 395eb04..51edbe7 100644 --- a/interface/slic.py +++ b/interface/slic.py @@ -33,7 +33,7 @@ class SlicScan(QObject): self.daq=daq Thread(target=self.Tmonitor).start() - def Tmonitor(self): + def Tmonitor(self): mythread = Thread(target=self.Tscanner).start() time.sleep(1) ostep = -1 @@ -63,6 +63,7 @@ class SlicScan(QObject): def Tsnap(self): self.snap = getSnap() + print('Acquired snap') self.sigsnap.emit(True) def stop(self): -- 2.49.1 From 4ed4630b8db6cf872920a529cf2722071ca2cec0 Mon Sep 17 00:00:00 2001 From: Sven Reiche Date: Thu, 17 Aug 2023 13:53:05 +0200 Subject: [PATCH 11/13] After debugging the dispersion tool during machine development shift --- app/adaptiveorbit.py | 31 +++++++++----------- app/dispersiontools.py | 65 +++++++++++++++++++++++++++++++++++------- interface/slic.py | 6 ++-- interface/snap.py | 3 +- 4 files changed, 71 insertions(+), 34 deletions(-) diff --git a/app/adaptiveorbit.py b/app/adaptiveorbit.py index 3b847fe..7889ac0 100644 --- a/app/adaptiveorbit.py +++ b/app/adaptiveorbit.py @@ -27,11 +27,11 @@ class AdaptiveOrbit: idx = '070' if bpm == 5 or bpm ==14: idx='410' - self.ATchx.append('SATUN%2.2d-DBPM%s:X1' % (bpm,idx)) - self.ATchy.append('SATUN%2.2d-DBPM%s:Y1' % (bpm,idx)) + self.ATchx.append('SATUN%2.2d-DBPM%s:X2' % (bpm,idx)) + self.ATchy.append('SATUN%2.2d-DBPM%s:Y2' % (bpm,idx)) self.bsAT = self.initBSStream([self.ATch0]+self.ATchx+self.ATchy) self.pvAT = self.initPV(self.ATchx) - self.kickerAR = self.initPV(['SATMA01-MCRX610:I-SET','SATMA01-MCRY610:I-SET','SATUN05-MCRX420:I-SET','SATUN05-MCRY420:I-SET','SFB_ORBIT_SAT:ONOFF1']) + self.kickerAT = self.initPV(['SATMA01-MCRX610:I-SET','SATMA01-MCRY610:I-SET','SATUN05-MCRX420:I-SET','SATUN05-MCRY420:I-SET','SFB_ORBIT_SAT:ONOFF1']) # select first beamline self.isAramis = True @@ -39,14 +39,7 @@ class AdaptiveOrbit: def initBSStream(self,channels): print("Initializing BSstream") - bs = BSCache(100000,10000) # 1 second time out, capazity for 100 second. -# bs.stop() -# for cnl in channels[1:]: -# if not is_available(cnl): -# raise ValueError(f"BS-Channel {cbl} is not available") -# res = make_channel_config(cnl,None,None) -# bs.channels[res]=res -# bs.get_var(channels[0]) # this starts also the stream into the cache + bs = BSCache(100000,100000) # 1000 second time out, capazity for 1000 second. bs.get_vars(channels) return bs @@ -54,8 +47,11 @@ class AdaptiveOrbit: print("Initializing EPICS Channels") pvs = [] for x in chx: - pvs.append(PV(x.replace(':X1',':X-REF-FB'))) - pvs.append(PV(x.replace(':X1',':Y-REF-FB'))) + if ':X1' in x or ':X2' in x: + pvs.append(PV(x.replace(':X1',':X-REF-FB'))) + pvs.append(PV(x.replace(':X1',':Y-REF-FB'))) + else: + pvs.append(PV(x)) con = [pv.wait_for_connection(timeout=0.2) for pv in pvs] for i, val in enumerate(con): if val is False: @@ -78,7 +74,7 @@ class AdaptiveOrbit: if beamline == 'Aramis': self.isAramis = True else: - self.isAthos = True + self.isAramis = False # all routine for accessing the machine (read/write) @@ -100,9 +96,9 @@ class AdaptiveOrbit: if self.isAramis: for i in range(len(fbval)): self.pvAR[i].value = fbval[i] - return - for i in range(len(fbval)): - self.pvAT[i].value = fbval[i] + else: + for i in range(len(fbval)): + self.pvAT[i].value = fbval[i] def getPVNames(self): if self.isAramis: @@ -120,7 +116,6 @@ class AdaptiveOrbit: return [pv.value for pv in self.kickerAT] def setKicker(self,vals): - return if self.isAramis: for i,val in enumerate(vals): self.kickerAR[i].value = val diff --git a/app/dispersiontools.py b/app/dispersiontools.py index e13bbcc..c1789a8 100644 --- a/app/dispersiontools.py +++ b/app/dispersiontools.py @@ -53,9 +53,10 @@ class Dispersion: self.pgroup='%s/%s' % (dirpath,self.scanname) self.scandir='/sf/data/'+self.pgroup - self.setBranch('Athos Dump') # enfore Athos dump settings for now + self.setBranch(branch) # enfore Athos dump settings for now self.Nsteps = 2 self.Nsamples = 1 + self.bsc = None def setBranch(self,branch = 'Aramis'): self.sc = None @@ -65,8 +66,11 @@ class Dispersion: self.setupAramis() elif branch == 'Athos': self.setupAthos() + elif branch =='Bunch Compressor 2': + self.setupBC2() else: self.branch = None + return self.branch def setup(self,scl = 1, Nsteps=5, Nsamples=10): # the setup is done from the main thread. Therefore I have to define the BSC here @@ -74,8 +78,10 @@ class Dispersion: self.Ns= Nsamples self.scale = scl # define stream + print('Getting BSCache') self.bsc = BSCache(100000,10000) # 100 second timeout, size for 100 second data taken self.bsc.get_vars(self.sensor) # this starts the stream into the cache + print('Getting BSCache done') def scan(self): # core routine to do all action. Will be a thread of the slic scanner wrapper @@ -97,33 +103,70 @@ class Dispersion: val = self.adj.get_current_value(readback=False) if self.adj2SV: self.adj2 = PVAdjustable(self.adj2SV,pvname_readback = self.adj2RB, accuracy = 0.1,ID = self.adjSV, name = self.name) - val2 = self.adj.get_current_value(readback=False) - dval = self.amp*scl * 0 + val2 = self.adj2.get_current_value(readback=False) + dval = self.amp*self.scale + print(self.adjSV,' - Scan Range:',val-dval,'to',val+dval) + if self.adj2SV: + print(self.adj2SV,' - Scan Range:',val2+dval,'to',val2-dval) # create scanner backend if self.adj2SV: self.sc=self.scanner.a2scan(self.adj, val-dval, val+dval, - self.adj2, val+dval, val-dval, - self.Ns, # steps + self.adj2, val2+dval, val2-dval, + n_intervals = self.N-1, # steps + n_pulses = self.Ns, # samples filename=self.branch, # relative directory for data start_immediately = False, # wait for execution to performe pre-action items return_to_initial_values=True) # return to initial values else: self.sc=self.scanner.ascan(self.adj, val-dval, val+dval, - self.Ns, # steps + n_intervals = self.N-1, # steps + n_pulses = self.Ns, # samples filename=self.branch, # relative directory for data start_immediately = False, # wait for execution to performe pre-action items return_to_initial_values=True) # return to initial values + # get aux data first + self.auxdata = getAux(self.aux) self.preaction() self.sc.run() - self.auxdata = getAux(self.aux) self.postaction() self.bsc.stop() # stop the queue + + ################################# # definition of the individual branches + def setupBC2(self): + # branch and name tag + self.branch='Bunch Compressor 2' + self.name ='BC2' + + # pre-scan item - needs an adjustment of the dipole current of about 2.3e-3 - to be implemented later. + self.pre = {} + self.pre['SFB_COMPRESSION_BC2_AR:ONOFF1']={'Val':0,'InitVal':0} + self.pre['SFB_COMPRESSION_BC2_AR:ONOFF2']={'Val':0,'InitVal':0} + self.pre['SFB_ORBIT_S10:ONOFF1']={'Val':0,'InitVal':0} + for pv in self.pre.keys(): + self.pre[pv]['adj']=PVAdjustable(pv) + + # adjustable + self.adjSV = 'S10:SET-E-GAIN-OP' + self.adjRB = 'S10:GET-E-GAIN-OP' + self.adj2SV = 'S20:SET-E-GAIN-OP' # counter adjustable + self.adj2RB = 'S20:GET-E-GAIN-OP' + self.amp = 5 # the amplitude of the scan, which can be scaled + + + # acquisition + sensor1 = getBSChannels('S10[BM].*-DBPM.*:[XY]1$') + sensor2 = getBSChannels('S1.*-RLLE-DSP:.*-VS$') + self.sensor = sensor1+sensor2 + + # auxiliar data to be read one + self.aux = getRFCalibrationChannels(sensor2,'SINBC02-MBND100:ENERGY-OP') + def setupAthosDump(self): # branch and name tag self.branch='Athos_Dump' @@ -175,12 +218,12 @@ class Dispersion: self.amp = 20 # the amplitude of the scan, which can be scaled # acquisition - sensor1 = getBSChannels('SAR.*DBPM.*:[XY]1$') + sensor1 = getBSChannels('SAR[CMU].*DBPM.*:[XY]1$') sensor2 = getBSChannels('S[23].*-RLLE-DSP:.*-VS$') self.sensor = sensor1+sensor2 # auxiliar data to be read one - self.aux = self.getRFCalibrationChannels(sensor2,'S10BC02-MBND100:ENERGY-OP') + self.aux = getRFCalibrationChannels(sensor2,'S10BC02-MBND100:ENERGY-OP') def setupAthos(self): @@ -204,7 +247,7 @@ class Dispersion: self.adjRB = 'S20:GET-E-GAIN-OP' self.adj2SV = 'S30:SET-E-GAIN-OP' # counter adjustable self.adj2RB = 'S30:GET-E-GAIN-OP' - self.amp = 10 # the amplitude of the scan, which can be scaled + self.amp = 5 # the amplitude of the scan, which can be scaled # acquisition sensor1 = getBSChannels('SAT[SDC].*DBPM.*:[XY]2$') @@ -212,7 +255,7 @@ class Dispersion: self.sensor = sensor1+sensor2 # auxiliar data to be read one - self.aux = self.getRFCalibrationChannels(sensor2,'S10BC02-MBND100:ENERGY-OP') + self.aux = getRFCalibrationChannels(sensor2,'S10BC02-MBND100:ENERGY-OP') ######################### diff --git a/interface/slic.py b/interface/slic.py index 51edbe7..a37b814 100644 --- a/interface/slic.py +++ b/interface/slic.py @@ -31,10 +31,10 @@ class SlicScan(QObject): self.clear() self.doSnap = snap self.daq=daq - Thread(target=self.Tmonitor).start() + Thread(name='Scan-Monitor',target=self.Tmonitor).start() def Tmonitor(self): - mythread = Thread(target=self.Tscanner).start() + mythread = Thread(name='Slic-Scanner',target=self.Tscanner).start() time.sleep(1) ostep = -1 while(self.daq.running()): @@ -59,7 +59,7 @@ class SlicScan(QObject): self.daq.scan() def startSnap(self): - Thread(target=self.Tsnap).start() + Thread(name='Snap-Acquisition',target=self.Tsnap).start() def Tsnap(self): self.snap = getSnap() diff --git a/interface/snap.py b/interface/snap.py index c1f7b17..714504b 100644 --- a/interface/snap.py +++ b/interface/snap.py @@ -59,8 +59,7 @@ def getSnap(pvs=None): for i,pv in enumerate(pvs): if val[i]: # filter out None values ret[pv]=float(val[i]) - epics.ca.clear_cache() - +# epics.ca.clear_cache() return ret def saveSnap(pvs={},label="", comment = "generated by application"): -- 2.49.1 From a0b5ce4c58d3ac47dde244c8b5a1cbd3df7767d4 Mon Sep 17 00:00:00 2001 From: reiche Date: Tue, 9 Jan 2024 10:47:51 +0100 Subject: [PATCH 12/13] Adding server modules --- app/adaptiveorbit.py | 5 ++- template/ServerTemplate.py | 33 ++++++++++++++ util/__init__.py | 2 + util/serverbase.py | 91 ++++++++++++++++++++++++++++++++++++++ util/zmqbase.py | 66 +++++++++++++++++++++++++++ 5 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 template/ServerTemplate.py create mode 100644 util/serverbase.py create mode 100644 util/zmqbase.py diff --git a/app/adaptiveorbit.py b/app/adaptiveorbit.py index 7889ac0..af1f089 100644 --- a/app/adaptiveorbit.py +++ b/app/adaptiveorbit.py @@ -47,9 +47,12 @@ class AdaptiveOrbit: print("Initializing EPICS Channels") pvs = [] for x in chx: - if ':X1' in x or ':X2' in x: + if ':X1' in x: pvs.append(PV(x.replace(':X1',':X-REF-FB'))) pvs.append(PV(x.replace(':X1',':Y-REF-FB'))) + elif ':X2' in x: + pvs.append(PV(x.replace(':X2',':X-REF-FB'))) + pvs.append(PV(x.replace(':X2',':Y-REF-FB'))) else: pvs.append(PV(x)) con = [pv.wait_for_connection(timeout=0.2) for pv in pvs] diff --git a/template/ServerTemplate.py b/template/ServerTemplate.py new file mode 100644 index 0000000..69bc95c --- /dev/null +++ b/template/ServerTemplate.py @@ -0,0 +1,33 @@ + +import signal + +import ServerBase + +class ServerTemplate(ServerBase.ServerBase): + def __init__(self, PVroot = 'MyServer', debug = False): + self.version='1.0.0' + self.program ='Server Template' + super(ServerTemplate, self).__init__(PVroot,debug,'127.0.0.1', 5678) # last too numbers are the IP adress and port of watchdog + + # connect to the individual handler, which must have the function terminate() implemented + # each handler should be their own thread to not interfere with the main process-loop + self.handler={} + + def terminateSubThreads(self): + for subserver in self.handler.keys(): + self.handler[subserver].terminate() + +if __name__ == '__main__': + + debug = True + server = ServerTemplate('SF-BC-SERVER', debug) + signal.signal(signal.SIGTERM,server.terminate) + try: + server.run() + except KeyboardInterrupt: + server.terminate(None,None) + + + + + diff --git a/util/__init__.py b/util/__init__.py index e69de29..31dccc0 100644 --- a/util/__init__.py +++ b/util/__init__.py @@ -0,0 +1,2 @@ +from .serverbase import ServerBase +from .zmqbase import ZMQBase diff --git a/util/serverbase.py b/util/serverbase.py new file mode 100644 index 0000000..9fa7e0c --- /dev/null +++ b/util/serverbase.py @@ -0,0 +1,91 @@ +import sys +import signal +import os +import socket +import logging +import logging.handlers +from logging.handlers import RotatingFileHandler +from datetime import datetime +import time + +from epics import PV +import ZMQBase + +class ServerBase (ZMQBase): + def __init__(self, root = 'MyServer', debug = False, WDServer = '127.0.0.1', WDPort = 5678): + + super(ServerBase,self).__init__(WDServer,WDPort) + + self.debug = debug + self.root = root + self.suffix='' + if self.debug: + self.suffix='-SIMU' + self.host = socket.gethostname() + self.pid = os.getpid() # process ID + + # enabling logging + self.logfilename="/sf/data/applications/BD-SERVER/%s.log" % self.root + handler = RotatingFileHandler(filename=self.logfilename, + mode='a', + maxBytes=5 * 1024 * 1024, + backupCount=1, + delay=0) + if self.debug: + logging.basicConfig(level=logging.INFO, + format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s', + datefmt='%m-%d %H:%M:%S') + else: + logging.basicConfig(level=logging.INFO, + format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s', + datefmt='%m-%d %H:%M:%S', + handlers=[handler,]) + self.logger=logging.getLogger(self.program) + + # setting up ZMQ interface + self.ZMQServerInfo(self.root,self.host,self.pid) + + # individual channels of main thread + self.PVstop = PV('%s:STOP%s' % (self.root,self.suffix)) + self.PVstop.value = 0 + self.PVstop.add_callback(self.stop) + self.PVping = PV('%s:PING%s' % (self.root,self.suffix)) + self.PVlog = PV('%s:LOG%s' % (self.root,self.suffix)) + + def stop(self,pvname=None,value=None,**kws): + self.logger.info('PV:STOP triggered at %s' % datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + if value > 0: + self.running=False + + def start(self): + self.logger.info('Starting Server: %s at %s' % (self.root,datetime.now().strftime('%Y-%m-%d %H:%M:%S'))) + self.logger.info('PV Root: %s' % self.root + self.logger.info('Version: %s' % self.version) + self.logger.info('Host: %s' % self.host) + self.logger.info('PID: %d' % self.pid) + if self.debug: + self.logger.info('Debug Mode') + + def run(self): + self.start() + self.running=True + while self.running: + time.sleep(1) + if self.ZMQPoll(): + self.logger.info('Watchdog Server requested termination') + self.running = False + self.PVping.value = datetime.now().strftime('Last active at %Y-%m-%d %H:%M:%S') + self.terminate(None,None) + + def terminate(self,signum,frame): + # stopping any sub thread with the server specific function terminateSubThreads + self.terminateSubThreads() + self.logger.info('Terminating Server at %s' % datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + self.ZMQPoll('quit') # informing the watchdog + print('Bunch Compressor Server is quitting...') + sys.exit(0) + + + + + diff --git a/util/zmqbase.py b/util/zmqbase.py new file mode 100644 index 0000000..1e883d3 --- /dev/null +++ b/util/zmqbase.py @@ -0,0 +1,66 @@ +import zmq +import socket +import sys + +class ZMQBase: + def __init__(self, host='127.0.0.1',port = 5678): + + self.host=host + self.port = port + self.msg={'action':'','PV':'','host':'','pid':0} + + self.REQUEST_TIMEOUT = 500 + self.REQUEST_RETRIES = 2 + self.SERVER_ENDPOINT = "tcp://%s:%d" % (host,port) + self.serverIsOffline=False # assume that it is online + + def ZMQServerInfo(self,PVroot,host,pid): + self.msg['PV']=PVroot + self.msg['host']=host + self.msg['pid']=pid + + + def ZMQPoll(self,tag='ping'): + self.msg['action']=tag + context = zmq.Context() + client = context.socket(zmq.REQ) + client.connect(self.SERVER_ENDPOINT) + client.send_pyobj(self.msg) + + retries_left = self.REQUEST_RETRIES + while True: + if (client.poll(self.REQUEST_TIMEOUT) & zmq.POLLIN) != 0: + reply = client.recv_pyobj() + check = self.ZMQIdentifyReply(reply) + if self.serverIsOffline: + self.logger.info("Watchdog server came online") + self.serverIsOffline=False + if check: + return (reply['action'] == 'quit') + else: + self.logger.warning("Malformed reply from server") + continue + + retries_left -= 1 + # Socket is confused. Close and remove it. + client.setsockopt(zmq.LINGER, 0) + client.close() + if retries_left == 0: + if not self.serverIsOffline: + self.logger.info("Watchdog server seems to be offline") + self.serverIsOffline=True + return False + + # Create new connection + client = context.socket(zmq.REQ) + client.connect(self.SERVER_ENDPOINT) + client.send_pyobj(self.msg) + + + def ZMQIdentifyReply(self,reply): + for field in ['PV','host','pid']: + if not reply[field] == self.msg[field]: + return False + return True + + -- 2.49.1 From 36eacba0465ce4a1a69ada916b1598bd8a693ecf Mon Sep 17 00:00:00 2001 From: reiche Date: Tue, 9 Jan 2024 11:21:37 +0100 Subject: [PATCH 13/13] Corrected the ZMQ base class inherited by the ServerBase Class --- util/__init__.py | 4 +++- util/serverbase.py | 9 +++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/util/__init__.py b/util/__init__.py index 31dccc0..7d06df8 100644 --- a/util/__init__.py +++ b/util/__init__.py @@ -1,2 +1,4 @@ -from .serverbase import ServerBase from .zmqbase import ZMQBase +from .serverbase import ServerBase + + diff --git a/util/serverbase.py b/util/serverbase.py index 9fa7e0c..80bc3e4 100644 --- a/util/serverbase.py +++ b/util/serverbase.py @@ -9,9 +9,10 @@ from datetime import datetime import time from epics import PV -import ZMQBase +sys.path.append('/sf/bd/packages/sfbd') +from sfbd.util import ZMQBase -class ServerBase (ZMQBase): +class ServerBase(ZMQBase): def __init__(self, root = 'MyServer', debug = False, WDServer = '127.0.0.1', WDPort = 5678): super(ServerBase,self).__init__(WDServer,WDPort) @@ -53,13 +54,13 @@ class ServerBase (ZMQBase): self.PVlog = PV('%s:LOG%s' % (self.root,self.suffix)) def stop(self,pvname=None,value=None,**kws): - self.logger.info('PV:STOP triggered at %s' % datetime.now().strftime('%Y-%m-%d %H:%M:%S')) if value > 0: + self.logger.info('PV:STOP triggered at %s' % datetime.now().strftime('%Y-%m-%d %H:%M:%S')) self.running=False def start(self): self.logger.info('Starting Server: %s at %s' % (self.root,datetime.now().strftime('%Y-%m-%d %H:%M:%S'))) - self.logger.info('PV Root: %s' % self.root + self.logger.info('PV Root: %s' % self.root) self.logger.info('Version: %s' % self.version) self.logger.info('Host: %s' % self.host) self.logger.info('PID: %d' % self.pid) -- 2.49.1