fel_per is a float (10.0): tmove=10.0 gave wait_pulses=twait//tmove=1.0,
used to repeat the CTA lists ('can't multiply sequence by non-int').
tmove is now written as int when fel_per is whole, and wait_pulses is
cast to int.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
889 lines
36 KiB
Python
889 lines
36 KiB
Python
import logging,sys,os,socket,time
|
|
|
|
from math import ceil
|
|
_log=logging.getLogger(__name__)
|
|
|
|
class DataCollectionAbort(Exception):
|
|
"""Raised when acquisition configuration is invalid and the current scan should abort."""
|
|
|
|
LASER_DELAY_PV='SLAAR03-LTIM-PDLY:DELAYNS_SLOW' # fine laser delay in ns, negative: laser before the X-rays
|
|
MOTION_KEYS=('cnt', 'scale', 'dwell', 'tmove', 'twait', 'ssz', 'smv', 'sdelay', 'use_trf') # recorded motion parameters
|
|
|
|
def motion_delay_pulses(mode, param):
|
|
'''
|
|
FEL pulses from the trigger (laser/droplet) to the X-ray pulse on the same aperture, as defined by the
|
|
CTA sequences in Jungfrau.config:
|
|
mode 1,3,4: 0 (same pulse)
|
|
mode 5: twait//tmove
|
|
mode 6: ssz[0]*ssz[1] (nominal: probe pass in the same aperture order as the pump pass)
|
|
mode 7: ssz[0]*ssz[1]*(twait//tmove+1)+twait//tmove
|
|
(tmove, twait in ms for mode 5 and 7: shapepath uses round((tmove+twait)/fel_per), its 'shots' doc of mode 7 is wrong)
|
|
raises KeyError/ValueError/TypeError if a needed parameter is missing or invalid
|
|
'''
|
|
mode=int(mode)
|
|
if mode in (5, 7):
|
|
tmove=param['tmove']
|
|
wait=int(param['twait']//tmove) if tmove else 0
|
|
if mode==5:
|
|
return wait
|
|
sx, sy=param['ssz']
|
|
return int(sx)*int(sy)*(wait+1)+wait
|
|
if mode==6:
|
|
sx, sy=param['ssz']
|
|
return int(sx)*int(sy)
|
|
return 0
|
|
|
|
def motion_tmove(mode, fel_per_ms):
|
|
'move time in ms for mode 5 and 7, fixed to one FEL pulse (the CTA sequences assume it), else None'
|
|
try:
|
|
if int(mode) not in (5, 7):
|
|
return None
|
|
except (TypeError, ValueError):
|
|
return None
|
|
return int(fel_per_ms) if float(fel_per_ms).is_integer() else fel_per_ms # 10 not 10.0 in the parameters
|
|
|
|
def motion_run_pulses(mode, param, count):
|
|
'''
|
|
FEL pulses of a collection (length of the CTA sequences in Jungfrau.config x repetitions), count: grid count (nx, ny)
|
|
returns (pulses, warnings): pulses is None if the collection would abort (the warnings tell why)
|
|
'''
|
|
mode=int(mode)
|
|
nx, ny=map(int, count)
|
|
if mode==4:
|
|
return nx*ny, []
|
|
wait=0
|
|
if mode in (5, 7):
|
|
tmove=param['tmove']
|
|
wait=int(param['twait']//tmove) if tmove else 0
|
|
if mode==5:
|
|
return nx*ny*(wait+1), []
|
|
sx, sy=map(int, param['ssz'])
|
|
warn=[]
|
|
if sx<=0 or sx%2:
|
|
warn.append(f'ssz[0] ({sx}) must be even and positive')
|
|
if sx>0 and nx%sx:
|
|
warn.append(f'ssz[0] ({sx}) must divide the chip columns ({nx})')
|
|
if mode==6 and sy!=ny:
|
|
warn.append(f'mode 6: ssz[1] ({sy}) must be the chip rows ({ny})')
|
|
if mode==7 and sy>0 and ny%sy:
|
|
warn.append(f'ssz[1] ({sy}) must divide the chip rows ({ny})')
|
|
if warn:
|
|
return None, warn
|
|
grp=sx*sy
|
|
if mode==6:
|
|
return (nx//sx)*(ny//sy)*(2*grp+sx-2), []
|
|
return (ny//sy)*((nx//sx)*(2*grp*(wait+1)+sx-2)+sy), [] # mode 7
|
|
|
|
def total_delay_ns(pulses, fel_per_ms, laser_delay_ns):
|
|
'''laser to X-ray delay in ns (negative: laser before the X-rays)'''
|
|
return laser_delay_ns-pulses*fel_per_ms*1e6
|
|
|
|
def _plain(v):
|
|
'numpy values/arrays and tuples to plain python (json header)'
|
|
if hasattr(v, 'tolist'):
|
|
return v.tolist()
|
|
if isinstance(v, (tuple, list)):
|
|
return [_plain(x) for x in v]
|
|
return v
|
|
|
|
logging.getLogger('matplotlib').setLevel(logging.INFO)
|
|
|
|
if socket.gethostname()=='ganymede':
|
|
base=os.path.abspath(os.path.dirname(__file__))
|
|
sys.path.insert(0, os.path.abspath(os.path.join(base,'../PBSwissMX/python')))
|
|
sys.path.insert(0, os.path.abspath(os.path.join(base,'../../PBTools')))
|
|
else:
|
|
base=os.path.abspath(os.path.dirname(__file__))
|
|
sys.path.insert(0, os.path.join(base,'PBTools'))
|
|
sys.path.insert(0, os.path.join(base,'PBSwissMX/python'))
|
|
sys.path.insert(0, os.path.expanduser('/sf/cristallina/applications/slic/slic-package'))
|
|
sys.path.insert(0, os.path.expanduser('/sf/cristallina/applications/mx/swissmx_tools/jfj'))
|
|
|
|
|
|
from PyQt5.QtWidgets import (QApplication,)
|
|
from app_config import AppCfg #settings, option, toggle_option
|
|
|
|
import epics
|
|
from pbtools.misc.pp_comm import PPComm
|
|
from pbtools.misc.gather import Gather
|
|
import shapepath
|
|
|
|
try:
|
|
from slic.core.acquisition import SFAcquisition
|
|
from slic.core.acquisition.broker.tools import get_endstation
|
|
from slic.devices.timing.events import CTASequencer
|
|
from ctadaq import CTAAcquisition
|
|
from jfjoch_device import JFJ
|
|
except ImportError as e:
|
|
_log.warning(f'import failed: {e}')
|
|
|
|
|
|
class Shutter:
|
|
|
|
def __init__(self,mode=1):
|
|
self._mode=mode
|
|
|
|
@property
|
|
def sync_flag(self):
|
|
# read at each open/close: the deltatau sync_flag can be changed in the preferences while the app runs
|
|
return QApplication.instance()._cfg.value(AppCfg.DT_MISC)['sync_flag']
|
|
|
|
def open(self):
|
|
mode=self._mode
|
|
if mode==0:
|
|
_log.info('open simulated shutter')
|
|
elif mode==1:
|
|
# open laser shutter
|
|
#epics.caput("SLAAR31-LPSYS-ESC:LHX1_SHUT_OPEN", 1) '''think this shutter is no longer used'''
|
|
if epics.caget("SLAAR31-LPSYS-ESCIP23:ONOFF.RVAL") == 1:
|
|
epics.caput("SLAAR31-LPSYS-ESCOP0:ONOFF", 1)
|
|
_log.info('laser shutter opened')
|
|
|
|
if self.sync_flag==0: #if using cta, sets pulse_picker output to follow pulser 0
|
|
# open fast shutter
|
|
epics.caput("SARES30-LTIM01-EVR0:RearUniv0_SOURCE", 0)
|
|
epics.caput("SARES30-LTIM01-EVR0:RearUniv0_SOURCE2", 0)
|
|
epics.caput("SARES30-LTIM01-EVR0:RearUniv0_SNUMPD", 0)
|
|
epics.caput("SARES30-LTIM01-EVR0:RearUniv0_SNUMPD2", 0)
|
|
|
|
epics.caput("SARES30-LTIM01-EVR0:RearUniv0-Ena-SP", 1)
|
|
_log.info('fast shutter opened')
|
|
|
|
def close(self):
|
|
mode=self._mode
|
|
if mode==0:
|
|
_log.info('close simulated shutter')
|
|
elif mode==1:
|
|
|
|
# close laser shutter
|
|
if epics.caget("SLAAR31-LPSYS-ESCIP23:ONOFF.RVAL") == 1:
|
|
epics.caput("SLAAR31-LPSYS-ESCOP2:ONOFF", 1)
|
|
_log.info('laser shutter closed')
|
|
#epics.caput("SLAAR31-LPSYS-ESC:LHX1_SHUT_CLOSE", 1) '''think this shutter is no longer used'''
|
|
|
|
# close fast shutter
|
|
epics.caput("SARES30-LTIM01-EVR0:RearUniv0-Ena-SP", 0)
|
|
if self.sync_flag==0: #if using cta, sets pulse_picker output back to high low, could do this every time?
|
|
epics.caput("SARES30-LTIM01-EVR0:RearUniv0_SOURCE", 3)
|
|
epics.caput("SARES30-LTIM01-EVR0:RearUniv0_SOURCE2", 4)
|
|
epics.caput("SARES30-LTIM01-EVR0:RearUniv0_SNUMPD", 0)
|
|
epics.caput("SARES30-LTIM01-EVR0:RearUniv0_SNUMPD2", 0)
|
|
_log.info('fast shutter closed')
|
|
|
|
class Deltatau:
|
|
def __init__(self,sim=False):
|
|
app=QApplication.instance()
|
|
cfg=app._cfg
|
|
host=cfg.value(AppCfg.DT_HOST)
|
|
# sim=False;host='localhost:10001:10002' # this only moves motors during acquisition
|
|
if sim:
|
|
self._comm=comm=None
|
|
self._gather=gather=None
|
|
else:
|
|
hpp=host.split(':')
|
|
param={'host':hpp[0]}
|
|
if len(hpp)>1:
|
|
param['port']=int(hpp[1])
|
|
if len(hpp)>2:
|
|
param['fast_gather_port']=int(hpp[2])
|
|
_log.info(' -> ssh-tunneling PPComm({host}:{port} {host}:{fast_gather_port})'.format(**param))
|
|
try:
|
|
self._comm=comm=PPComm(**param,timeout=2.0)
|
|
self._gather=gather=Gather(comm)
|
|
except (socket.timeout,socket.gaierror) as e:
|
|
_log.critical(f'can not connect to deltatau:"{host}" -> {e}')
|
|
self._shapepath=sp=shapepath.ShapePath(comm, gather, verbose=0xff, sync_mode=1, sync_flag=3)
|
|
|
|
class Jungfrau:
|
|
def __init__(self,sim=False):
|
|
# setup slic parameters
|
|
if sim:
|
|
self._sim=True
|
|
return
|
|
try:
|
|
self._pv_pulse_id=epics.PV('SAR-EXPMX-EVR0:RX-PULSEID')
|
|
self._pv_pulse_id.connect()
|
|
except NameError as e:
|
|
_log.critical(f'Jungfrau not connected: {e}')
|
|
|
|
self.n_pulses_run = None
|
|
|
|
self.image_pulse_mask = None
|
|
self.images_per_cycle = None
|
|
self.pulses_per_cycle = None
|
|
|
|
self.motion_mode = None
|
|
|
|
self.block_size_to_slic = None
|
|
|
|
def _pulse_count_for_images(self, n_images):
|
|
if self.images_per_cycle is None or self.pulses_per_cycle is None:
|
|
raise ValueError("Pulse/image mapping not configured")
|
|
|
|
full_cycles, remainder = divmod(n_images, self.images_per_cycle)
|
|
pulses = full_cycles * self.pulses_per_cycle
|
|
if remainder == 0:
|
|
return pulses
|
|
|
|
count = 0
|
|
for idx, is_image in enumerate(self.image_pulse_mask):
|
|
if is_image:
|
|
count += 1
|
|
if count == remainder:
|
|
return pulses + idx + 1
|
|
|
|
raise ValueError(
|
|
f"Could not map {remainder} images into pulse mask of length {len(self.image_pulse_mask)}"
|
|
)
|
|
|
|
def _pulse_blocks_for_jf_files(self, jf_block_size):
|
|
"""Return either a constant raw pulse block size or a list of per-file pulse counts.
|
|
|
|
For mode 6/7 the raw pulse block size can vary between JF files because the
|
|
image-to-pulse mapping is not one-to-one. This method preserves exact
|
|
Jungfrau file boundaries by splitting pulses at the image counts used by JFJ.
|
|
"""
|
|
total_images = self.images
|
|
if total_images is None:
|
|
raise ValueError("Total image count is not configured")
|
|
if jf_block_size <= 0:
|
|
raise ValueError("JF block size must be positive")
|
|
|
|
file_sizes = []
|
|
full_files, remainder = divmod(total_images, jf_block_size)
|
|
file_sizes.extend([jf_block_size] * full_files)
|
|
if remainder:
|
|
file_sizes.append(remainder)
|
|
|
|
boundaries = []
|
|
current_images = 0
|
|
for file_size in file_sizes:
|
|
current_images += file_size
|
|
boundaries.append(self._pulse_count_for_images(current_images))
|
|
|
|
block_sizes = [boundaries[0]]
|
|
for prev, curr in zip(boundaries, boundaries[1:]):
|
|
block_sizes.append(curr - prev)
|
|
|
|
if len(set(block_sizes)) == 1:
|
|
return block_sizes[0]
|
|
return block_sizes
|
|
|
|
def config(self,**kwargs):
|
|
if getattr(self,'_sim',False):
|
|
_log.info(f'simulated')
|
|
return
|
|
app=QApplication.instance() #temproary fix, couldnt access these in function, maybe the bt above needs to be self.detectors ... etc
|
|
cfg=app._cfg
|
|
det = cfg.value(AppCfg.DAQ_DET)
|
|
#print("det", repr(det))
|
|
detectors = [det] if det.get("name") else None
|
|
bs_channels = cfg.value(AppCfg.DAQ_BS_CH)
|
|
pv_channels = cfg.value(AppCfg.DAQ_PV_CH)
|
|
loc=cfg.value(AppCfg.DAQ_LOC)
|
|
run=cfg.value(AppCfg.DAQ_RUN)
|
|
dt_misc = cfg.value(AppCfg.DT_MISC)
|
|
code_gen=kwargs.get('code_gen',0)
|
|
sync_mode=dt_misc['sync_mode']
|
|
sync_flag=dt_misc['sync_flag']
|
|
self.n_pulses_run = None
|
|
self.image_pulse_mask = None
|
|
self.images_per_cycle = None
|
|
self.pulses_per_cycle = None
|
|
self.block_size_to_slic = None
|
|
self.block_size_to_jungfrau = None
|
|
# Store mode-specific parameters for block size calculation
|
|
self.mode_wait_pulses = None
|
|
self.mode_ssz = None
|
|
self.mode_apperture_group_size = None
|
|
self.mode_number_of_apperture_groups = None
|
|
#print(kwargs)
|
|
motion_mode = kwargs['mode']
|
|
self.motion_mode = motion_mode
|
|
# recorded in the JFJ header (acquire): what actually runs, not what the main page shows
|
|
motion_param={k:_plain(kwargs[k]) for k in MOTION_KEYS if k in kwargs}
|
|
try:
|
|
delay_pulses=motion_delay_pulses(motion_mode, kwargs)
|
|
except (KeyError, ValueError, TypeError) as e:
|
|
_log.warning(f'delay pulses not defined for mode {motion_mode}: {e!r}')
|
|
delay_pulses=None
|
|
self.scan_meta={'chip_type':kwargs.get('chip_type', ''), 'motion_mode':int(motion_mode),
|
|
'motion_param':motion_param, 'delay_pulses':delay_pulses}
|
|
_log.info(f'scan meta: {self.scan_meta}')
|
|
|
|
if loc['jungfraujoch']:
|
|
try:
|
|
self.jfj = JFJ("http://sf-daq-2:8080")
|
|
self.detectors=None
|
|
_log.info(f'JungFrauJoch connected')
|
|
except NameError as e:
|
|
self.jfj = None
|
|
_log.critical(f'JungfrauJoch not connected: {e}')
|
|
is_scan_step=False
|
|
|
|
# JFJ comment block
|
|
# if loc['jungfraujoch']:
|
|
# self.jfj = None
|
|
# self.detectors = None
|
|
# _log.warning('JFJ bypassed - running in simulated connected mode')
|
|
# print('JFJ bypassed')
|
|
# else:
|
|
# self.jfj = None
|
|
# _log.info(f'JungFrauJoch is not in use')
|
|
# print('JungFrauJoch not in use')
|
|
# JFJ comment block
|
|
|
|
#if sync_flag==0:
|
|
if motion_mode == 1 or motion_mode == 3:
|
|
#grid_cnt=kwargs['grid']['count']
|
|
repetitions=int(kwargs['points'][:, 0].max())+1 #grid_cnt[0] #'x' or number of columns # if mode = 1 or 3
|
|
cta_multiplier=int(kwargs['points'][:, 1].max())+1#grid_cnt[1] #'y' or number of appertures in a column/number of rows
|
|
elif motion_mode == 4 or motion_mode == 5 or motion_mode == 6 or motion_mode == 7:
|
|
grid_cnt=kwargs['grid']['count']
|
|
repetitions=grid_cnt[0] #'x' or number of columns # if mode = 1 or 3
|
|
cta_multiplier=grid_cnt[1] #'y' or number of appertures in a column/number of rows
|
|
number_of_appertures = repetitions*cta_multiplier
|
|
cta=CTASequencer("SAR-CCTA-ESC")
|
|
cta.cfg.repetitions = repetitions
|
|
self.images=repetitions*cta_multiplier
|
|
on_off_ratio = int(run['num_of_on_to_1_off'])+1
|
|
if motion_mode==5:
|
|
if kwargs['tmove']==0:
|
|
wait_pulses=0
|
|
else:
|
|
wait_pulses=int(kwargs['twait']//kwargs['tmove']) # int: used to repeat lists
|
|
self.mode_wait_pulses = wait_pulses
|
|
#xray_seq=[0,]*wait_pulses+[1] # multiplier is proportional to wait_time i.e. 10 ms = 1, 20 ms =2, 30 ms =3.
|
|
#xray_sequence=[0,]*wait_pulses+[1]
|
|
#cta.seq[200]=xray_sequence*cta_multiplier # x-ray_shutter
|
|
#cta.seq[214] = [1,] + [0,] * (len(xray_sequence * cta_multiplier) -1)
|
|
#trigger_sequence = [1,] + [0,] * wait_pulses + [0,] * wait_pulses + [0]
|
|
#cta.seq[215]=trigger_sequence*(cta_multiplier//2) # laser_shutter
|
|
#image_label_sequence = [0,] * wait_pulses + [1] + [0,] * wait_pulses + [0]
|
|
#cta.seq[216]=image_label_sequence*(cta_multiplier//2)
|
|
#cta.seq[219] = xray_sequence * cta_multiplier
|
|
#on_off_ratio = 2 #1 on to 1 off which means 2 in the whole sequence
|
|
xray_sequence = [0,] * wait_pulses + [1,]
|
|
cta.seq[200] = xray_sequence * cta_multiplier # x-ray_shutter
|
|
cta.seq[214] = [1,] + [0,] * (len(xray_sequence * cta_multiplier) -1) #start motion
|
|
if run["triggered"]:
|
|
trigger_on = [1,] + [0,] * wait_pulses
|
|
trigger_off = [0,] + [0,] * wait_pulses
|
|
trigger_sequence = trigger_off + trigger_on * (on_off_ratio-1)
|
|
image_on = [0,] * wait_pulses + [1,]
|
|
image_label_sequence = [0,] * wait_pulses + [0,] + image_on * (on_off_ratio-1)
|
|
cta.seq[215] = trigger_sequence * int(cta_multiplier/on_off_ratio) # trigger (laser_shutter or droplet ejector)
|
|
cta.seq[216] = image_label_sequence * int(cta_multiplier/on_off_ratio) # image label (on or off)
|
|
else:
|
|
no_trigger_sequence = [0,] + [0,] * wait_pulses
|
|
cta.seq[215] = no_trigger_sequence * cta_multiplier # x-ray_shutter # trigger (laser_shutter or droplet ejector)
|
|
cta.seq[216] = no_trigger_sequence * cta_multiplier # x-ray_shutter # image label (on or off)1
|
|
cta.seq[219] = xray_sequence * cta_multiplier # detector trigger
|
|
self.n_pulses_run = len(cta.seq[200])*repetitions
|
|
|
|
#print(on_off_ratio,cta_multiplier,int(cta_multiplier/on_off_ratio),len(cta.seq[200]),len(cta.seq[214]),len(cta.seq[215]),len(cta.seq[216]),len(cta.seq[219]))
|
|
#self.motion_mode = 5
|
|
elif motion_mode==6:
|
|
ssz = kwargs['ssz']
|
|
#smv = kwargs['smv']
|
|
grid_cnt=kwargs['grid']['count']
|
|
repetitions=grid_cnt[0] #'x' or number of columns # if mode = 1 or 3
|
|
cta_multiplier=grid_cnt[1] #'y' or number of appertures in a column/number of rows
|
|
if ssz[0] <= 0 or ssz[0] % 2 != 0:
|
|
message = f"Mode 6 requires ssz[0] to be even and positive, got {ssz[0]}"
|
|
_log.warning(message)
|
|
raise DataCollectionAbort(message)
|
|
if repetitions % ssz[0] != 0:
|
|
message = f"Mode 6 requires repetitions ({repetitions}) to be divisible by ssz[0] ({ssz[0]})"
|
|
_log.warning(message)
|
|
raise DataCollectionAbort(message)
|
|
if ssz[1] != cta_multiplier:
|
|
message = f"Mode 6 requires ssz[1] == vertical chip length ({cta_multiplier}), got {ssz[1]}"
|
|
_log.warning(message)
|
|
raise DataCollectionAbort(message)
|
|
smv = ssz[0]-1 #max(ssz[0]-1, ssz[1]) commented out for fast camera tests, need a better work around for non-equal jumps in the CTA or motion script, currently avoid!
|
|
apperture_group_size = ssz[0] * ssz[1]
|
|
|
|
#number_of_apperture_groups = number_of_appertures // apperture_group_size
|
|
number_of_apperture_groups=(repetitions//ssz[0])*(cta_multiplier//ssz[1])
|
|
# Store mode 6 parameters for block size calculation
|
|
self.mode_ssz = ssz
|
|
self.mode_apperture_group_size = apperture_group_size
|
|
self.mode_number_of_apperture_groups = number_of_apperture_groups
|
|
_log.debug(f"number_of_apperture_groups = {number_of_apperture_groups}")
|
|
#print(f"actually want 162//8 * {162//7} (or 162//8 * 162//7) which = 460, so 460*8*7=25760")
|
|
transition_pulses = smv
|
|
#on_off_ratio = 2 #1 on to 1 off which means 2 in the whole sequence
|
|
xray_sequence = [0,] * apperture_group_size + [1,] * apperture_group_size + [0,] * (transition_pulses - 1)
|
|
cta.seq[200] = xray_sequence
|
|
cta.seq[214] = [1,] + [0,] * (apperture_group_size -1) + [0,] * apperture_group_size + [0,] * (transition_pulses - 1) #start motion
|
|
if run["triggered"]:
|
|
if on_off_ratio > apperture_group_size:
|
|
on_off_ratio = apperture_group_size
|
|
on_off_sequence = [1,] * (on_off_ratio - 1) + [0,]
|
|
cta.seq[215] = on_off_sequence * (apperture_group_size//on_off_ratio) + [0,] * apperture_group_size + [0,] * (transition_pulses -1) # Trigger 1:1
|
|
repeat_sequence_laser_closed = [1, ] + [0,] * (apperture_group_size - 1)
|
|
repeat_sequence_laser_open = [1,] + [0,] * (apperture_group_size - 1)
|
|
tranisition_sequence_laser_shut = [0,] * (transition_pulses - 1) #close shutter after final point before starting move...?
|
|
cta.seq[213] = repeat_sequence_laser_open + repeat_sequence_laser_closed + tranisition_sequence_laser_shut # Trigger 1:1
|
|
cta.seq[216] = [0,] * apperture_group_size + on_off_sequence * (apperture_group_size//on_off_ratio) + [0,] * (transition_pulses - 1) # Label image light dark 1:1 #change back to 1,0 for normal on off measurements
|
|
else:
|
|
cta.seq[215] = [0] * apperture_group_size * 2 + [0,] * (transition_pulses - 1) # trigger (laser_shutter or droplet ejector)
|
|
cta.seq[216] = [0] * apperture_group_size * 2 + [0,] * (transition_pulses -1) # image label (on or off)1
|
|
cta.seq[219] = xray_sequence # detector trigger
|
|
cta.cfg.repetitions = number_of_apperture_groups # number of repeitions is now number of times we repeat the loop
|
|
self.images=number_of_apperture_groups*apperture_group_size
|
|
_log.info(f"mode 6: length of sequence={len(cta.seq[200])} apperture in group={apperture_group_size} "
|
|
f"number_of_apperture_groups={number_of_apperture_groups}")
|
|
#print(f"n_pulses = {self.n_pulses_run} and n_appertures and images = {number_of_appertures}")
|
|
|
|
elif motion_mode==7:
|
|
ssz = kwargs['ssz']
|
|
self.mode_ssz = ssz
|
|
if ssz[0] <= 0 or ssz[0] % 2 != 0:
|
|
message = f"Mode 7 requires ssz[0] to be even and positive, got {ssz[0]}"
|
|
_log.warning(message)
|
|
raise DataCollectionAbort(message)
|
|
if repetitions % ssz[0] != 0:
|
|
message = f"Mode 7 requires repetitions ({repetitions}) to be divisible by ssz[0] ({ssz[0]})"
|
|
_log.warning(message)
|
|
raise DataCollectionAbort(message)
|
|
if cta_multiplier % ssz[1] != 0:
|
|
message = f"Mode 7 requires cta_multiplier ({cta_multiplier}) to be divisible by ssz[1] ({ssz[1]})"
|
|
_log.warning(message)
|
|
raise DataCollectionAbort(message)
|
|
if kwargs['tmove']==0:
|
|
wait_pulses=0
|
|
else:
|
|
wait_pulses=int(kwargs['twait']//kwargs['tmove']) # int: used to repeat lists
|
|
self.mode_wait_pulses = wait_pulses
|
|
|
|
apperture_group_size = ssz[0]*ssz[1]
|
|
sections_per_row = repetitions//ssz[0]
|
|
number_of_rows = cta_multiplier//ssz[1]
|
|
number_of_apperture_groups = sections_per_row*number_of_rows
|
|
|
|
# Store mode 7 parameters for block size calculation
|
|
self.mode_apperture_group_size = apperture_group_size
|
|
self.mode_number_of_apperture_groups = number_of_apperture_groups
|
|
|
|
# matches shapepath.py mode-7 defaults: tsx=ssz[0]-1 (section-to-section, same row),
|
|
# tsy=ssz[1] (row-to-row) - the pvt time (in shots) each transition move is queued with.
|
|
section_transition_pulses = ssz[0]-1
|
|
row_transition_pulses = ssz[1]
|
|
|
|
aperture_sequence = [0,]*wait_pulses+[1,]
|
|
xray_return_sequence = aperture_sequence*apperture_group_size
|
|
xray_dark_sequence = [0,]*len(xray_return_sequence)
|
|
section_transition_sequence = [0,]*(section_transition_pulses-1)
|
|
row_transition_sequence = [0,]*row_transition_pulses
|
|
|
|
# one section: dark (pump pass) + aperture pattern (return pass) + gap to next section;
|
|
# one row: sections_per_row sections + gap to next row; repeated number_of_rows times
|
|
# by the CTA sequencer directly, same pattern as mode 6.
|
|
section_xray_sequence = xray_dark_sequence+xray_return_sequence+section_transition_sequence
|
|
cta.seq[200] = section_xray_sequence*sections_per_row+row_transition_sequence
|
|
cta.seq[219] = cta.seq[200]
|
|
|
|
if run["triggered"]:
|
|
if on_off_ratio > apperture_group_size:
|
|
on_off_ratio = apperture_group_size
|
|
droplet_sequence = ([1,]+[0,]*wait_pulses)*(on_off_ratio-1)
|
|
no_droplet_sequence = [0,]+[0,]*wait_pulses
|
|
trigger_dark_sequence = (droplet_sequence+no_droplet_sequence)*(apperture_group_size//on_off_ratio)
|
|
trigger_return_sequence = [0,]*len(xray_return_sequence)
|
|
section_trigger_sequence = trigger_dark_sequence+trigger_return_sequence+section_transition_sequence
|
|
cta.seq[215] = section_trigger_sequence*sections_per_row+row_transition_sequence
|
|
|
|
image_on_sequence = ([0,]*wait_pulses+[1,])*(on_off_ratio-1)
|
|
image_off_sequence = [0,]*wait_pulses+[0,]
|
|
image_return_sequence = (image_on_sequence+image_off_sequence)*(apperture_group_size//on_off_ratio)
|
|
image_dark_sequence = [0,]*len(xray_return_sequence)
|
|
section_image_sequence = image_dark_sequence+image_return_sequence+section_transition_sequence
|
|
cta.seq[216] = section_image_sequence*sections_per_row+row_transition_sequence
|
|
else:
|
|
no_trigger_sequence = [0,]*len(cta.seq[200])
|
|
cta.seq[215] = no_trigger_sequence
|
|
cta.seq[216] = no_trigger_sequence
|
|
|
|
cta.seq[214] = [1,]+[0,]*(len(cta.seq[200])-1)
|
|
cta.cfg.repetitions = number_of_rows
|
|
self.images = number_of_apperture_groups*apperture_group_size
|
|
|
|
_log.info(f"mode 7: apperture_group_size={apperture_group_size} sections_per_row={sections_per_row} "
|
|
f"number_of_rows={number_of_rows} number_of_apperture_groups={number_of_apperture_groups} "
|
|
f"section_transition_pulses={section_transition_pulses} row_transition_pulses={row_transition_pulses} "
|
|
f"wait_pulses={wait_pulses} length of one row sequence={len(cta.seq[200])}")
|
|
|
|
else:
|
|
_log.info('not mode 5 or 6')
|
|
#need to add on off ratio
|
|
# no extra rows 1:1
|
|
#self.motion_mode = 4
|
|
cta.seq[214]=[1,]+[0,]*(cta_multiplier-1) #start motion
|
|
cta.seq[200]=[1,]*cta_multiplier #uncomment me for normal operation
|
|
#cta.seq[200]=[1,0,]*(cta_multiplier//2) # x-ray_shutter
|
|
cta.seq[219]=[1,]*cta_multiplier #trigger detector
|
|
if run["triggered"]:
|
|
#cta.seq[215]=[1,0,]*(cta_multiplier//2) # Trigger 1:1
|
|
_log.info('Oi John, you left the light on! From Martin')
|
|
cta.seq[215]=[1,]*cta_multiplier # shutter always open
|
|
_log.info('event 215 is on for every pulse in mode 4, and 216 is assuming light:dark is 1:1')
|
|
cta.seq[216]=[1,0,]*(cta_multiplier//2) # Label image light dark 1:1 #change back to 1,0 for normal on off measurements
|
|
self.n_pulses_run = len(cta.seq[200])*repetitions
|
|
|
|
|
|
cta.seq.upload()
|
|
|
|
try:
|
|
|
|
# --------------------------------------------------------
|
|
# detector exposure topology
|
|
# --------------------------------------------------------
|
|
|
|
self.image_pulse_mask = [
|
|
int(x) == 1 for x in cta.seq[219]
|
|
]
|
|
|
|
self.images_per_cycle = sum(
|
|
self.image_pulse_mask
|
|
)
|
|
|
|
self.pulses_per_cycle = len(
|
|
self.image_pulse_mask
|
|
)
|
|
|
|
if self.images_per_cycle == 0:
|
|
|
|
raise ValueError(
|
|
"No detector images found in cta.seq[219]"
|
|
)
|
|
|
|
jf_block_size = run['block_size']
|
|
|
|
# --------------------------------------------------------
|
|
# total dataset pulse count and block size mapping
|
|
# --------------------------------------------------------
|
|
|
|
self.n_pulses_run = self._pulse_count_for_images(self.images)
|
|
block_sizes = self._pulse_blocks_for_jf_files(jf_block_size)
|
|
self.block_size_to_slic = block_sizes
|
|
|
|
if motion_mode == 4:
|
|
# For continuous acquisition, image pulses and raw pulses are one-to-one.
|
|
self.block_size_to_slic = jf_block_size
|
|
|
|
_log.debug(
|
|
f"Mode {motion_mode}: exact BS pulse blocks for {jf_block_size} JF images = {self.block_size_to_slic}, "
|
|
f"images_per_cycle={self.images_per_cycle}, pulses_per_cycle={self.pulses_per_cycle}, "
|
|
f"n_pulses_run={self.n_pulses_run}"
|
|
)
|
|
|
|
_log.debug(
|
|
f"pulse/image mapping: motion_mode={motion_mode}, "
|
|
f"images_per_cycle={self.images_per_cycle}, "
|
|
f"pulses_per_cycle={self.pulses_per_cycle}, "
|
|
f"jf_block_size={jf_block_size}, "
|
|
f"block_size_to_slic={self.block_size_to_slic}, "
|
|
f"n_pulses_run={self.n_pulses_run}"
|
|
)
|
|
|
|
_log.info(
|
|
f"motion_mode={motion_mode} images={self.images} "
|
|
f"images_per_cycle={self.images_per_cycle} pulses_per_cycle={self.pulses_per_cycle} "
|
|
f"n_pulses_run={self.n_pulses_run} block_size_to_slic={self.block_size_to_slic}"
|
|
)
|
|
|
|
except Exception as e:
|
|
|
|
_log.warning(
|
|
f'failed to build pulse/image mapping: {e}'
|
|
)
|
|
|
|
self.image_pulse_mask = None
|
|
self.images_per_cycle = None
|
|
self.pulses_per_cycle = None
|
|
|
|
self.block_size_to_slic = None
|
|
|
|
self._daq=CTAAcquisition(cta, loc['end_station'], loc['p_group'], default_detectors=detectors,
|
|
default_channels=bs_channels,
|
|
default_pvs=pv_channels, rate_multiplicator=1, append_user_tag_to_data_dir=True)#, timeout=30)
|
|
#else:
|
|
# self._daq=SFAcquisition(
|
|
# loc['end_station'], loc['p_group'],
|
|
# default_detectors=detectors, default_channels=bs_channels, default_pvs=pv_channels,
|
|
# rate_multiplicator=1, append_user_tag_to_data_dir=True)
|
|
|
|
def acquire(self, n_pulses, wait=False):
|
|
if getattr(self,'_sim',False):
|
|
_log.info(f'simulated')
|
|
return
|
|
try:
|
|
daq=self._daq
|
|
except AttributeError:
|
|
_log.info(f'simulated')
|
|
return
|
|
app=QApplication.instance()
|
|
cfg=app._cfg
|
|
run=cfg.value(AppCfg.DAQ_RUN)
|
|
loc=cfg.value(AppCfg.DAQ_LOC)
|
|
data_proc=cfg.value(AppCfg.DATA_PROC)
|
|
daq_sample=cfg.value(AppCfg.DAQ_SAMPLE)
|
|
daq_uc=cfg.value(AppCfg.DAQ_UC)
|
|
try:
|
|
self._pulse_id_start=int(self._pv_pulse_id.value)
|
|
except TypeError as e:
|
|
_log.warning(f'failed to get _pulse_id_start: {e}')
|
|
if self.n_pulses_run:
|
|
n_pulses_run=self.n_pulses_run
|
|
_log.info(f'self.n_pulses_run {self.n_pulses_run} is equal to n_pulses_run {n_pulses_run}')
|
|
_log.info(f'number of images to JFJ is {self.images}')
|
|
else:
|
|
n_pulses_run = n_pulses + run['padding']
|
|
_log.info(f'self.n_pulses_run {self.n_pulses_run} is not equal to n_pulses_run {n_pulses_run}')
|
|
#n_pulses_run*=2 # comment me out please when not using 10 ms wait (for stop and go)
|
|
images_per_file = run['block_size']
|
|
sample_name = daq_sample['protein'] #protein_name
|
|
prefix = run['prefix']
|
|
user_tag = f'{sample_name}_{prefix}'
|
|
if self.jfj:
|
|
#daq_uc={"a":70,"b":70,"c":70,"alpha":90,"beta":90,"gamma":90}
|
|
unit_cell = {"a":daq_uc['a'],"b":daq_uc['b'], "c":daq_uc['c'],"alpha":daq_uc['alpha'], "beta":daq_uc['beta'], "gamma":daq_uc['gamma']}
|
|
geometry_file_location = loc['geometry_file_location']
|
|
trigger_event = loc['event_trigger']
|
|
trigger_flag = run['triggered']
|
|
laser_energy = run['laser_pulse_energy']
|
|
dt_misc = cfg.value(AppCfg.DT_MISC)
|
|
fel_per = dt_misc['fel_per']
|
|
try:
|
|
laser_delay_ns = float(epics.caget(LASER_DELAY_PV, timeout=1.0))
|
|
except (TypeError, ValueError):
|
|
_log.warning(f'laser delay: no value from {LASER_DELAY_PV}')
|
|
laser_delay_ns = None
|
|
scan_meta = getattr(self, 'scan_meta', {})
|
|
delay_pulses = scan_meta.get('delay_pulses')
|
|
if delay_pulses is None or laser_delay_ns is None:
|
|
trigger_delay = None
|
|
else:
|
|
trigger_delay = total_delay_ns(delay_pulses, fel_per, laser_delay_ns)
|
|
_log.info(f'delay: {delay_pulses} pulses x {fel_per} ms + laser {laser_delay_ns} ns = {trigger_delay} ns')
|
|
|
|
transmission_ATT53 = epics.caget("SARFE10-OATT053:UsrRec.TD")
|
|
transmission_ATT150 = epics.caget("SAROP31-OATA150:UsrRec.TD")
|
|
transmission = transmission_ATT53*transmission_ATT150
|
|
detector_distance_mm = round(epics.caget("SAR-EXPMX:MOT_DET_Z.RBV"),2)
|
|
|
|
run_number = self._daq.client.next_run()
|
|
is_scan_step = True
|
|
pgroup=loc['p_group']
|
|
try:
|
|
endstation = get_endstation()
|
|
_log.info(f'got endstation through slic:{endstation}')
|
|
except:
|
|
endstation = loc['end_station'] #'cristallina' #need cdoe form slic to direct this
|
|
_log.info(f'got endstation through gui:{endstation}')
|
|
jfj_file_prefix = f'sf/{endstation}/data/{pgroup}/raw/run{run_number:04}-{user_tag}/data/acq'
|
|
self.jfj_file_prefix = jfj_file_prefix
|
|
|
|
header_appendix = {}
|
|
for key in data_proc.keys():
|
|
value = data_proc[key]
|
|
header_appendix[f'{key}']=value
|
|
|
|
header_appendix['geometry_file_location'] = geometry_file_location
|
|
header_appendix['trigger_flag'] = trigger_flag
|
|
header_appendix['trigger_delay'] = trigger_delay # total delay in ns (same as total_delay_ns)
|
|
header_appendix['trigger_event'] = trigger_event
|
|
header_appendix['laser_pulse_energy'] = laser_energy
|
|
header_appendix['chip_type'] = scan_meta.get('chip_type', '')
|
|
header_appendix['motion_mode'] = scan_meta.get('motion_mode')
|
|
header_appendix['motion_param'] = scan_meta.get('motion_param', {})
|
|
header_appendix['fel_per_ms'] = fel_per
|
|
header_appendix['delay_pulses'] = delay_pulses
|
|
header_appendix['laser_delay_ns'] = laser_delay_ns
|
|
header_appendix['total_delay_ns'] = trigger_delay
|
|
header_appendix['file_prefix'] = f'run{run_number:04}-{user_tag}'
|
|
try:
|
|
self.jfj.acquire(
|
|
beam_x_pxl = loc['beam_x'],
|
|
beam_y_pxl = loc['beam_y'],
|
|
detector_distance_mm = detector_distance_mm,
|
|
incident_energy_keV = loc['incident_energy_kev'],
|
|
transmission = transmission,
|
|
sample_name = sample_name,
|
|
run_number = run_number,
|
|
file_prefix = jfj_file_prefix,
|
|
experiment_group = pgroup,
|
|
ntrigger = self.images,
|
|
images_per_file = images_per_file,
|
|
unit_cell = unit_cell,
|
|
space_group_number = daq_uc['space_group_number'],
|
|
header_appendix = header_appendix
|
|
)
|
|
except Exception as e:
|
|
|
|
_log.warning('error in priming JFJoch continuing, if not debugging motion be warned JFJoch not on or an error')
|
|
_log.error(f'JFJoch exception rasied as: {e}')
|
|
|
|
_log.info('JFJ primed')
|
|
else:
|
|
is_scan_step=False
|
|
|
|
if self.block_size_to_slic:
|
|
|
|
block_size = self.block_size_to_slic
|
|
|
|
_log.info(f'using exact pulse/image mapping: block_size_to_slic={block_size}')
|
|
|
|
else:
|
|
|
|
block_size = run['block_size']
|
|
|
|
_log.info(f'using simple block size={block_size}')
|
|
|
|
if type(self._daq) is CTAAcquisition:
|
|
|
|
if self.block_size_to_slic:
|
|
|
|
self._daq.acquire(
|
|
user_tag,
|
|
n_pulses=n_pulses_run,
|
|
n_block_size=self.block_size_to_slic,
|
|
wait=False,
|
|
cell_name=run['cell_name'],
|
|
is_scan_step=is_scan_step
|
|
)
|
|
|
|
else:
|
|
|
|
self._daq.acquire(
|
|
user_tag,
|
|
n_pulses=max(
|
|
n_pulses_run,
|
|
block_size
|
|
),
|
|
n_block_size=block_size,
|
|
wait=False,
|
|
cell_name=run['cell_name'],
|
|
is_scan_step=is_scan_step
|
|
)
|
|
|
|
else:
|
|
|
|
self._daq.acquire(
|
|
user_tag,
|
|
n_pulses=max(n_pulses_run, block_size),
|
|
n_repeat=ceil(n_pulses_run/block_size),
|
|
wait=False, cell_name=run['cell_name'],
|
|
is_scan_step=is_scan_step
|
|
)
|
|
|
|
cfg.setValue(AppCfg.DAQ_RUN,run)
|
|
is_scan_step=False
|
|
|
|
def stop_triggers(self):
|
|
'''
|
|
called on abort (also from the GUI thread): stop the CTA sequence (detector, x-ray shutter and laser
|
|
triggers) and end the BS/PV data retrieval at the current pulse id. The shutters are closed separately
|
|
by Shutter.close()
|
|
'''
|
|
if getattr(self,'_sim',False):
|
|
return
|
|
daq=getattr(self,'_daq',None)
|
|
if daq is None:
|
|
return
|
|
try:
|
|
stop_pid=int(self._pv_pulse_id.get())
|
|
except Exception as e:
|
|
stop_pid=None
|
|
_log.warning(f'abort: failed to get pulse id, no further data retrieval: {e}')
|
|
if hasattr(daq,'abort'): # CTAAcquisition
|
|
_log.warning(f'abort: stop CTA sequence, retrieve data up to pulse id {stop_pid}')
|
|
daq.abort(stop_pid)
|
|
else:
|
|
task=getattr(daq,'current_task',None)
|
|
if task is not None and task.stopper:
|
|
_log.warning('abort: stop DAQ task')
|
|
task.stopper()
|
|
|
|
def cancel(self, timeout=60.):
|
|
'''
|
|
called from the collection worker after an abort: cancel JFJ and wait (max. timeout seconds each)
|
|
for the data retrieval to finish and JFJ to be idle again
|
|
'''
|
|
if getattr(self,'_sim',False):
|
|
return
|
|
jfj=getattr(self,'jfj',None)
|
|
if jfj:
|
|
try:
|
|
jfj.api.cancel_post()
|
|
_log.warning('abort: JFJ cancel sent')
|
|
except Exception as e:
|
|
_log.error(f'abort: JFJ cancel failed: {e}')
|
|
task=getattr(getattr(self,'_daq',None),'current_task',None)
|
|
if task is not None:
|
|
task.thread.join(timeout)
|
|
if task.thread.is_alive():
|
|
_log.error(f'abort: data retrieval still running after {timeout}s')
|
|
else:
|
|
_log.info('abort: data retrieval finished')
|
|
if jfj:
|
|
t_end=time.time()+timeout
|
|
status=None
|
|
while time.time()<t_end:
|
|
try:
|
|
status=jfj.get_daq_status()
|
|
except Exception as e:
|
|
status=f'? ({e})'
|
|
if status=='Idle':
|
|
break
|
|
time.sleep(.5)
|
|
if status=='Idle':
|
|
_log.info('abort: JFJ idle')
|
|
else:
|
|
_log.error(f'abort: JFJ not idle after {timeout}s: {status}')
|
|
|
|
def gather_upload(self):
|
|
if getattr(self,'_sim',False):
|
|
_log.info(f'simulated')
|
|
return
|
|
try:
|
|
daq=self._daq
|
|
except AttributeError:
|
|
_log.info(f'simulated')
|
|
return
|
|
try:
|
|
self._pulse_id_end=int(self._pv_pulse_id.value)
|
|
except TypeError as e:
|
|
_log.warning(f'failed to get _pulse_id_start: {e}')
|
|
else:
|
|
_log.debug(f'pulse_id: {self._pulse_id_start}..{self._pulse_id_end}')
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import os
|
|
import argparse
|
|
logging.basicConfig(level=logging.DEBUG,format='%(name)s:%(levelname)s:%(module)s:%(lineno)d:%(funcName)s:%(message)s ')
|
|
if hostname=='ganymede':
|
|
# use EPICS locally
|
|
# os.environ['EPICS_CA_ADDR_LIST']='localhost'
|
|
# use EPICS if connected to ESC network
|
|
os.environ['EPICS_CA_ADDR_LIST']='129.129.244.255 sf-saresc-cagw.psi.ch:5062 sf-saresc-cagw.psi.ch:5066'
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--mode", "-m", help="qt test", type=lambda x:int(x, 0), default=0)
|
|
args = parser.parse_args()
|
|
|
|
_log.info('Arguments:{}'.format(args.__dict__))
|
|
app=QApplication(sys.argv)
|
|
app._cfg=cfg=AppCfg()
|
|
|
|
if args.mode &0x01:
|
|
dt=Deltatau()
|
|
if args.mode&0x02:
|
|
jf=Jungfrau()
|
|
jf.acquire(n_pulses=100, wait=True)
|
|
jf.gather_upload()
|
|
|