diff --git a/.travis.yml b/.travis.yml index 5dbf2ec..0a8364d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -53,7 +53,7 @@ before_install: install: - export GIT_FULL_HASH=`git rev-parse HEAD` - - conda create -n testenv nose python=$TRAVIS_PYTHON_VERSION pytest coverage pip databroker bluesky flake8 pyFAI mongoquery codecov attrs metadatastore filestore -c conda-forge -c lightsource2-tag -c soft-matter + - conda create -n testenv nose python=$TRAVIS_PYTHON_VERSION pytest coverage pip databroker bluesky flake8 pyFAI mongoquery codecov attrs metadatastore filestore -c conda-forge -c lightsource2-tag -c soft-matter ophyd - source activate testenv - 'pip install https://github.com/NSLS-II/portable-mds/zipball/master#egg=portable_mds' - 'pip install https://github.com/NSLS-II/portable-fs/zipball/master#egg=portable_fs' diff --git a/xpdsim/dets.py b/xpdsim/dets.py index b243426..5844454 100644 --- a/xpdsim/dets.py +++ b/xpdsim/dets.py @@ -23,7 +23,7 @@ from cycler import cycler from pims import ImageSequence from pkg_resources import resource_filename as rs_fn -from bluesky.utils import new_uid +from .robot import Robot DATA_DIR = rs_fn('xpdsim', 'data/') @@ -61,11 +61,14 @@ class SimulatedPE1C(be.ReaderWithFileStore): """ def __init__(self, name, read_fields, fs, shutter=None, - dark_fields=None, **kwargs): + dark_fields=None, Robot=None, **kwargs): self.images_per_set = PutGet() self.number_of_sets = PutGet() self.cam = SimulatedCam() + self.read_fields = read_fields + self.sample_num = None self.shutter = shutter + self.Robot = Robot self._staged = False super().__init__(name, read_fields, fs=fs, **kwargs) self.ready = True # work around a hack in Reader @@ -75,39 +78,25 @@ def __init__(self, name, read_fields, fs, shutter=None, else: self._dark_fields = None - def trigger(self): - if self.shutter and self._dark_fields and \ - self.shutter.read()['rad']['value'] == 0: - read_v = {field: {'value': func(), 'timestamp': ttime.time()} - for field, func in self._dark_fields.items() - if field in self.read_attrs} - self._result.clear() - for idx, (name, reading) in enumerate(read_v.items()): - # Save the actual reading['value'] to disk and create a record - # in FileStore. - np.save('{}_{}.npy'.format(self._path_stem, idx), - reading['value']) - datum_id = new_uid() - self.fs.insert_datum(self._resource_id, datum_id, - dict(index=idx)) - # And now change the reading in place, replacing the value with - # a reference to FileStore. - reading['value'] = datum_id - self._result[name] = reading - - delay_time = self.exposure_time - if delay_time: - if self.loop.is_running(): - st = be.SimpleStatus() - self.loop.call_later(delay_time, st._finished) - return st - else: - ttime.sleep(delay_time) - - return be.NullStatus() + def trigger_read(self): + if self.Robot: + if self.Robot.get_sample_number() != \ + self.sample_num: + self.sample_num = self.Robot.get_sample_number() + self.read_fields.update({k: v for k, v in + self.Robot.get_fields_dict().items if k + in self.read_fields.keys()}) + if self.shutter and self._dark_fields and \ + self.shutter.read()['rad']['value'] == 0: + rv = {field: {'value': func(), 'timestamp': ttime.time()} + for field, func in self._dark_fields.items() + if field in self.read_attrs} else: - return super().trigger() + rv = super().trigger_read() + read_v = dict(rv) + read_v['pe1_image']['value'] = read_v['pe1_image']['value'].copy() + return read_v def build_image_cycle(path): @@ -133,7 +122,11 @@ def build_image_cycle(path): chess_path = os.path.join(DATA_DIR, 'chess/') -def det_factory(name, fs, path, shutter=None, **kwargs): +def robot_factory(theta, sample_map=None, **kwargs): + return Robot(theta, sample_map=sample_map) + + +def det_factory(name, fs, path, shutter=None, Robot=None, **kwargs): """Build a detector using real images Parameters @@ -150,8 +143,7 @@ def det_factory(name, fs, path, shutter=None, **kwargs): detector: SimulatedPE1C instance The detector """ - cycle = build_image_cycle( - path) + cycle = build_image_cycle(path) gen = cycle() def nexter(): @@ -160,17 +152,27 @@ def nexter(): if shutter: stream_piece = next(gen) sample_img = stream_piece['pe1_image'] - gen = chain((i for i in [stream_piece]), gen) # put the piece on top + gen = chain((i for i in [stream_piece]), gen) + # put the piece on top def dark_nexter(): return np.zeros(sample_img.shape) - return SimulatedPE1C(name, - {'pe1_image': lambda: nexter()}, fs=fs, - shutter=shutter, - dark_fields={'pe1_image': lambda: dark_nexter()}, + if Robot is None: + return SimulatedPE1C(name, {'pe1_image': lambda: nexter()}, + fs=fs, shutter=shutter, + dark_fields={'pe1_image': lambda: + dark_nexter()}, **kwargs) + else: + return SimulatedPE1C(name, {'pe1_image': lambda: nexter()}, + fs=fs, shutter=shutter, + dark_fields={'pe1_image': lambda: + dark_nexter()}, + Robot=Robot, **kwargs) + + if Robot is None: + return SimulatedPE1C(name, {'pe1_image': lambda: nexter()}, fs=fs, **kwargs) - - return SimulatedPE1C(name, - {'pe1_image': lambda: nexter()}, fs=fs, - **kwargs) + else: + return SimulatedPE1C(name, {'pe1_image': lambda: nexter()}, fs=fs, + Robot=Robot, **kwargs) diff --git a/xpdsim/robot.py b/xpdsim/robot.py new file mode 100644 index 0000000..1334f8e --- /dev/null +++ b/xpdsim/robot.py @@ -0,0 +1,102 @@ +# Detector will get the sample information from the robot via a mediator to +# determine which image to display. +import asyncio +from ophyd import Device, EpicsSignal, EpicsSignalRO +from ophyd import Component as Cpt +from ophyd import Signal +from ophyd.utils import set_and_wait +from bluesky import Msg +from bluesky.plans import subs_wrapper, count, list_scan, single_gen +from bluesky.callbacks import LiveTable +import time as ttime + + +class Robot(Device): + sample_number = Cpt(Signal, value=0) + load_cmd = Cpt(Signal, value=0) + unload_cmd = Cpt(Signal, value=0) + execute_cmd = Cpt(Signal, value=0) + status = Cpt(Signal, value=0) + current_sample_number = Cpt(Signal, value=0) + + # Map sample types to load position and measurement position + TH_POS = {'capillary': {'load': None, 'measure': None}, + 'plate': {'load': 0, 'measure': 90}, + None: {'load': None, 'measure': None}} + + def __init__(self, theta, *args, sample_map=None, **kwargs): + self.theta = theta # theta is a motor + self.sample_map = sample_map + # sample_map = {sample #: {'pe1_image: nexter} + self._current_sample_geometry = None + super().__init__("", *args, **kwargs) + + def get_fields_dict(self): + n = self.get_sample_number() + return self.sample_map[n] + + def get_sample_number(self): + return self.sample_number + + def _poll_until_idle(self): + ttime.sleep(3) # gives robot plenty of time to start + while self.status.get() != 'Idle': + ttime.sleep(.1) + + def _poll_until_sample_cleared(self): + while self.current_sample_number.get() != 0: + ttime.sleep(.1) + + def load_sample(self, sample_number, sample_geometry=None): + # If no sample is loaded, current_sample_number = 0 + # is reported by the robot + if self.current_sample_number.get() != 0: + raise RuntimeError("Sample %d is already loaded." + % self.current_sample_number.get()) + + # Rotate theta into loading position if necessary + load_pos = self.TH_POS[sample_geometry]['load'] + if load_pos is not None: + print('Moving theta to load position') + self.theta.move(load_pos, wait=True) + + # Loading the sample is a three-step procedure: + # Set sample_number; issue load_cmd; issue execute_cmd. + set_and_wait(self.sample_number, sample_number) + set_and_wait(self.load_cmd, 1) + # set_and_wait is an ophyd.utils import + self.execute_cmd.put(1) + print('Loading...') + self._poll_until_idle() + + # Rotate theta into measurement position if necessary + measure_pos = self.TH_POS[sample_geometry]['measure'] + if measure_pos is not None: + print('Moving theta to measure position') + self.theta.move(measure_pos, wait=True) + + # Stash the current sample geomtery for reference when we unload + self._current_sample_geometry = sample_geometry + + def unload_sample(self): + if self.current_sample_number.get() == 0: + # there is nothing to do + return + + # Rotate theta into loading position if necessary (e.g. flat plate + # model) + load_pos = self.TH_POS[self._current_sample_geomgery]['load'] + if load_pos is not None: + print('Moving theta to unload position') + self.theta.move(load_pos, wait=True) + + set_and_wait(self.unload_cmd, 1) + self.execute_cmd.put(1) + print('Unloading...') + self._poll_until_idle() + self._poll_until_sample_cleared() + self._current_sample_geometry = None + + def stop(self): + self.theta.stop() + super().stop() diff --git a/xpdsim/tests/test_dets.py b/xpdsim/tests/test_dets.py index d7c4f27..72221cd 100644 --- a/xpdsim/tests/test_dets.py +++ b/xpdsim/tests/test_dets.py @@ -41,12 +41,13 @@ def test_dets_shutter(db, tmp_dir, name, fp): for n, d in db.restream(db[-1], fill=True): if n == 'event': assert_array_equal(d['data']['pe1_image'], - np.zeros(next(cg)['pe1_image'].shape)) + np.zeros(d['data']['pe1_image'].shape)) assert uid is not None # With the shutter up RE(abs_set(shctl1, 1, wait=True)) uid = RE(scan) + next(cg) for n, d in db.restream(db[-1], fill=True): if n == 'event': assert_array_equal(d['data']['pe1_image'], next(cg)['pe1_image']) diff --git a/xpdsim/tests/test_robot.py b/xpdsim/tests/test_robot.py new file mode 100644 index 0000000..a788e09 --- /dev/null +++ b/xpdsim/tests/test_robot.py @@ -0,0 +1,35 @@ +import bluesky.examples as be +from bluesky.plans import Count +from bluesky.tests.utils import setup_test_run_engine +from numpy.testing import assert_array_equal +from ..dets import robot_factory, nsls_ii_path, build_image_cycle, \ + chess_path, det_factory + + +test_params = [('nslsii', nsls_ii_path), ('chess', chess_path)] + + +# @pytest.mark.parametrize(('name', 'fp'), test_params) +def test_robot(db, tmp_dir): + th = be.Mover('theta', {'theta': lambda x: x}, {'x': 0}) + sm = {} + for i, (name, path) in enumerate(test_params): + cycle = build_image_cycle(path) + gen = cycle() + + def nexter(): + return next(gen)['pe1_image'] + sm[i] = {'pe1_image': lambda: nexter()} + r = robot_factory(th, sample_map=sm) + det = det_factory(name, db.fs, path, save_path=tmp_dir, Robot=r) + RE = setup_test_run_engine() + RE.subscribe('all', db.mds.insert) + scan = Count([det], ) + uid = RE(scan) + db.fs.register_handler('RWFS_NPY', be.ReaderWithFSHandler) + cycle2 = build_image_cycle() + cg = cycle2() + for n, d in db.restream(db[-1], fill=True): + if n == 'event': + assert_array_equal(d['data']['pe1_image'], next(cg)['pe1_image']) + assert uid is not None