record chip type, motion mode and laser delay in the JFJ header

- header_appendix: chip_type, motion_mode, motion_param, fel_per_ms,
  delay_pulses, laser_delay_ns and total_delay_ns, taken from the job
  that actually runs (Jungfrau.config), not from the GUI
- laser delay read from SLAAR03-LTIM-PDLY:DELAYNS_SLOW (ns, negative:
  laser before the X-rays); total = laser - delay_pulses * fel_per;
  trigger_delay now holds the total delay
- motion_delay_pulses(): trigger to X-ray pulses per mode, same rules as
  the CTA sequences (mode 6/7 nominal)
- remove the unused manual laser_delay from the run parameters
- fix laser_pulse_energy, which was written with the trigger delay

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
John Henry BealeandClaude Opus 5.5 committed 2026-10-19 08:24:38 +02:00
1 parent a2873056c3
commit 4be0f03437
3 files changed
+79 -5

No files matched your search

+6 -2
View File
@@ -177,7 +177,6 @@ class AppCfg(QSettings):
'padding':10,
'cell_name' : 'na',
'triggered': False,
'laser_delay': '',
'laser_pulse_energy': '',
'num_of_on_to_1_off': '1',
'block_size':1000,
@@ -239,6 +238,12 @@ class AppCfg(QSettings):
_log.warning(f'post sample tube: absolute out positions created from the out deltas: {pst}')
self.setValue(AppCfg.DFT_POS_PST, pst)
# laser_delay was a manual entry (never used): the delay is now read from the laser delay PV
run=self.value(AppCfg.DAQ_RUN)
if run and 'laser_delay' in run:
del run['laser_delay']
self.setValue(AppCfg.DAQ_RUN, run)
#if AppCfg.ACTIVATE_PULSE_PICKER not in keys:
# self.setValue(AppCfg.ACTIVATE_PULSE_PICKER, False)
#if AppCfg.SKIP_ESCAPE_TRANSITIONS_IF_SAFE not in keys:
@@ -522,7 +527,6 @@ verbose bits:
{'name':'prefix', 'value':daq_run['prefix'], 'type':'str'},
{'name':'padding', 'value':daq_run['padding'], 'type':'int'}, #at some point move to old_daq_config
{'name':'cell_name', 'value':daq_run['cell_name'], 'type':'str'}, #at some point move to old_daq_config
{'name':'laser_delay', 'value':daq_run['laser_delay'], 'type':'str'},
{'name':'laser_pulse_energy', 'value':daq_run['laser_pulse_energy'], 'type':'str'},
{'name':'triggered', 'value':daq_run['triggered'], 'type':'bool'},
{'name':'num_of_on_to_1_off', 'value':daq_run['num_of_on_to_1_off'], 'type':'int', 'limits':(1, 6)},
+71 -3
View File
@@ -6,6 +6,44 @@ _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 (nominal, as mode 6)
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 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':
@@ -220,6 +258,16 @@ class Jungfrau:
#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:
@@ -563,8 +611,21 @@ class Jungfrau:
geometry_file_location = loc['geometry_file_location']
trigger_event = loc['event_trigger']
trigger_flag = run['triggered']
trigger_delay = run['laser_delay']
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")
@@ -590,9 +651,16 @@ class Jungfrau:
header_appendix['geometry_file_location'] = geometry_file_location
header_appendix['trigger_flag'] = trigger_flag
header_appendix['trigger_delay'] = trigger_delay
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'] = trigger_delay
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(
+2
View File
@@ -1593,6 +1593,7 @@ class WndSwissMx(QMainWindow, Ui_MainWindow):
except AttributeError as e:
_log.warning(f'no scan parameters for object->skipped:{go}')
continue
param['chip_type']=getattr(go, '_chip_type', t.__name__) # recorded in the JFJ header
vb=self.vb
grp=self._goTracked
mft=self._moduleFixTarget
@@ -2262,6 +2263,7 @@ object settings:
go=UsrGO.Path((fx-sz[0]/2, fy-sz[1]/2), sz, gp.points, fiducial, fidScl,**param)
else:
_log.error('index not handeled')
go._chip_type=mft._cbType.currentText() # recorded in the JFJ header
grp=self._goTracked
grp.addItem(go)