From 4be0f03437051b402cc4f40d01ae44540ca3be5a Mon Sep 17 00:00:00 2001 From: John Henry Beale Date: Mon, 19 Oct 2026 08:24:38 +0200 Subject: [PATCH] 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 --- app_config.py | 8 ++++-- psi_device.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++++--- swissmx.py | 2 ++ 3 files changed, 79 insertions(+), 5 deletions(-) diff --git a/app_config.py b/app_config.py index f0530f9..9d1f63f 100644 --- a/app_config.py +++ b/app_config.py @@ -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)}, diff --git a/psi_device.py b/psi_device.py index 41db50e..77bc426 100644 --- a/psi_device.py +++ b/psi_device.py @@ -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( diff --git a/swissmx.py b/swissmx.py index e481b6c..3d03011 100755 --- a/swissmx.py +++ b/swissmx.py @@ -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)