Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
92 changes: 47 additions & 45 deletions xpdsim/dets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/')

Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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
Expand All @@ -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():
Expand All @@ -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)
102 changes: 102 additions & 0 deletions xpdsim/robot.py
Original file line number Diff line number Diff line change
@@ -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()
3 changes: 2 additions & 1 deletion xpdsim/tests/test_dets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
Expand Down
35 changes: 35 additions & 0 deletions xpdsim/tests/test_robot.py
Original file line number Diff line number Diff line change
@@ -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