def set(self, val): if self._set_st is not None: raise RuntimeError(f'trying to set {self.name}' ' while a set is in progress') cmd_map = {self.open_str: self.open_cmd, self.close_str: self.close_cmd} target_map = {self.open_str: self.open_val, self.close_str: self.close_val} cmd_sig = cmd_map[val] target_val = target_map[val] st = DeviceStatus(self) if self.status.get() == target_val: st._finished() return st self._set_st = st print(self.name, val, id(st)) enums = self.status.enum_strs def shutter_cb(value, timestamp, **kwargs): value = enums[int(value)] if value == target_val: self._set_st = None self.status.clear_sub(shutter_cb) st._finished() cmd_enums = cmd_sig.enum_strs count = 0 def cmd_retry_cb(value, timestamp, **kwargs): nonlocal count value = cmd_enums[int(value)] count += 1 if count > self.MAX_ATTEMPTS: cmd_sig.clear_sub(cmd_retry_cb) self._set_st = None self.status.clear_sub(shutter_cb) st._finished(success=False) if value == 'None': if not st.done: time.sleep(self.RETRY_PERIOD) cmd_sig.set(1) ts = datetime.datetime.fromtimestamp(timestamp) \ .strftime(_time_fmtstr) if count > 2: msg = '** ({}) Had to reactuate shutter while {}ing' print(msg.format(ts, val if val != 'Close' else val[:-1])) else: cmd_sig.clear_sub(cmd_retry_cb) cmd_sig.subscribe(cmd_retry_cb, run=False) self.status.subscribe(shutter_cb) cmd_sig.set(1) return st
class SaturnSoftTrigger(BlueskyInterface): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._status = None self._acquisition_signal = self.mca.erase_start self.stage_sigs[self.mca.stop_signal] = 1 self.stage_sigs[self.dxp.preset_mode] = 'Real time' #self.stage_sigs[self.dxp.preset_mode] = 'Live time' self._count_signal = self.mca.preset_real_time self._count_time = None def stage(self): if self._count_time is not None: self.stage_sigs[self._count_signal] = self._count_time super().stage() def unstage(self): try: super().unstage() finally: if self._count_signal in self.stage_sigs: del self.stage_sigs[self._count_signal] self._count_time = None def trigger(self): "Trigger one acquisition." if self._staged != Staged.yes: raise RuntimeError("This detector is not ready to trigger." "Call the stage() method before triggering.") self._status = DeviceStatus(self) self._acquisition_signal.put(1, callback=self._acquisition_done) return self._status def _acquisition_done(self, **kwargs): '''pyepics callback for when put completion finishes''' if self._status is not None: self._status._finished() self._status = None @property def count_time(self): '''Exposure time, as set by bluesky''' return self._count_time @count_time.setter def count_time(self, count_time): self._count_time = count_time
def trigger(self): status = DeviceStatus(self) if self.status: status._finished() else: self._statuses.append(status) if not status.done: logger.warning('---') logger.warning('Waiting for beam status to change...') logger.warning('---') return status
class SaturnSoftTrigger(BlueskyInterface): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._status = None self._acquisition_signal = self.mca.erase_start self.stage_sigs[self.mca.stop_signal] = 1 self.stage_sigs[self.dxp.preset_mode] = 'Real time' self._count_signal = self.mca.preset_real_time self._count_time = None def stage(self): if self._count_time is not None: self.stage_sigs[self._count_signal] = self._count_time super().stage() def unstage(self): try: super().unstage() finally: if self._count_signal in self.stage_sigs: del self.stage_sigs[self._count_signal] self._count_time = None def trigger(self): "Trigger one acquisition." if self._staged != Staged.yes: raise RuntimeError("This detector is not ready to trigger." "Call the stage() method before triggering.") self._status = DeviceStatus(self) self._acquisition_signal.put(1, callback=self._acquisition_done) return self._status def _acquisition_done(self, **kwargs): '''pyepics callback for when put completion finishes''' if self._status is not None: self._status._finished() self._status = None @property def count_time(self): '''Exposure time, as set by bluesky''' return self._count_time @count_time.setter def count_time(self, count_time): self._count_time = count_time
def wait_until_settled(self, timeout=None, timeout_fail=False): """ plan: wait for controller signal to reach target within tolerance """ # see: https://stackoverflow.com/questions/2829329/catch-a-threads-exception-in-the-caller-thread-in-python t0 = time.time() _st = DeviceStatus(self.signal) if self.settled: # just in case signal already at target _st._finished(success=True) else: started = False def changing_cb(*args, **kwargs): if started and self.settled: _st._finished(success=True) token = self.signal.subscribe(changing_cb) started = True report = 0 while not _st.done and not self.settled: elapsed = time.time() - t0 if timeout is not None and elapsed > timeout: _st._finished(success=self.settled) msg = f"{self.controller_name} Timeout after {elapsed:.2f}s" msg += f", target {self.target.get():.2f}{self.units.get()}" msg += f", now {self.signal.get():.2f}{self.units.get()}" print(msg) if timeout_fail: raise TimeoutError(msg) continue if elapsed >= report: report += self.report_interval_s msg = f"Waiting {elapsed:.1f}s" msg += f" to reach {self.target.get():.2f}{self.units.get()}" msg += f", now {self.signal.get():.2f}{self.units.get()}" print(msg) yield from bps.sleep(self.poll_s) self.signal.unsubscribe(token) self.record_signal() elapsed = time.time() - t0 print(f"Total time: {elapsed:.3f}s, settled:{_st.success}")
def kickoff(self): self._running_status = running_status = DeviceStatus(self) holding_status = DeviceStatus(self) if self.hold.get(): def state_cb(value, old_value, **kwargs): if old_value != 2 and value == 2: holding_status._finished() self.state.clear_sub(state_cb) self.state.subscribe(state_cb, run=False) else: holding_status._finished() self.go.put(1, wait=True) def running_cb(value, old_value, obj, **kwargs): if old_value == 1 and value == 0: obj.clear_sub(running_cb) running_status._finished() self.running.subscribe(running_cb, run=False) return holding_status
def kickoff(self): self._running_status = running_status = DeviceStatus(self) holding_status = DeviceStatus(self) if self.hold.get(): def state_cb(value, old_value, **kwargs): if old_value != 2 and value == 2: holding_status._finished() self.state.clear_sub(state_cb) self.state.subscribe(state_cb, run=False) else: holding_status._finished() self.go.put(1, wait=True) def running_cb(value, old_value, obj, **kwargs): if old_value == 1 and value == 0: obj.clear_sub(running_cb) running_status._finished() self.running.subscribe(running_cb, run=False) return holding_status
def trigger(self): # Re-implement this to trigger as desired in bluesky status = DeviceStatus(self) status._finished() return status
def sscan_1D( sscan, poll_delay_s=0.001, phase_timeout_s = 60.0, running_stream="primary", final_array_stream=None, device_settings_stream="settings", md=None): """ simple 1-D scan using EPICS synApps sscan record assumes the sscan record has already been setup properly for a scan PARAMETERS sscan : Device one EPICS sscan record (instance of `apstools.synApps.sscanRecord`) running_stream : str or `None` (default: ``"primary"``) Name of document stream to write positioners and detectors data made available while the sscan is running. This is typically the scan data, row by row. If set to `None`, this stream will not be written. final_array_stream : str or `None` (default: ``None``) Name of document stream to write positioners and detectors data posted *after* the sscan has ended. If set to `None`, this stream will not be written. device_settings_stream : str or `None` (default: ``"settings"``) Name of document stream to write *settings* of the sscan device. This is all the information returned by ``sscan.read()``. If set to `None`, this stream will not be written. poll_delay_s : float (default: 0.001 seconds) How long to sleep during each polling loop while collecting interim data values and waiting for sscan to complete. Must be a number between zero and 0.1 seconds. phase_timeout_s : float (default: 60 seconds) How long to wait after last update of the ``sscan.FAZE``. When scanning, we expect the scan phase to update regularly as positioners move and detectors are triggered. If the scan hangs for some reason, this is a way to end the plan early. To cancel this feature, set it to ``None``. NOTE about the document stream names Make certain the names for the document streams are different from each other. If you make them all the same (such as ``primary``), you will have difficulty when reading your data later on. *Don't cross the streams!* EXAMPLE Assume that the chosen sscan record has already been setup. from apstools.devices import sscanDevice scans = sscanDevice(P, name="scans") from apstools.plans import sscan_1D RE(sscan_1D(scans.scan1), md=dict(purpose="demo")) """ global new_data, inactive_deadline if not (0 <= poll_delay_s <= 0.1): raise ValueError( "poll_delay_s must be a number between 0 and 0.1," f" received {poll_delay_s}") t0 = time.time() sscan_status = DeviceStatus(sscan.execute_scan) started = False new_data = False inactive_deadline = time.time() if phase_timeout_s is not None: inactive_deadline += phase_timeout_s def execute_cb(value, timestamp, **kwargs): """watch for sscan to complete""" if started and value in (0, "IDLE"): sscan_status._finished() sscan.execute_scan.unsubscribe_all() sscan.scan_phase.unsubscribe_all() def phase_cb(value, timestamp, **kwargs): """watch for new data""" global new_data, inactive_deadline if phase_timeout_s is not None: inactive_deadline = time.time() + phase_timeout_s if value in (15, "RECORD SCALAR DATA"): new_data = True # set flag for main plan # acquire only the channels with non-empty configuration in EPICS sscan.select_channels() # pre-identify the configured channels sscan_data_objects = _get_sscan_data_objects(sscan) # watch for sscan to complete sscan.execute_scan.subscribe(execute_cb) # watch for new data to be read out sscan.scan_phase.subscribe(phase_cb) _md = dict(plan_name="sscan_1D") _md.update(md or {}) yield from bps.open_run(_md) # start data collection yield from bps.mv(sscan.execute_scan, 1) # start sscan started = True # collect and emit data, wait for sscan to end while not sscan_status.done or new_data: if new_data and running_stream is not None: yield from bps.create(running_stream) for _k, obj in sscan_data_objects.items(): yield from bps.read(obj) yield from bps.save() new_data = False if phase_timeout_s is not None and time.time() > inactive_deadline: print(f"No change in sscan record for {phase_timeout_s} seconds.") print("ending plan early as unsuccessful") sscan_status._finished(success=False) yield from bps.sleep(poll_delay_s) # dump the complete data arrays if final_array_stream is not None: yield from bps.create(final_array_stream) # we have to search for the arrays since they have ``kind="omitted"`` # (which means they do not get reported by the ``.read()`` method) for part in (sscan.positioners, sscan.detectors): for nm in part.read_attrs: if "." not in nm: # TODO: write just the acquired data, not the FULL arrays! yield from bps.read(getattr(part, nm).array) yield from bps.save() # dump the entire sscan record into another stream if device_settings_stream is not None: yield from bps.create(device_settings_stream) yield from bps.read(sscan) yield from bps.save() yield from bps.close_run() elapsed = time.time() - t0 print(f"total time for sscan_1D: {elapsed} s") return sscan_status
class HPLC(Device): ready = Cpt(EpicsSignal, 'io1') injected = Cpt(EpicsSignalRO, 'io2') done = Cpt(EpicsSignalRO, 'io3') bypass = Cpt(EpicsSignal, '_bypass') def __init__(self, *args, read_filepath, write_filepath, **kwargs): self.hplc_status = HPLCStatus.idle self._injected_status = None self._done_status = None self._bypass_status = None self._resource = None self._read_filepath = read_filepath self._write_filepath = write_filepath super().__init__(*args, **kwargs) def stage(self): self.injected.subscribe(self._injected_changed) self.done.subscribe(self._done_changed) self.bypass.subscribe(self._bypass_changed) def unstage(self): self.injected.clear_sub(self._injected_changed) self.done.clear_sub(self._done_changed) self.bypass.clear_sub(self._bypass_changed) self._injected_status = None self._done_status = None self._bypass_status = None # self._resource = None def kickoff(self): """ Set 'ready' to True and return a status object tied to 'injected'. """ self.ready.set(1) self.hplc_status = HPLCStatus.waiting_injected self._injected_status = DeviceStatus(self.injected) self._done_status = DeviceStatus(self.done) return self._injected_status def complete(self): """ Return a status object tied to 'done'. """ if self._done_status is None: raise RuntimeError("must call kickoff() before complete()") return self._done_status def collect(self): """ Yield events that reference the data files generated by HPLC. the HPLC run using a batch file should always export data into /GPFS/xf16id/Windows/hplc_export.txt """ # in principle there are lots of things that can be saved # for now just keep the chromatograms sections = readShimadzuDatafile('/GPFS/xf16id/Windows/hplc_export.txt', return_all_sections=True) import numpy as np yield { 'time': time.time(), 'seq_num': 1, 'data': { 'foo': np.random.rand(2048, 1) }, 'timestamps': { 'foo': time.time() } } # TODO Decide whether you want to 'chunk' the dataset into 'events'. # Insert a datum per event and yield a partial event document. #for i in range(1): # yield {'time': time.time(), # 'seq_num': i+1, # 'data': {'foo': np.random.rand(2048, 1)}, #datum_id}, # 'timestamps': {'foo': time.time()}} def describe_collect(self): return { self.name: { 'foo': { 'dtype': 'array', 'shape': (2048, ), 'source': 'TO DO' } } } def _injected_changed(self, value, old_value, **kwargs): """Mark the status object returned by 'kickoff' as finished when injected goes from 0 to 1.""" if self._injected_status is None: return if (old_value == 0) and (value == 1): self.ready.set(0) self.hplc_status = HPLCStatus.waiting_done self._injected_status._finished() def _done_changed(self, value, old_value, **kwargs): """Mark the status object returned by 'complete' as finished when done goes from 0 to 1.""" if self._done_status is None: return if (old_value == 0) and (value == 1): self.hplc_status = HPLCStatus.idle self._done_status._finished() def _bypass_changed(self, value, old_value, **kwargs): """Mark the status object returned by 'complete' as finished when done goes from 0 to 1.""" if value == 0: return print('Bypass used: {}, hplc state: {}'.format(value, self.hplc_status)) if (value == 1) and self.hplc_status == HPLCStatus.waiting_injected: self._injected_changed(1, 0) elif (value == 2) and self.hplc_status == HPLCStatus.waiting_done: self._done_changed(1, 0) self.bypass.set(0)
class XPStraj(Device): def __init__(self, ip_addr, group, name, devices=None): """ ip_addr: IP of the XPS controller group: PVT positioner grouped defined in the controller name: devices: the corresponding Ophyd device for the posiitoner group, useful in a scan """ if devices is None: raise Exception( "devices is None: the corelation between XPS and bs motor names must be defined." ) super().__init__(name=name) self.xps = XPS() self.ip_addr = ip_addr self.sID = self.xps.TCP_ConnectToServer(ip_addr, 5001, 0.050) # 20 ms timeout is suggested for single-socket communication, per programming manual objs = self.xps.ObjectsListGet(self.sID)[1].split(';;')[0].split(';') if group not in objs: print("group %s does not exist." % group) print("valid objects are", objs) raise Exception self.group = group self.motors = [mot for mot in objs if (group + '.') in mot] self.Nmot = len(self.motors) self.devices = devices for m in devices.keys(): if m not in self.motors: raise Exception('invalid motor: ', m) print(m, devices[m].name) self.device_names = [devices[k].name for k in devices.keys()] self.verified = False uname = getpass.getuser() self.traj_files = [ "TrajScan_FW.trj-%s" % uname, "TrajScan_BK.trj-%s" % uname ] self.traj_par = { 'run_forward_traj': True, 'no_of_segments': 0, 'no_of_rampup_points': 0, 'segment_displacement': 0, 'segment_duration': 0, 'motor': None, 'rampup_distance': 0, 'motor2': None } self.time_modified = time.time() self.start_time = 0 self._traj_status = None self.detectors = None self.datum = None def stage(self): self.datum = {} def unstage(self): """ abort whatever is still going on?? """ self.abort_traj() self._traj_status = None def pulse(self, duration=0.00001): # GPIO3.DO, pin 4 mask = '1' # it seems that only DO1 works self.xps.GPIODigitalSet(self.sID, "GPIO3.DO", mask, 1) #time.sleep(duration) self.xps.GPIODigitalSet(self.sID, "GPIO3.DO", mask, 0) def read_configuration(self): ret = [(k, { 'value': self.traj_par[k], 'timestamp': self.time_modified }) for k in self.traj_par.keys()] return OrderedDict(ret) def describe_configuration(self): pass def select_forward_traj(self, op=True): if op: self.traj_par['run_forward_traj'] = True else: self.traj_par['run_forward_traj'] = False def moving(self): err, msg = self.xps.GroupMotionStatusGet(self.sID, self.group, self.Nmot) if (np.asarray(msg.split(',')) == '0').all(): return False return True def abort_traj(self): if self.moving(): err, msg = self.xps.GroupMoveAbort(self.sID, self.group) return err, msg return def kickoff(self): """ run the trajectory """ if self.verified == False: raise Exception("trajectory not defined/verified.") #self._traj_status = DeviceStatus(self.ExecuteState) self._traj_status = DeviceStatus(self) ##self.exec_traj(self.traj_par['run_forward_traj']) # should not block th = threading.Thread(target=self.exec_traj, args=(self.traj_par['run_forward_traj'], )) th.start() # always done, the scan should never even try to wait for this #status = DeviceStatus(self) #status._finished() return self._traj_status def complete(self): """ according to run_engine.py: Tell a flyer, 'stop collecting, whenever you are ready'. Return a status object tied to 'done'. """ if self._traj_status is None: raise RuntimeError("must call kickoff() before complete()") while not self._traj_status.done: print( f"{time.asctime()}: waiting for the trajectory to finish ... ", end='') time.sleep(1) print("Done.") return self._traj_status def collect_asset_docs(self): """ when the run eigine process the "collect" message, 3 functions are called (see bluesky.bundlers) collect_asset_docs(): returns resource and datum document (name, doc) RE emit(DocumentNames(name), doc) called once per scan? name is always "resource"? describe_collect(): returns a dictionary of {stream_name: data_keys, ...} RE emit(DocumentNames.descriptor, doc) collect(): returns a list of events [ev, ...], RE emit(DocumentNames.event, ev) or add to bulk data for later emit() call DocumentNames is defined in event_model, enum followed HXN example """ asset_docs_cache = [] for det in self.detectors: k = f'{det.name}_image' #det.dispatch(k, ttime.time()) (name, resource), = det.file.collect_asset_docs() assert name == 'resource' asset_docs_cache.append(('resource', resource)) resource_uid = resource['uid'] datum_id = '{}/{}'.format(resource_uid, 0) self.datum[k] = [datum_id, ttime.time()] datum = { 'resource': resource_uid, 'datum_id': datum_id, 'datum_kwargs': { 'point_number': 0 } } asset_docs_cache.append(('datum', datum)) return tuple(asset_docs_cache) def collect(self): """ save position data, called at the end of a scan (not at the end of a trajectory) this is now recorded in self.readback, as accumulated by self.update_readback() """ now = time.time() data = {} ts = {} data[self.traj_par['fast_axis']] = self.read_back['fast_axis'] ts[self.traj_par['fast_axis']] = self.read_back['timestamp'] if self.traj_par['motor2'] is not None: data[self.traj_par['slow_axis']] = self.read_back['slow_axis'] ts[self.traj_par['slow_axis']] = self.read_back['timestamp2'] for det in self.detectors: # first make sure that all data file are saved, otherwise next time when the detector is # staged, the files that are not yet saved will be lost Ni = det.cam.num_images.get() #Nc1 = 0 t0 = time.time() while det.cam.detector_state.get(as_string=True) is "Acquire": time.sleep(0.1) """ while True: Nc = det.cam.array_counter.get() #if Ni==Nc or Nc==Nc1: if Ni==Nc or time.time()-t0>5: # either the final file count is reached, or waited for too long (5s) and no new files # show up, which can happen if one or more trajectories were cut short during the raster break #Nc1 = Nc print('data files are still being written for %s, %d -> %d' % (det.name, Nc, Ni)) #time.sleep(5) time.sleep(0.5) """ k = f'{det.name}_image' (data[k], ts[k]) = self.datum[k] for k, desc in det.read().items(): data[k] = desc['value'] ts[k] = desc['timestamp'] ret = { 'time': time.time(), 'data': data, 'timestamps': ts, } yield ret def describe_collect(self): '''Describe details for the flyer collect() method''' ret = {} ret[self.traj_par['fast_axis']] = { 'dtype': 'number', 'shape': (1, ), 'source': 'PVT trajectory readback position' } if self.traj_par['motor2'] is not None: ret[self.traj_par['slow_axis']] = { 'dtype': 'number', 'shape': (1, ), 'source': 'motor position readback' } for det in self.detectors: ret[f'{det.name}_image'] = det.make_data_key() for k, desc in det.describe().items(): ret[k] = desc return {'primary': ret} def define_traj(self, motor, N, dx, dt, motor2=None, dy=0, Nr=2): """ the idea is to use FW/BK trjectories in a scan each trajactory involves a single motor only relative motion, N segements of length dx from the current position duration of each segment is dt Additional segments (Nr, at least 2) are required to ramp up and down, e.g.: # dt, x, v_out 1.0, 0.16667, 0.5 1.0, 1.0, 1.0 ... ... 1.0, 1.0, 1.0 1.0, 0.83333, 0.5 1.0, 0,0, 0.0 detector triggering should start from the 5th segment """ self.verified = False if motor not in self.motors: print("motor %s not in the list of motors: " % motor, self.motors) raise Exception err, ret = self.xps.PositionerMaximumVelocityAndAccelerationGet( self.sID, motor) mvel, macc = np.asarray(ret.split(','), dtype=np.float) midx = self.motors.index(motor) jj = np.zeros(Nr + N + Nr) jj[0] = 1 jj[Nr - 1] = -1 jj[-1] = 1 jj[-Nr] = -1 # these include the starting state of acc=vel=disp=0 disp = np.zeros(Nr + N + Nr + 1) vel = np.zeros(Nr + N + Nr + 1) acc = np.zeros(Nr + N + Nr + 1) for i in range(N + 2 * Nr): acc[i + 1] = acc[i] + jj[i] * dt vel[i + 1] = vel[i] + acc[i] * dt + jj[i] * dt * dt / 2 disp[i + 1] = vel[i] * dt + acc[i] * dt * dt / 2 + jj[ i] * dt * dt * dt / 6 vel = vel / vel.max() * dx / dt disp = disp / disp.max() * dx self.ramp_dist = disp[1:Nr + 1].sum() # rows in a PVT trajectory file correspond ot the segments # for each row/segment, the elements are # time, axis 1 displancement, axis 1 velocity out, axsi 2 ... ot1 = np.zeros((Nr + N + Nr, 1 + 2 * self.Nmot)) ot1[:, 0] = dt ot1[:, 2 * midx + 1] = disp[1:] ot1[:, 2 * midx + 2] = vel[1:] ot2 = np.zeros((Nr + N + Nr, 1 + 2 * self.Nmot)) ot2[:, 0] = dt ot2[:, 2 * midx + 1] = -disp[1:] ot2[:, 2 * midx + 2] = -vel[1:] np.savetxt("/tmp/" + self.traj_files[0], ot1, fmt='%f', delimiter=', ') np.savetxt("/tmp/" + self.traj_files[1], ot2, fmt='%f', delimiter=', ') ftp = FTP(self.ip_addr) ftp.connect() ftp.login("Administrator", "Administrator") ftp.cwd("Public/Trajectories") for fn in self.traj_files: file = open("/tmp/" + fn, "rb") ftp.storbinary('STOR %s' % fn, file) file.close() ftp.quit() for fn in self.traj_files: err, ret = self.xps.MultipleAxesPVTVerification( self.sID, self.group, fn) if err != '0': print(ret) raise Exception("trajectory verification failed.") err, ret = self.xps.MultipleAxesPVTVerificationResultGet( self.sID, motor) self.verified = True self.traj_par = { 'run_forward_traj': True, 'no_of_segments': N, 'no_of_rampup_points': Nr, 'segment_displacement': dx, 'segment_duration': dt, 'motor': motor, 'rampup_distance': self.ramp_dist, 'motor2': motor2, 'motor2_disp': dy } self.traj_par['fast_axis'] = xps_trj.devices[motor].name if motor2 is not None: self.traj_par['slow_axis'] = motor2.name self.time_modified = time.time() def exec_traj(self, forward=True, clean_event_queue=False, n_retry=5): """ execuate either the foward or backward trajectory """ if self.verified == False: raise Exception("trajectory not defined/verified.") N = self.traj_par['no_of_segments'] Nr = self.traj_par['no_of_rampup_points'] motor = self.traj_par['motor'] dt = self.traj_par['segment_duration'] if forward: traj_fn = self.traj_files[0] else: traj_fn = self.traj_files[1] # otherwise starting the trajectory might generate an error while self.moving(): time.sleep(0.2) # first set up gathering self.xps.GatheringReset(self.sID) # pulse is generated when the positioner enters the segment print( "starting a trajectory with triggering parameters: %d, %d, %.3f ..." % (Nr + 1, N + Nr + 1, dt)) self.xps.MultipleAxesPVTPulseOutputSet(self.sID, self.group, Nr + 1, N + Nr + 1, dt) self.xps.MultipleAxesPVTVerification(self.sID, self.group, traj_fn) self.xps.GatheringConfigurationSet(self.sID, [motor + ".CurrentPosition"]) self.xps.sendAndReceive( self.sID, 'EventExtendedConfigurationTriggerSet(' + 'Always,0,0,0,0,%s.PVT.TrajectoryPulse,0,0,0,0)' % self.group) self.xps.sendAndReceive( self.sID, 'EventExtendedConfigurationActionSet(' + 'GatheringOneData,0,0,0,0)') # all trigger event for gathering should be removed if clean_event_queue: err, ret = self.xps.EventExtendedAllGet(self.sID) if err == '0': for ev in ret.split(';'): self.xps.EventExtendedRemove(self.sID, ev) eID = self.xps.EventExtendedStart(self.sID)[1] self.start_time = time.time() for i in range(n_retry): # can this generate some triggers and then fail? if so this failure should kill the run # as there is no book keeping to know which images from the detectors are garbage # and the postions and image sequences will be off [err, ret] = self.xps.MultipleAxesPVTExecution(self.sID, self.group, traj_fn, 1) if err == '0': break elif i == n_retry - 1: self.safe_stop() print( f'An error (code {err}) as occured when starting trajectory execution, retry #{i+1} ...' ) #if err=='-22': # Group state must be READY [err, ret] = self.xps.GroupMotionEnable(self.sID, self.group) print(f"attempted to re-enable motion group: ", end='') time.sleep(1) self.xps.GatheringStopAndSave(self.sID) self.xps.EventExtendedRemove(self.sID, eID) self.update_readback() print('end of trajectory execution, ', end='') if self._traj_status != None: self._traj_status._finished() def safe_stop(self): fast_shutter.close() # generate enough triggers to complete exposure det = self.detectors[0] Ni = det.cam.num_images.get() Nc = det.cam.array_counter.get() """for i in range(Ni-Nc): det.trigger() print('%d more data points to complete exposure ... ' % (Ni-Nc-i), end='\r') time.sleep(self.traj_par['segment_duration']) """ raise Exception('a hardware error has occured, aborting ... ') def readback_traj(self): print('reading back trajectory ...') err, ret = self.xps.GatheringCurrentNumberGet(self.sID) ndata = int(ret.split(',')[0]) err, ret = self.xps.GatheringDataMultipleLinesGet(self.sID, 0, ndata) return [float(p) for p in ret.split('\n') if p != ''] def clear_readback(self): self.read_back = {} self.read_back['fast_axis'] = [] self.read_back['timestamp'] = [] if self.traj_par['motor2'] is not None: self.read_back['slow_axis'] = [] self.read_back['timestamp2'] = [] def update_readback(self): self.read_back['fast_axis'] = self.readback_traj() # start_time is the beginning of the execution # pulse is generated when the positioner enters the segment ?? # timestamp correspond to the middle of the segment N = self.traj_par['no_of_segments'] Nr = self.traj_par['no_of_rampup_points'] dt = self.traj_par['segment_duration'] self.read_back['timestamp'] = list(self.start_time + (0.5 + Nr + np.arange(N + 1)) * dt) if self.traj_par['motor2'] is not None: self.read_back['slow_axis'] = self.traj_par['motor2'].position self.read_back['timestamp2'] = time.time()
def trigger(self): # Re-implement this to trigger as desired in bluesky status = DeviceStatus(self) status._finished() return status
class XPStraj(Device): def __init__(self, ip_addr, group, name, devices={ 'scan.rY': ss2.ry, 'scan.Y': ss2.y, 'scan.X': ss2.x }): """ ip_addr: IP of the XPS controller group: PVT positioner grouped defined in the controller name: devices: the corresponding Ophyd device for the posiitoner group, useful in a scan """ super().__init__(name=name) self.xps = XPS() self.ip_addr = ip_addr self.sID = self.xps.TCP_ConnectToServer(ip_addr, 5001, 0.050) # 20 ms timeout is suggested for single-socket communication, per programming manual objs = self.xps.ObjectsListGet(self.sID)[1].split(';;')[0].split(';') if group not in objs: print("group %s does not exist." % group) print("valid objects are", objs) raise Exception self.group = group self.motors = [mot for mot in objs if (group + '.') in mot] self.Nmot = len(self.motors) self.devices = devices for m in devices.keys(): if m not in self.motors: raise Exception('invalid motor: ', m) print(m, devices[m].name) self.device_names = [devices[k].name for k in devices.keys()] self.verified = False uname = getpass.getuser() self.traj_files = [ "TrajScan_FW.trj-%s" % uname, "TrajScan_BK.trj-%s" % uname ] self.traj_par = { 'run_forward_traj': True, 'no_of_segments': 0, 'no_of_rampup_points': 0, 'segment_displacement': 0, 'segment_duration': 0, 'motor': None, 'rampup_distance': 0, 'motor2': None } self.time_modified = time.time() self.start_time = 0 self._traj_status = None self.detectors = None self.datum = None def stage(self): self.datum = {} def unstage(self): """ abort whatever is still going on?? """ self.abort_traj() self._traj_status = None def pulse(self, duration=0.00001): # GPIO3.DO, pin 4 mask = '1' # it seems that only DO1 works self.xps.GPIODigitalSet(self.sID, "GPIO3.DO", mask, 1) #time.sleep(duration) self.xps.GPIODigitalSet(self.sID, "GPIO3.DO", mask, 0) def read_configuration(self): ret = [(k, { 'value': self.traj_par[k], 'timestamp': self.time_modified }) for k in self.traj_par.keys()] return OrderedDict(ret) def describe_configuration(self): pass def select_forward_traj(self, op=True): if op: self.traj_par['run_forward_traj'] = True else: self.traj_par['run_forward_traj'] = False def moving(self): err, msg = self.xps.GroupMotionStatusGet(self.sID, self.group, self.Nmot) if (np.asarray(msg.split(',')) == '0').all(): return False return True def abort_traj(self): if self.moving(): err, msg = self.xps.GroupMoveAbort(self.sID, self.group) return err, msg return def kickoff(self): """ run the trajectory """ if self.verified == False: raise Exception("trajectory not defined/verified.") #self._traj_status = DeviceStatus(self.ExecuteState) self._traj_status = DeviceStatus(self) ##self.exec_traj(self.traj_par['run_forward_traj']) # should not block th = threading.Thread(target=self.exec_traj, args=(self.traj_par['run_forward_traj'], )) th.start() # always done, the scan should never even try to wait for this #status = DeviceStatus(self) #status._finished() return self._traj_status def complete(self): """ Return a status object tied to 'done'. """ #if self._traj_status is None: # raise RuntimeError("must call kickoff() before complete()") self._traj_status.done = not self.moving() return self._traj_status def collect_asset_docs(self): """ adapted from HXN fly scan example """ asset_docs_cache = [] for det in self.detectors: k = f'{det.name}_image' #det.dispatch(k, ttime.time()) (name, resource), = det.file.collect_asset_docs() assert name == 'resource' asset_docs_cache.append(('resource', resource)) resource_uid = resource['uid'] datum_id = '{}/{}'.format(resource_uid, 0) self.datum[k] = [datum_id, ttime.time()] datum = { 'resource': resource_uid, 'datum_id': datum_id, 'datum_kwargs': { 'point_number': 0 } } asset_docs_cache.append(('datum', datum)) return tuple(asset_docs_cache) def collect(self): """ save position data, called at the end of a scan (not at the end of a trajectory) this is now recorded in self.readback, as accumulated by self.update_readback() """ now = time.time() data = {} ts = {} data[self.traj_par['fast_axis']] = self.read_back['fast_axis'] ts[self.traj_par['fast_axis']] = now if self.traj_par['motor2'] is not None: data[self.traj_par['slow_axis']] = self.read_back['slow_axis'] ts[self.traj_par['slow_axis']] = now for det in self.detectors: k = f'{det.name}_image' (data[k], ts[k]) = self.datum[k] for k, desc in det.read().items(): data[k] = desc['value'] ts[k] = desc['timestamp'] ret = { 'time': time.time(), 'data': data, 'timestamps': ts, } yield ret def describe_collect(self): '''Describe details for the flyer collect() method''' ret = {} ret[self.traj_par['fast_axis']] = { 'dtype': 'number', 'shape': (1, ), 'source': 'PVT trajectory readback position' } if self.traj_par['motor2'] is not None: ret[self.traj_par['slow_axis']] = { 'dtype': 'number', 'shape': (1, ), 'source': 'motor position readback' } for det in self.detectors: ret[f'{det.name}_image'] = det.make_data_key() for k, desc in det.describe().items(): ret[k] = desc return {'primary': ret} def define_traj(self, motor, N, dx, dt, motor2=None, dy=0, Nr=2): """ the idea is to use FW/BK trjectories in a scan each trajactory involves a single motor only relative motion, N segements of length dx from the current position duration of each segment is dt Additional segments (Nr, at least 2) are required to ramp up and down, e.g.: # dt, x, v_out 1.0, 0.16667, 0.5 1.0, 1.0, 1.0 ... ... 1.0, 1.0, 1.0 1.0, 0.83333, 0.5 1.0, 0,0, 0.0 detector triggering should start from the 5th segment """ self.verified = False if motor not in self.motors: print("motor %s not in the list of motors: " % motor, self.motors) raise Exception err, ret = self.xps.PositionerMaximumVelocityAndAccelerationGet( self.sID, motor) mvel, macc = np.asarray(ret.split(','), dtype=np.float) midx = self.motors.index(motor) jj = np.zeros(Nr + N + Nr) jj[0] = 1 jj[Nr - 1] = -1 jj[-1] = 1 jj[-Nr] = -1 # these include the starting state of acc=vel=disp=0 disp = np.zeros(Nr + N + Nr + 1) vel = np.zeros(Nr + N + Nr + 1) acc = np.zeros(Nr + N + Nr + 1) for i in range(N + 2 * Nr): acc[i + 1] = acc[i] + jj[i] * dt vel[i + 1] = vel[i] + acc[i] * dt + jj[i] * dt * dt / 2 disp[i + 1] = vel[i] * dt + acc[i] * dt * dt / 2 + jj[ i] * dt * dt * dt / 6 vel = vel / vel.max() * dx / dt disp = disp / disp.max() * dx self.ramp_dist = disp[1:Nr + 1].sum() # rows in a PVT trajectory file correspond ot the segments # for each row/segment, the elements are # time, axis 1 displancement, axis 1 velocity out, axsi 2 ... ot1 = np.zeros((Nr + N + Nr, 1 + 2 * self.Nmot)) ot1[:, 0] = dt ot1[:, 2 * midx + 1] = disp[1:] ot1[:, 2 * midx + 2] = vel[1:] ot2 = np.zeros((Nr + N + Nr, 1 + 2 * self.Nmot)) ot2[:, 0] = dt ot2[:, 2 * midx + 1] = -disp[1:] ot2[:, 2 * midx + 2] = -vel[1:] np.savetxt("/tmp/" + self.traj_files[0], ot1, fmt='%f', delimiter=', ') np.savetxt("/tmp/" + self.traj_files[1], ot2, fmt='%f', delimiter=', ') ftp = FTP(self.ip_addr) ftp.connect() ftp.login("Administrator", "Administrator") ftp.cwd("Public/Trajectories") for fn in self.traj_files: file = open("/tmp/" + fn, "rb") ftp.storbinary('STOR %s' % fn, file) file.close() ftp.quit() for fn in self.traj_files: err, ret = self.xps.MultipleAxesPVTVerification( self.sID, self.group, fn) if err != '0': print(ret) raise Exception("trajectory verification failed.") err, ret = self.xps.MultipleAxesPVTVerificationResultGet( self.sID, motor) self.verified = True self.traj_par = { 'run_forward_traj': True, 'no_of_segments': N, 'no_of_rampup_points': Nr, 'segment_displacement': dx, 'segment_duration': dt, 'motor': motor, 'rampup_distance': self.ramp_dist, 'motor2': motor2, 'motor2_disp': dy } self.traj_par['fast_axis'] = xps_trj.devices[motor].name if motor2 is not None: self.traj_par['slow_axis'] = motor2.name self.time_modified = time.time() def exec_traj(self, forward=True, clean_event_queue=False): """ execuate either the foward or backward trajectory """ if self.verified == False: raise Exception("trajectory not defined/verified.") N = self.traj_par['no_of_segments'] Nr = self.traj_par['no_of_rampup_points'] motor = self.traj_par['motor'] dt = self.traj_par['segment_duration'] if forward: traj_fn = self.traj_files[0] else: traj_fn = self.traj_files[1] # otherwise starting the trajectory might generate an error while self.moving(): time.sleep(0.2) # first set up gathering self.xps.GatheringReset(self.sID) # pulse is generated when the positioner enters the segment print("setting triggering parameters: %d, %d, %.3f" % (Nr + 1, N + Nr + 1, dt)) self.xps.MultipleAxesPVTPulseOutputSet(self.sID, self.group, Nr + 1, N + Nr + 1, dt) self.xps.MultipleAxesPVTVerification(self.sID, self.group, traj_fn) self.xps.GatheringConfigurationSet(self.sID, [motor + ".CurrentPosition"]) self.xps.sendAndReceive( self.sID, 'EventExtendedConfigurationTriggerSet(' + 'Always,0,0,0,0,%s.PVT.TrajectoryPulse,0,0,0,0)' % self.group) self.xps.sendAndReceive( self.sID, 'EventExtendedConfigurationActionSet(' + 'GatheringOneData,0,0,0,0)') # all trigger event for gathering should be removed if clean_event_queue: err, ret = self.xps.EventExtendedAllGet(self.sID) if err == '0': for ev in ret.split(';'): self.xps.EventExtendedRemove(self.sID, ev) eID = self.xps.EventExtendedStart(self.sID)[1] self.start_time = time.time() self.xps.MultipleAxesPVTExecution(self.sID, self.group, traj_fn, 1) self.xps.GatheringStopAndSave(self.sID) self.xps.EventExtendedRemove(self.sID, eID) self.update_readback() if self._traj_status != None: self._traj_status._finished() def readback_traj(self): err, ret = self.xps.GatheringCurrentNumberGet(self.sID) ndata = int(ret.split(',')[0]) err, ret = self.xps.GatheringDataMultipleLinesGet(self.sID, 0, ndata) return [float(p) for p in ret.split('\n') if p != ''] def clear_readback(self): self.read_back = {} self.read_back['fast_axis'] = [] if self.traj_par['motor2'] is not None: self.read_back['slow_axis'] = [] def update_readback(self): self.read_back['fast_axis'] += self.readback_traj() if self.traj_par['motor2'] is not None: self.read_back['slow_axis'] += [self.traj_par['motor2'].position]
def trigger(self): #TODO: modify settle_time to be from a acquiretime pv acq_time = self.acquire_time.get() _status = DeviceStatus(self, settle_time=acq_time) _status._finished(success=True) return _status
class ContinuousAcquisitionTrigger(BlueskyInterface): """ This trigger mixin class records images when it is triggered. It expects the detector to *already* be acquiring, continously. """ def __init__(self, *args, plugin_name=None, image_name=None, **kwargs): if plugin_name is None: raise ValueError("plugin name is a required keyword argument") super().__init__(*args, **kwargs) self._plugin = getattr(self, plugin_name) if image_name is None: image_name = '_'.join([self.name, 'image']) self._plugin.stage_sigs[self._plugin.auto_save] = 'No' self.cam.stage_sigs[self.cam.image_mode] = 'Continuous' self._plugin.stage_sigs[self._plugin.file_write_mode] = 'Capture' self._image_name = image_name self._status = None self._num_captured_signal = self._plugin.num_captured self._num_captured_signal.subscribe(self._num_captured_changed) self._save_started = False def stage(self): if self.cam.acquire.get() != 1: ##foolishly added by DO try: self.cam.acquire.put(1) print("The detector wasn't properly acquiring :(") time.sleep(1) print("Don't worry, I probably fixed it for you!") except: raise RuntimeError( "The ContinuousAcuqisitionTrigger expects " "the detector to already be acquiring." "I was unable to fix it, please restart the ui." ) #new line, DO #old style, simple runtime error (no rescue attempt) #raise RuntimeError("The ContinuousAcuqisitionTrigger expects " # "the detector to already be acquiring.") #if we get this far, we can now stage the detector. super().stage() # put logic to look up proper dark frame # die if none is found def trigger(self): "Trigger one acquisition." if not self._staged: raise RuntimeError("This detector is not ready to trigger." "Call the stage() method before triggering.") self._save_started = False self._status = DeviceStatus(self) self._desired_number_of_sets = self.number_of_sets.get() self._plugin.num_capture.put(self._desired_number_of_sets) self.dispatch(self._image_name, ttime.time()) # reset the proc buffer, this needs to be generalized self.proc.reset_filter.put(1) self._plugin.capture.put(1) # Now the TIFF plugin is capturing. return self._status def _num_captured_changed(self, value=None, old_value=None, **kwargs): "This is called when the 'acquire' signal changes." if self._status is None: return if value == self._desired_number_of_sets: # This is run on a thread, so exceptions might pass silently. # Print and reraise so they are at least noticed. try: self.tiff.write_file.put(1) except Exception as e: print(e) raise self._save_started = True if value == 0 and self._save_started: self._status._finished() self._status = None self._save_started = False