def __call__(self, *args, subs=None, sub_factories=None, **kwargs): scan_kwargs = dict() # Any kwargs valid for the scan go to the scan, not the RE. for k, v in kwargs.copy().items(): if k in self.params: scan_kwargs[k] = kwargs.pop(k) from bluesky.global_state import gs RE_params = list(signature(gs.RE.__call__).parameters.keys()) if set(RE_params) & set(self.params): raise AssertionError("The names of the scan's arguments clash " "the RunEngine arguments. Use different " "names. Avoid: {0}".format(RE_params)) self.scan = self.scan_class(gs.DETS, *args, **scan_kwargs) # Combine subs passed as args and subs set up in subs attribute. _subs = defaultdict(list) _update_lists(_subs, normalize_subs_input(subs)) _update_lists(_subs, normalize_subs_input(self.subs)) # Create a sub from each sub_factory. _update_lists(_subs, _run_factories(sub_factories, self.scan)) _update_lists(_subs, _run_factories(self.sub_factories, self.scan)) # Set up scan attributes. self.scan.configuration = self.configuration self.scan.flyers = self.flyers # Any remainging kwargs go the RE. To be safe, no args are passed # to RE; RE args effectively become keyword-only arguments. return gs.RE(self.scan, _subs, **kwargs)
def construct_subs(plan_name, **kwargs): '''Construct the subscriptions functions from factories. This is a small dependency injection tool to construct subscriptions based on `plan_name` by consulting `gs.SUB_FACTORIES` and combining `'common'` and `plan_name` lists. The values in `**kwargs` are injected into the factories as needed by consulting the signature. Parameters ---------- plan_name : str The key used to get the subscription factories Returns ------- subs : dict A dictionary suitable for handing to the RE or to the sub_wrapper plan. ''' factories = gs.SUB_FACTORIES.get('common', []) factories.extend(gs.SUB_FACTORIES.get(plan_name, [])) kwargs.setdefault('gs', gs) ret = normalize_subs_input(None) for factory in factories: try: inp = get_factory_input(factory) except InvalidFactory as e: warnings.warn('factory {fn} could not be run due to {e!r}'.format( fn=factory.__name__, e=e)) else: fact_kwargs = {} missing_kwargs = set() for k in inp.req: try: fact_kwargs[k] = kwargs[k] except KeyError: missing_kwargs.add(k) if missing_kwargs: warnings.warn('The factory {fn} could not be run due to ' 'missing ' 'required input: {missing!r}'.format( fn=factory.__name__, missing=missing_kwargs)) continue for k in inp.opt: try: fact_kwargs[k] = kwargs[k] except KeyError: pass update_sub_lists(ret, normalize_subs_input(factory(**fact_kwargs))) return ret
def _run_factories(factories, scan): factories = normalize_subs_input(factories) out = {k: list(filterfalse(lambda x: x is None, (sf(scan) for sf in v))) for k, v in factories.items()} gs._SECRET_STASH = out return out
def _run_factories(factories, scan): '''Run sub factory functions for a scan Factory functions should return lists, which will be added onto the subscription key (e.g., 'all' or 'start') specified in the factory definition. If the factory function returns None, the list will not be modified. ''' factories = normalize_subs_input(factories) out = {k: list(filterfalse(lambda x: x is None, (sf(scan) for sf in v))) for k, v in factories.items()} gs._SECRET_STASH = out return out
def _run_factories(factories, scan): '''Run sub factory functions for a scan Factory functions should return lists, which will be added onto the subscription key (e.g., 'all' or 'start') specified in the factory definition. If the factory function returns None, the list will not be modified. ''' factories = normalize_subs_input(factories) out = { k: list(filterfalse(lambda x: x is None, (sf(scan) for sf in v))) for k, v in factories.items() } gs._SECRET_STASH = out return out
def __call__(self, sample, plan, subs=None, *, verify_write=False, dark_strategy=periodic_dark, raise_if_interrupted=False, **metadata_kw): # The CustomizedRunEngine knows about a Beamtime object, and it # interprets integers for 'sample' as indexes into the Beamtime's # lists of Samples from all its Experiments. # deprecated from v0.5 release #if getattr(glbl, 'collection', None) is None: # raise RuntimeError("No collection has been linked to current " # "experiment yet.\nPlease do\n" # ">>> open_collection(<collection_name>)\n" # "before you run any prun") if isinstance(sample, int): try: sample = self.beamtime.samples[sample] except IndexError: print("WARNING: hmm, there is no sample with index `{}`" ", please do `bt.list()` to check if it exists yet" .format(sample)) return # If a plan is given as a string, look in up in the global registry. if isinstance(plan, int): try: plan = self.beamtime.scanplans[plan] except IndexError: print("WARNING: hmm, there is no scanplan with index `{}`" ", please do `bt.list()` to check if it exists yet" .format(plan)) return # If the plan is an xpdAcq 'ScanPlan', make the actual plan. if isinstance(plan, ScanPlan): plan = plan.factory() _subs = normalize_subs_input(subs) if verify_write: _subs.update({'stop': verify_files_saved}) # No keys in metadata_kw are allows to collide with sample keys. if set(sample) & set(metadata_kw): raise ValueError("These keys in metadata_kw are illegal " "because they are always in sample: " "{}".format(set(sample) & set(metadata_kw))) if self._beamtime.get('bt_wavelength') is None: print("WARNING: there is no wavelength information in current" "beamtime object, scan will keep going....") metadata_kw.update(sample) sh = glbl.shutter # force to open shutter before scan and close it after if glbl.shutter_control: plan = bp.pchain(bp.abs_set(sh, 1), plan, bp.abs_set(sh, 0)) # Alter the plan to incorporate dark frames. if glbl.auto_dark: plan = dark_strategy(plan) plan = bp.msg_mutator(plan, _inject_qualified_dark_frame_uid) # Load calibration file if glbl.auto_load_calib: plan = bp.msg_mutator(plan, _inject_calibration_md) # Execute super().__call__(plan, subs, raise_if_interrupted=raise_if_interrupted, **metadata_kw) # deprecated from v0.5 release # insert collection #_insert_collection(glbl.collection_name, glbl.collection, # self._run_start_uids) return self._run_start_uids
def __call__(self, sample, plan, subs=None, *, verify_write=False, dark_strategy=periodic_dark, raise_if_interrupted=False, **metadata_kw): # The CustomizedRunEngine knows about a Beamtime object, and it # interprets integers for 'sample' as indexes into the Beamtime's # lists of Samples from all its Experiments. # deprecated from v0.5 release #if getattr(glbl, 'collection', None) is None: # raise RuntimeError("No collection has been linked to current " # "experiment yet.\nPlease do\n" # ">>> open_collection(<collection_name>)\n" # "before you run any xrun") if isinstance(sample, int): try: sample = self.beamtime.samples[sample] except IndexError: print( "WARNING: hmm, there is no sample with index `{}`" ", please do `bt.list()` to check if it exists yet".format( sample)) return # If a plan is given as a string, look in up in the global registry. if isinstance(plan, int): try: plan = self.beamtime.scanplans[plan] except IndexError: print( "WARNING: hmm, there is no scanplan with index `{}`" ", please do `bt.list()` to check if it exists yet".format( plan)) return # If the plan is an xpdAcq 'ScanPlan', make the actual plan. if isinstance(plan, ScanPlan): plan = plan.factory() _subs = normalize_subs_input(subs) if verify_write: _subs.update({'stop': verify_files_saved}) # No keys in metadata_kw are allows to collide with sample keys. if set(sample) & set(metadata_kw): raise ValueError("These keys in metadata_kw are illegal " "because they are always in sample: " "{}".format(set(sample) & set(metadata_kw))) if self._beamtime.get('bt_wavelength') is None: print("WARNING: there is no wavelength information in current" "beamtime object, scan will keep going....") metadata_kw.update(sample) sh = glbl.shutter if glbl.shutter_control: # Alter the plan to incorporate dark frames. # only works if user allows shutter control if glbl.auto_dark: plan = dark_strategy(plan) plan = bp.msg_mutator(plan, _inject_qualified_dark_frame_uid) # force to close shutter after scan plan = bp.finalize_wrapper(plan, bp.abs_set(sh, 0, wait=True)) # Load calibration file if glbl.auto_load_calib: plan = bp.msg_mutator(plan, _inject_calibration_md) # Insert glbl mask plan = bp.msg_mutator(plan, _inject_mask) # Execute return super().__call__(plan, subs, raise_if_interrupted=raise_if_interrupted, **metadata_kw)
def __call__(self, sample, plan, subs=None, *, verify_write=False, dark_strategy=periodic_dark, robot=False, **metadata_kw): """ Execute a plan Any keyword arguments other than those listed below will be interpreted as metadata and recorded with the run. Parameters ---------- sample : int or dict-like or list of int or dict-like Sample metadata. If a beamtime object is linked, an integer will be interpreted as the index appears in the ``bt.list()`` method, corresponding metadata will be passed. A customized dict can also be passed as the sample metadata. plan : int or generator or list of int or generator Scan plan. If a beamtime object is linked, an integer will be interpreted as the index appears in the ``bt.list()`` method, corresponding scan plan will be A generator or that yields ``Msg`` objects (or an iterable that returns such a generator) can also be passed. subs: callable, list, or dict, optional Temporary subscriptions (a.k.a. callbacks) to be used on this run. Default to None. For convenience, any of the following are accepted: * a callable, which will be subscribed to 'all' * a list of callables, which again will be subscribed to 'all' * a dictionary, mapping specific subscriptions to callables or lists of callables; valid keys are {'all', 'start', 'stop', 'event', 'descriptor'} verify_write: bool, optional Double check if the data have been written into database. In general data is written in a lossless fashion at the NSLS-II. Therefore, False by default. dark_strategy: callable, optional. Protocol of taking dark frame during experiment. Default to the logic of matching dark frame and light frame with the sample exposure time and frame rate. Details can be found at ``http://xpdacq.github.io/xpdAcq/usb_Running.html#automated-dark-collection`` robot: bool, optional If true run the scan as a robot scan, defaults to False metadata_kw: Extra keyword arguments for specifying metadata in the run time. If the extra metdata has the same key as the ``sample``, ``ValueError`` will be raised. Returns ------- uids : list list of uids (i.e. RunStart Document uids) of run(s) """ if self.md.get("robot", None) is not None: raise RuntimeError("Robot must be specified at call time, not in" "global metadata") if robot: metadata_kw.update(robot=True) # The CustomizedRunEngine knows about a Beamtime object, and it # interprets integers for 'sample' as indexes into the Beamtime's # lists of Samples from all its Experiments. # Turn everything into lists sample, plan = self._normalize_sample_plan(sample, plan) # Turn ints into actual samples sample = self.translate_to_sample(sample) if robot: print("This is the current experimental plan:") print("Sample Name: Sample Position") for s, p in [ (k, [o[1] for o in v]) for k, v in groupby(zip(sample, plan), key=lambda x: x[0]) ]: print( s["sample_name"], ":", self._beamtime.robot_info[s["sa_uid"]], ) for pp in p: # Check if this is a registered scanplan if isinstance(pp, int): print( indent( "{}".format( list( self.beamtime.scanplans.values())[pp]), "\t", )) else: print("This scan is not a registered scanplan so no " "summary") ip = input("Is this ok? [y]/n") if ip.lower() == "n": return # Turn ints into generators plan = self.translate_to_plan(plan, sample) # Collect the plans by contiguous samples and chain them sample, plan = zip( *[(k, pchain(*[o[1] for o in v])) for k, v in groupby(zip(sample, plan), key=lambda x: x[0])]) # Make the complete plan by chaining the chained plans total_plan = [] for s, p in zip(sample, plan): if robot: # If robot scan inject the needed md into the sample s.update(self._beamtime.robot_info[s["sa_uid"]]) total_plan.append(robot_wrapper(p, s)) else: total_plan.append(p) plan = pchain(*total_plan) _subs = normalize_subs_input(subs) if verify_write: _subs.update({"stop": verify_files_saved}) if self._beamtime and self._beamtime.get("bt_wavelength") is None: print("WARNING: there is no wavelength information in current" "beamtime object, scan will keep going....") if glbl["shutter_control"]: # Alter the plan to incorporate dark frames. # only works if user allows shutter control if glbl["auto_dark"]: plan = dark_strategy(plan) plan = bpp.msg_mutator(plan, _inject_qualified_dark_frame_uid) # force to close shutter after scan plan = bpp.finalize_wrapper( plan, bps.abs_set( xpd_configuration["shutter"], XPD_SHUTTER_CONF["close"], wait=True, ), ) # Load calibration file if glbl["auto_load_calib"]: plan = bpp.msg_mutator(plan, _inject_calibration_md) # Insert xpdacq md version plan = bpp.msg_mutator(plan, _inject_xpdacq_md_version) # Insert analysis stage tag plan = bpp.msg_mutator(plan, _inject_analysis_stage) # Insert filter metadata plan = bpp.msg_mutator(plan, _inject_filter_positions) # Execute return super().__call__(plan, subs, **metadata_kw)
def __call__(self, sample: typing.Union[int, str, dict, list, tuple], plan: typing.Union[int, str, typing.Generator, ScanPlan, list, tuple], subs: typing.Union[typing.Callable, dict, list] = None, *, verify_write: bool = False, dark_strategy: typing.Callable = periodic_dark, robot: bool = False, ask_before_run: bool = False, **metadata_kw): """ Execute a plan. The call is basically the same as the RE. The only difference is that the plan will be mutated and preprocessed to incorpate sample meta-data, dark fram stragy and so on. Any keyword arguments other than those listed below will be interpreted as metadata and recorded with the run. Parameters ---------- sample : int or dict-like or list of int or dict-like Sample metadata If a beamtime object is linked, an integer will be interpreted as the index appears in the ``bt.list()`` method, corresponding metadata will be passed. A customized dict can also be passed as the sample metadata. plan : int or generator or list of int or generator Scan plan. If a beamtime object is linked, an integer will be interpreted as the index appears in the ``bt.list()`` method, corresponding scan plan will be A generator or that yields ``Msg`` objects (or an iterable that returns such a generator) can also be passed. subs: callable, list, or dict, optional Temporary subscriptions (a.k.a. callbacks) to be used on this run. Default to None. For convenience, any of the following are accepted: * a callable, which will be subscribed to 'all' * a list of callables, which again will be subscribed to 'all' * a dictionary, mapping specific subscriptions to callables or lists of callables; valid keys are {'all', 'start', 'stop', 'event', 'descriptor'} verify_write: bool, optional Double check if the data have been written into database. In general data is written in a lossless fashion at the NSLS-II. Therefore, False by default. dark_strategy: callable, optional. Protocol of taking dark frame during experiment. Default to the logic of matching dark frame and light frame with the sample exposure time and frame rate. Details can be found at ``http://xpdacq.github.io/xpdAcq/usb_Running.html#automated-dark-collection`` robot: bool, optional If true run the scan as a robot scan, defaults to False metadata_kw: Extra keyword arguments for specifying metadata in the run time. If the extra metdata has the same key as the ``sample``, ``ValueError`` will be raised. Returns ------- uids : list list of uids (i.e. RunStart Document uids) of run(s) """ if self.md.get("robot", None) is not None: raise RuntimeError("Robot must be specified at call time, not in" "global metadata") dark_strategy = dark_strategy if glbl["auto_dark"] else None shutter_control = ( xpd_configuration["shutter"], XPD_SHUTTER_CONF["close"]) if glbl["shutter_control"] else None # The CustomizedRunEngine knows about a Beamtime object, and it # interprets integers for 'sample' as indexes into the Beamtime's # lists of Samples from all its Experiments grand_plan = xpdacq_composer(self.beamtime, sample, plan, robot=robot, shutter_control=shutter_control, dark_strategy=dark_strategy, auto_load_calib=glbl['auto_load_calib']) # normalize the subs _subs = normalize_subs_input(subs) if subs else {} # verify writing files if verify_write: _subs.update({"stop": verify_files_saved}) if robot: metadata_kw.update({'robot': True}) if ask_before_run: ip = input("Is this ok? [y]/n") if ip.lower() == "n": return return super(CustomizedRunEngine, self).__call__(grand_plan, _subs, **metadata_kw)