class FooDeviceTargetValue(BaseDevice): foo = Quantity(q.mm) def __init__(self, value): super(BaseDevice, self).__init__() self._value = value self._test_value = value async def _get_foo(self): delta = 1 * q.mm return self._value + delta async def _set_foo(self, value): self._value = value async def _get_target_foo(self): return self._value def test_setter(self, value): self._test_value = value def test_getter(self): return self._test_value def test_target(self): return 10 * q.mm test = Quantity(q.mm, fset=_test_setter, fget=_test_getter, fget_target=_test_target)
class FooDevice(BaseDevice): state = State(default='standby') no_write = Parameter() foo = Quantity(q.m, check=check(source='*', target='moved')) bar = Quantity(q.m) test = Quantity(q.m, fset=_test_setter, fget=_test_getter) def __init__(self, default): super(FooDevice, self).__init__() self._value = default self._param_value = 0 * q.mm self._test_value = 0 * q.mm async def _get_foo(self): return self._value @transition(target='moved') async def _set_foo(self, value): self._value = value async def _get_bar(self): return 5 * q.m param = Parameter(fget=_get_foo, fset=_set_foo) async def _get_param(self): return self._param_value async def _set_param(self, value): self._param_value = value param = Parameter(fget=_get_param, fset=_set_param)
class StorageRing(Device): """Read-only access to storage ring information. .. py:attribute:: current Ring current .. py:attribute:: energy Ring energy .. py:attribute:: lifetime Ring lifetime in hours """ current = Quantity(q.mA, help="Current") energy = Quantity(q.MeV, help="Energy") lifetime = Quantity(q.hour, help="Expected lifetime") def __init__(self): super(StorageRing, self).__init__() async def _get_current(self): raise AccessorNotImplementedError async def _get_energy(self): raise AccessorNotImplementedError async def _get_lifetime(self): raise AccessorNotImplementedError
class SpiralMixin(Parameterizable): """ Mixin for spiral tomography. """ start_position_vertical = Quantity(q.mm) """Initial position of the vertical motor.""" vertical_shift_per_tomogram = Quantity(q.mm) """Vertical shift per tomogram.""" sample_height = Quantity(q.mm) """Height of the sample. *vertical_motor* will be scanned from *start_position_vertical* to *sample_height* + *vertical_shift_per_tomogram* to sample the whole specimen. """ num_tomograms = Parameter() """Number of tomograms, that are required to cover the whole specimen.""" def __init__(self, vertical_motor, start_position_vertical, sample_height, vertical_shift_per_tomogram): """ :param vertical_motor: LinearMotor to translate the sample along the tomographic axis. :type vertical_motor: concert.devices.motors.base.LinearMotor :param start_position_vertical: Start position of *vertical_motor*. :type start_position_vertical: q.mm :param sample_height: Height of the sample. :type sample_height: q.mm :param vertical_shift_per_tomogram: Distance *vertical_motor* is translated during one *angular_range*. :type vertical_shift_per_tomogram: q.mm """ self._start_position_vertical = start_position_vertical self._vertical_shift_per_tomogram = vertical_shift_per_tomogram self._sample_height = sample_height self._vertical_motor = vertical_motor Parameterizable.__init__(self) async def _get_start_position_vertical(self): return self._start_position_vertical async def _get_num_tomograms(self): shift = await self.get_vertical_shift_per_tomogram() height = await self.get_sample_height() return abs(height / shift).to_base_units().magnitude + 1 async def _get_vertical_shift_per_tomogram(self): return self._vertical_shift_per_tomogram async def _get_sample_height(self): return self._sample_height async def _set_start_position_vertical(self, position): self._start_position_vertical = position async def _set_sample_height(self, height): self._sample_height = height async def _set_vertical_shift_per_tomogram(self, shift): self._vertical_shift_per_tomogram = shift
class Monochromator(Device): """Monochromator device which is used to filter the beam in order to get a very narrow energy bandwidth. .. py:attribute:: energy Monochromatic energy in electron volts. .. py:attribute:: wavelength Monochromatic wavelength in meters. """ state = State(default="standby") energy = Quantity(q.eV, help="Energy") wavelength = Quantity(q.nanometer, help="Wavelength") async def _get_energy(self): try: return await self._get_energy_real() except AccessorNotImplementedError: return wavelength_to_energy(await self._get_wavelength_real()) @check(source="standby", target="standby") async def _set_energy(self, energy): try: await self._set_energy_real(energy) except AccessorNotImplementedError: await self._set_wavelength_real(energy_to_wavelength(energy)) async def _get_wavelength(self): try: return await self._get_wavelength_real() except AccessorNotImplementedError: return energy_to_wavelength(await self._get_energy_real()) @check(source="standby", target="standby") async def _set_wavelength(self, wavelength): try: await self._set_wavelength_real(wavelength) except AccessorNotImplementedError: await self._set_energy_real(wavelength_to_energy(wavelength)) async def _get_energy_real(self): raise AccessorNotImplementedError async def _set_energy_real(self, energy): raise AccessorNotImplementedError async def _get_wavelength_real(self): raise AccessorNotImplementedError async def _set_wavelength_real(self, wavelength): raise AccessorNotImplementedError
class UserLimitDevice(BaseDevice): """A device which tunnles the limit setting via user-defined functions.""" foo = Quantity(q.mm) def __init__(self): super().__init__() self['foo']._user_lower_getter = self.get_user_lower self['foo']._user_lower_setter = self.set_user_lower self['foo']._user_upper_getter = self.get_user_upper self['foo']._user_upper_setter = self.set_user_upper self.lower_via_func = None self.upper_via_func = None async def get_user_lower(self): return self.lower_via_func async def set_user_lower(self, value): self.lower_via_func = value async def get_user_upper(self): return self.upper_via_func async def set_user_upper(self, value): self.upper_via_func = value
class ExternalLimitDevice(BaseDevice): def __init__(self, value): super(ExternalLimitDevice, self).__init__() self._value = value foo = Quantity(q.mm, external_lower_getter=get_external_lower, external_upper_getter=get_external_upper)
class Scales(Device): """Base scales class.""" weight = Quantity(q.g, help="Weighted mass") def __init__(self): super(Scales, self).__init__() async def _get_weight(self): raise AccessorNotImplementedError
class FooDeviceTagetValue(BaseDevice): foo = Quantity(q.mm) test = Quantity(q.mm, fset=_test_setter, fget=_test_getter, fget_target=_test_target) def __init__(self, value): super(BaseDevice, self).__init__() self._value = value def _get_foo(self): delta = np.random.randint(low=1, high=100) * q.mm return self._value + delta def _set_foo(self, value): self._value = value def _get_target_foo(self): return self._value
class PhotoDiode(Device): """ Impementation of photo diode with V output signal """ intensity = Quantity(q.V) def __init__(self): super(PhotoDiode, self).__init__() def _get_intensity(self): raise AccessorNotImplementedError
class LightSource(Device): """A base LightSource class.""" intensity = Quantity(q.V) def __init__(self): super(LightSource, self).__init__() async def _set_intensity(self, value): raise AccessorNotImplementedError async def _get_intensity(self): raise AccessorNotImplementedError
class Detector(Device): """ A base detector class for holding cameras and optics necessary to do acquire image. """ pixel_width = Quantity(q.m, help="Effective pixel width") pixel_height = Quantity(q.m, help="Effective pixel height") def _get_pixel_width(self): return self.camera.sensor_pixel_width * self.magnification def _get_pixel_height(self): return self.camera.sensor_pixel_height * self.magnification @property def camera(self): raise AccessorNotImplementedError @property def magnification(self): raise AccessorNotImplementedError
class RotationMotor(_PositionMixin, base.RotationMotor): """A rotational step motor dummy.""" motion_velocity = Quantity(q.deg / q.s) def __init__(self, upper_hard_limit=None, lower_hard_limit=None): base.RotationMotor.__init__(self) _PositionMixin.__init__(self) self._position = 0 * q.deg self._lower_hard_limit = -np.inf * q.deg self._upper_hard_limit = np.inf * q.deg self._motion_velocity = 50000 * q.deg / q.s if upper_hard_limit: self._upper_hard_limit = upper_hard_limit if lower_hard_limit: self._lower_hard_limit = lower_hard_limit
class AccessorCheckDevice(Parameterizable): foo = Quantity(q.m) def __init__(self, future, check): super(AccessorCheckDevice, self).__init__() self.check = check self.future = future self._value = 0 * q.mm async def _set_foo(self, value): self.check(self.future) self._value = value time.sleep(0.01) async def _get_foo(self): self.check(self.future) time.sleep(0.01) return self._value
class LinearMotor(_PositionMixin, base.LinearMotor): """A linear step motor dummy.""" motion_velocity = Quantity(q.mm / q.s) def __init__(self, position=None, upper_hard_limit=None, lower_hard_limit=None): base.LinearMotor.__init__(self) _PositionMixin.__init__(self) self._motion_velocity = 200000 * q.mm / q.s if position: self._position = position if upper_hard_limit: self._upper_hard_limit = upper_hard_limit if lower_hard_limit: self._lower_hard_limit = lower_hard_limit
class RotationMotor(_PositionMixin): """ One-dimensional rotational motor. .. attribute:: position Position of the motor in angular units. """ async def _get_state(self): raise NotImplementedError state = State(default='standby') position = Quantity(q.deg, help="Angular position", check=check(source=['hard-limit', 'standby'], target=['hard-limit', 'standby'])) def __init__(self): super(RotationMotor, self).__init__()
class ContinuousRotationMotor(RotationMotor, _VelocityMixin): """ One-dimensional rotational motor with adjustable velocity. .. attribute:: velocity Current velocity in angle per time unit. """ def __init__(self): super(ContinuousRotationMotor, self).__init__() async def _get_state(self): raise NotImplementedError state = State(default='standby') velocity = Quantity(q.deg / q.s, help="Angular velocity", check=check(source=['hard-limit', 'standby', 'moving'], target=['moving', 'standby']))
class LinearMotor(_PositionMixin): """ One-dimensional linear motor. .. attribute:: position Position of the motor in length units. """ def __init__(self): super(LinearMotor, self).__init__() def _get_state(self): raise NotImplementedError state = State(default='standby') position = Quantity(q.mm, help="Position", check=check(source=['hard-limit', 'standby'], target=['hard-limit', 'standby']))
class Pump(Device): """A pumping device.""" state = State(default='standby') flow_rate = Quantity(q.l / q.s, help="Flow rate") def __init__(self): super(Pump, self).__init__() @async @check(source='standby', target='pumping') def start(self): """ start() Start pumping. """ self._start() @async @check(source='pumping', target='standby') def stop(self): """ stop() Stop pumping. """ self._stop() def _get_flow_rate(self): raise AccessorNotImplementedError def _set_flow_rate(self, flow_rate): raise AccessorNotImplementedError def _start(self): raise NotImplementedError def _stop(self): raise NotImplementedError
class ContinuousLinearMotor(LinearMotor): """ One-dimensional linear motor with adjustable velocity. .. attribute:: velocity Current velocity in length per time unit. """ def __init__(self): super(ContinuousLinearMotor, self).__init__() def _get_state(self): raise NotImplementedError def _cancel_velocity(self): self._abort() state = State(default='standby') velocity = Quantity(q.mm / q.s, help="Linear velocity", check=check(source=['hard-limit', 'standby', 'moving'], target=['moving', 'standby']))
class Base(base.Camera): exposure_time = Quantity(q.s) sensor_pixel_width = Quantity(q.micrometer) sensor_pixel_height = Quantity(q.micrometer) roi_x0 = Quantity(q.pixel) roi_y0 = Quantity(q.pixel) roi_width = Quantity(q.pixel) roi_height = Quantity(q.pixel) def __init__(self): super(Base, self).__init__() self._frame_rate = 1000 / q.s self._trigger_source = self.trigger_sources.AUTO self._exposure_time = 1 * q.ms self._roi_x0 = 0 * q.pixel self._roi_y0 = 0 * q.pixel self._roi_width = 640 * q.pixel self._roi_height = 480 * q.pixel async def _get_sensor_pixel_width(self): return 5 * q.micrometer async def _get_sensor_pixel_height(self): return 5 * q.micrometer async def _get_roi_x0(self): return self._roi_x0 async def _set_roi_x0(self, x0): self._roi_x0 = x0 async def _get_roi_y0(self): return self._roi_y0 async def _set_roi_y0(self, y0): self._roi_y0 = y0 async def _get_roi_width(self): return self._roi_width async def _set_roi_width(self, roi): self._roi_width = roi async def _get_roi_height(self): return self._roi_height async def _set_roi_height(self, roi): self._roi_height = roi async def _get_exposure_time(self): return self._exposure_time async def _set_exposure_time(self, value): self._exposure_time = value async def _get_frame_rate(self): return self._frame_rate async def _set_frame_rate(self, frame_rate): self._frame_rate = frame_rate async def _get_trigger_source(self): return self._trigger_source async def _set_trigger_source(self, source): self._trigger_source = source @transition(target='recording') async def _record_real(self): pass @transition(target='standby') async def _stop_real(self): pass async def _trigger_real(self): pass
class Camera(Device): """Base class for remotely controllable cameras. .. py:attribute:: frame-rate Frame rate of acquisition in q.count per time unit. """ trigger_sources = Bunch(['AUTO', 'SOFTWARE', 'EXTERNAL']) trigger_types = Bunch(['EDGE', 'LEVEL']) state = State(default='standby') frame_rate = Quantity(1 / q.second, help="Frame frequency") trigger_source = Parameter(help="Trigger source") def __init__(self): super(Camera, self).__init__() self.convert = identity @background @check(source='standby', target='recording') async def start_recording(self): """ start_recording() Start recording frames. """ await self._record_real() @background @check(source='recording', target='standby') async def stop_recording(self): """ stop_recording() Stop recording frames. """ await self._stop_real() @contextlib.asynccontextmanager async def recording(self): """ recording() A context manager for starting and stopping the camera. In general it is used with the ``async with`` keyword like this:: async with camera.recording(): frame = await camera.grab() """ await self.start_recording() try: yield finally: LOG.log(AIODEBUG, 'stop recording in recording()') await self.stop_recording() @background async def trigger(self): """Trigger a frame if possible.""" await self._trigger_real() @background async def grab(self): """Return a NumPy array with data of the current frame.""" return self.convert(await self._grab_real()) async def stream(self): """ stream() Grab frames continuously yield them. This is an async generator. """ await self.set_trigger_source(self.trigger_sources.AUTO) await self.start_recording() while await self.get_state() == 'recording': yield await self.grab() async def _get_trigger_source(self): raise AccessorNotImplementedError async def _set_trigger_source(self, source): raise AccessorNotImplementedError async def _record_real(self): raise AccessorNotImplementedError async def _stop_real(self): raise AccessorNotImplementedError async def _trigger_real(self): raise AccessorNotImplementedError async def _grab_real(self): raise AccessorNotImplementedError
class Radiography(Experiment): """ Radiography experiment This records dark images (without beam) and flat images (with beam and without the sample) as well as the projections with the sample in the beam. """ num_flats = Parameter() """Number of images acquired for flatfield correction.""" num_darks = Parameter() """Number of images acquired for dark correction.""" num_projections = Parameter() """Number of projection images.""" radio_position = Quantity(q.mm) """Position of the flat_motor to acquire projection images of the sample.""" flat_position = Quantity(q.mm) """Position of the flat_motor to acquire images without the sample.""" def __init__(self, walker, flat_motor, radio_position, flat_position, camera, num_flats, num_darks, num_projections, separate_scans=True): """ :param walker: Walker for storing experiment data. :type walker: concert.storage.Walker :param flat_motor: LinearMotor for moving sample in and out of the beam. :type flat_motor: concert.devices.motors.base.LinearMotor :param radio_position: Position of *flat_motor* that the sample is positioned in the beam. :type radio_position: q.mm :param flat_position: Position of *flat_motor* that the sample is positioned out of the beam. :type flat_position: q.mm :param camera: Camera to acquire the images. :type camera: concert.devices.cameras.base.Camera :param num_flats: Number of images for flatfield correction. :type num_flats: int :param num_darks: Number of images for dark correction. :type num_darks: int :param num_projections: Number of projections. :type num_projections: int """ self._num_flats = num_flats self._num_darks = num_darks self._num_projections = num_projections self._radio_position = radio_position self._flat_position = flat_position self._finished = None self._flat_motor = flat_motor self._camera = camera darks_acq = Acquisition("darks", self._take_darks) flats_acq = Acquisition("flats", self._take_flats) radios_acq = Acquisition("radios", self._take_radios) super().__init__([darks_acq, flats_acq, radios_acq], walker, separate_scans=separate_scans) async def _last_acquisition_running(self) -> bool: return await self.acquisitions[-1].get_state() == "running" async def _get_num_flats(self): return self._num_flats async def _get_num_darks(self): return self._num_darks async def _get_num_projections(self): return self._num_projections async def _get_radio_position(self): return self._radio_position async def _get_flat_position(self): return self._flat_position async def _set_num_flats(self, n): self._num_flats = int(n) async def _set_num_darks(self, n): self._num_darks = int(n) async def _set_num_projections(self, n): self._num_projections = int(n) async def _set_flat_position(self, position): self._flat_position = position async def _set_radio_position(self, position): self._radio_position = position async def _prepare_flats(self): """ Called before flat images are acquired. The *flat_motor* is moved to the *flat_position* and :py:meth:`.start_sample_exposure()` will be called. """ await self._flat_motor.set_position(await self.get_flat_position()) await self.start_sample_exposure() async def _finish_flats(self): """ Called after all flat images are acquired. Does nothing in this class. """ pass async def _prepare_darks(self): """ Called before the dark images are acquired. Calls :py:meth:`.stop_sample_exposure()`. """ await self.stop_sample_exposure() async def _finish_darks(self): """ Called after all dark images are acquired. Does nothing in this class. """ pass async def _prepare_radios(self): """ Called before the projection images are acquired. The *flat_motor* is moved to the *radio_position* and :py:meth:`.start_sample_exposure()` will be called. """ await self._flat_motor.set_position(await self.get_radio_position()) await self.start_sample_exposure() async def _finish_radios(self): """ Function that is called after all frames are acquired. It will be called only once. This calls :py:meth:`.stop_sample_exposure()`. """ if self._finished: return await self.stop_sample_exposure() self._finished = True @background async def run(self): self._finished = False await super().run() async def _take_radios(self): """ Generator for projection images. First :py:meth:`._prepare_radios()` is called. Afterwards :py:meth:`._produce_frames()` generates the frames. At the end :py:meth:`._finish_radios()` is called. """ try: await self._prepare_radios() async for frame in self._produce_frames(self._num_projections): yield frame finally: await self._finish_radios() async def _take_flats(self): """ Generator for taking flatfield images First :py:meth:`._prepare_flats()` is called. Afterwards :py:meth:`._produce_frames()` generates the frames. At the end :py:meth:`._finish_flats()` is called. """ try: await self._prepare_flats() async for frame in self._produce_frames(self._num_flats): yield frame finally: await self._finish_flats() async def _take_darks(self): """ Generator for taking dark images First :py:meth:`._prepare_darks()` is called. Afterwards :py:meth:`._produce_frames()` generates the frames. At the end :py:meth:`._finish_darks()` is called. """ try: await self._prepare_darks() async for frame in self._produce_frames(self._num_darks): yield frame finally: await self._finish_darks() async def _produce_frames(self, number, **kwargs): """ Generator of frames. Sets the camera to auto-trigger and then grabs *number* of frames. :param number: Number of frames that are generated :type number: int """ await self._camera.set_trigger_source("AUTO") async with self._camera.recording(): for i in range(int(number)): yield await self._camera.grab() async def start_sample_exposure(self): """ This function must implement in a way that the sample is exposed by radiation, like opening a shutter or starting an X-ray tube. """ raise NotImplementedError() async def stop_sample_exposure(self): """ This function must implement in a way that the sample is not exposed by radiation, like closing a shutter or switching off an X-ray tube. """ raise NotImplementedError()
class Positioner(Device): """Combines more motors which move to form a complex motion. *axes* is a list of :class:`.Axis` instances. *position* is a 3D vector of coordinates specifying the global position of the positioner. If a certain coordinate in the positioner is missing, then when we set the position or orientation we can specify the respective vector position to be zero or numpy.nan. """ position = Quantity(q.m, help="Global position", lower=-INF_VECTOR * q.m, upper=INF_VECTOR * q.m) orientation = Quantity(q.rad, help="Orientation of the coordinate system", lower=-INF_VECTOR * q.rad, upper=INF_VECTOR * q.rad) def __init__(self, axes, position=None): super(Positioner, self).__init__() self.translators = {} self.rotators = {} self.global_position = None for axis in axes: if isinstance(axis.motor, LinearMotor): self.translators[axis.coordinate] = axis else: self.rotators[axis.coordinate] = axis @background async def move(self, position): """ move(position) Move by specified *position*. """ await self.set_position(await self.get_position() + position) @background async def rotate(self, angles): """ rotate(angles) Rotate by *angles*. """ await self.set_orientation(await self.get_orientation() + angles) @background async def right(self, value): """ right(value) Move right by *value*.""" return await self.move(_vectorize(value, 'x')) @background async def left(self, value): """ left(value) Move left by *value*.""" return await self.right(-value) @background async def up(self, value): """ up(value) Move up by *value*. """ return await self.move(_vectorize(value, 'y')) @background async def down(self, value): """ down(value) Move down by *value*. """ return await self.up(-value) @background async def forward(self, value): """ forward(value) Move forward by *value*. """ return await self.move(_vectorize(value, 'z')) @background async def back(self, value): """ back(value) Move back by *value*. """ return await self.forward(-value) async def _get_position(self): """Get the position of the positioner.""" return await self._get_vector(self.translators) async def _set_position(self, position): """3D translation to *position*.""" await self._set_vector(position, self.translators) async def _get_orientation(self): """Get the angular position of the positioner.""" return await self._get_vector(self.rotators) async def _set_orientation(self, angles): """Rotation with magnitudes *angles*.""" await self._set_vector(angles, self.rotators) async def _get_vector(self, axes): """Get the current translation or orientation vector.""" vector = [] unit = q.m if axes == self.translators else q.rad for coordinate in ['x', 'y', 'z']: if coordinate in axes: vector.append( (await axes[coordinate].get_position()).to(unit).magnitude) else: vector.append(np.nan) return vector * unit async def _set_vector(self, vector, axes): """Set position (angular or translational) given by *vector* on *axes*.""" coros = [] for i, coordinate in enumerate(['x', 'y', 'z']): magnitude = vector[i].magnitude if not np.isnan(magnitude): if coordinate not in axes: if magnitude != 0: # Last chance is to specify the coordinate to be zero. raise PositionerError( 'Cannot move in {} coordinate'.format(coordinate)) else: coro = axes[coordinate].set_position(vector[i]) coros.append(coro) await asyncio.gather(*coros)
class DummyDevice(Device): """A dummy device.""" position = Quantity(unit=q.mm) sleep_time = Quantity(unit=q.s) # Value with a check-decorated setter before it is bound to instance, so still a function value = Parameter() # value with check wrapping the bound method cvalue = Parameter(check=check(source='*', target='standby')) # Value set elsewhere evalue = Parameter(fset=set_evalue, fget=get_evalue, check=check(source='*', target='standby')) slow = Parameter() state = State(default='standby') def __init__(self, slow=None): super(DummyDevice, self).__init__() self._position = 1 * q.mm self._value = 0 self._slow = slow self._sleep_time = 0.5 * q.s async def _get_sleep_time(self): return self._sleep_time async def _set_sleep_time(self, value): self._sleep_time = value async def _get_position(self): return self._position async def _set_position(self, value): self._position = value async def _get_slow(self): try: LOG.log(AIODEBUG, 'get slow start %s', self._slow) await asyncio.sleep((await self.get_sleep_time()).magnitude) LOG.log(AIODEBUG, 'get slow finish %s', self._slow) return self._slow except asyncio.CancelledError: LOG.log(AIODEBUG, 'get slow cancelled %s', self._slow) raise except KeyboardInterrupt: # do not scream LOG.debug("KeyboardInterrupt caught while getting") async def _set_slow(self, value): try: LOG.log(AIODEBUG, 'set slow start %s', value) await asyncio.sleep((await self.get_sleep_time()).magnitude) LOG.log(AIODEBUG, 'set slow finish %s', value) self._slow = value except asyncio.CancelledError: LOG.log(AIODEBUG, 'set slow cancelled %s', value) raise async def _get_value(self): """Get the real value.""" return self._value async def _get_target_value(self): """Get the real value.""" return self._value + 1 @check(source='standby', target=['standby', 'hard-limit']) @transition(immediate='moving', target='standby') async def _set_value(self, value): """The real value setter.""" self._value = value async def _get_cvalue(self): """The real value setter.""" return self._value async def _set_cvalue(self, value): """The real value setter.""" self._value = value @background async def do_nothing(self, value=None): """Do nothing. For testing task canellation.""" await self._do_nothing(value=value) async def _do_nothing(self, value=None): LOG.log(AIODEBUG, f'Start doing nothing: {value}') try: await asyncio.sleep(1) LOG.log(AIODEBUG, f'Stop doing nothing: {value}') return value except asyncio.CancelledError: LOG.log(AIODEBUG, f'Doing nothing cancelled: {value}') raise async def _emergency_stop(self): LOG.debug('Emergency stop on a dummy device') await asyncio.sleep(0.5) self._state_value = 'aborted'
def __init__(self, name, params=None): """ Create a new libuca camera. The *name* is passed to the uca plugin manager. :raises CameraError: In case camera *name* does not exist. """ super(Camera, self).__init__() import gi gi.require_version('Uca', '2.0') from gi.repository import GObject, GLib, Uca self._manager = Uca.PluginManager() params = params if params else {} try: self.uca = self._manager.get_camerah(name, params) except GLib.GError as ge: raise base.CameraError(str(ge)) except Exception: raise base.CameraError("`{0}' is not a valid camera".format(name)) units = { Uca.Unit.METER: q.m, Uca.Unit.SECOND: q.s, Uca.Unit.DEGREE_CELSIUS: q.celsius, Uca.Unit.COUNT: q.dimensionless, Uca.Unit.PIXEL: q.pixel, } parameters = {} for prop in self.uca.props: if prop.name in ('trigger-source', 'trigger-type', 'frames-per-second'): continue getter, setter, unit = None, None, None uca_unit = self.uca.get_unit(prop.name) if uca_unit in units: unit = units[uca_unit] if prop.flags & GObject.ParamFlags.READABLE: getter = _new_getter_wrapper(prop.name, unit) if prop.flags & GObject.ParamFlags.WRITABLE: setter = _new_setter_wrapper(prop.name, unit) name = prop.name.replace('-', '_') if uca_unit in units: parameters[name] = Quantity(unit, fget=getter, fset=setter, help=prop.blurb) else: parameters[name] = Parameter(fget=getter, fset=setter, help=prop.blurb) if parameters: self.install_parameters(parameters) class _Dummy(object): pass setattr(self.uca, 'enum_values', _Dummy()) def get_enum_bunch(enum): enum_map = {} for key, value in list(enum.__enum_values__.items()): name = value.value_nick.upper().replace('-', '_') enum_map[name] = key return Bunch(enum_map) for prop in self.uca.props: if hasattr(prop, 'enum_class'): setattr(self.uca.enum_values, prop.name.replace('-', '_'), get_enum_bunch(prop.default_value)) self._uca_get_frame_rate = _new_getter_wrapper('frames-per-second') self._uca_set_frame_rate = _new_setter_wrapper('frames-per-second') # Invert the uca trigger source dict in order to return concert values trigger_dict = self.uca.enum_values.trigger_source.__dict__ self._uca_to_concert_trigger = { v: k for k, v in list(trigger_dict.items()) } self._uca_get_trigger = _new_getter_wrapper('trigger-source') self._uca_set_trigger = _new_setter_wrapper('trigger-source') self._record_shape = None self._record_dtype = None
class XRayTube(Device): """ A base x-ray tube class. """ voltage = Quantity(q.kV) current = Quantity(q.uA) power = Quantity(q.W) state = State(default='off') def __init__(self): super(XRayTube, self).__init__() async def _get_state(self): raise AccessorNotImplementedError async def _get_voltage(self): raise AccessorNotImplementedError async def _set_voltage(self, voltage): raise AccessorNotImplementedError async def _get_current(self): raise AccessorNotImplementedError async def _set_current(self, current): raise AccessorNotImplementedError async def _get_power(self): return (await self.get_voltage() * await self.get_current()).to(q.W) @background @check(source='off', target='on') async def on(self): """ on() Enables the x-ray tube. """ await self._on() @background @check(source='on', target='off') async def off(self): """ off() Disables the x-ray tube. """ await self._off() async def _on(self): """ Implementation of on(). """ raise NotImplementedError async def _off(self): """ Implementation of off(). """ raise NotImplementedError
class ContinuousSpiralTomography(ContinuousTomography, SpiralMixin): """ Spiral Tomography This implements a helical acquisition scheme, where the sample is translated perpendicular to the beam while the sample is rotated and the projections are recorded. """ vertical_velocity = Quantity(q.mm / q.s) def __init__(self, walker, flat_motor, tomography_motor, vertical_motor, radio_position, flat_position, camera, start_position_vertical, sample_height, vertical_shift_per_tomogram, num_flats=200, num_darks=200, num_projections=3000, angular_range=180 * q.deg, start_angle=0 * q.deg, separate_scans=True): """ :param walker: Walker for storing experiment data. :type walker: concert.storage.Walker :param flat_motor: LinearMotor for moving sample in and out of the beam. :type flat_motor: concert.devices.motors.base.LinearMotor :param tomography_motor: ContinuousRotationMotor for tomography scan. :type tomography_motor: concert.devices.motors.base.ContinuousLinearMotor :param vertical_motor: ContinuousLinearMotor to translate the sample along the tomographic axis. :type vertical_motor: concert.devices.motors.base.ContinuousLinearMotor :param radio_position: Position of *flat_motor* that the sample is positioned in the beam. :type radio_position: q.mm :param flat_position: Position of *flat_motor* that the sample is positioned out of the beam. :type flat_position: q.mm :param camera: Camera to acquire the images. :type camera: concert.devices.cameras.base.Camera :param start_position_vertical: Start position of *vertical_motor*. :type start_position_vertical: q.mm :param sample_height: Height of the sample. :type sample_height: q.mm :param vertical_shift_per_tomogram: Distance *vertical_motor* is translated during one *angular_range*. :type vertical_shift_per_tomogram: q.mm :param num_flats: Number of images for flatfield correction. :type num_flats: int :param num_darks: Number of images for dark correction. :type num_darks: int :param num_projections: Number of projections. :type num_projections: int :param angular_range: Range for the scan of the *tomography_motor*. :type angular_range: q.deg :param start_angle: Start position of *tomography_motor* for the first projection. :type start_angle: q.deg """ ContinuousTomography.__init__(self, walker=walker, flat_motor=flat_motor, tomography_motor=tomography_motor, radio_position=radio_position, flat_position=flat_position, camera=camera, num_flats=num_flats, num_darks=num_darks, num_projections=num_projections, angular_range=angular_range, start_angle=start_angle, separate_scans=separate_scans) SpiralMixin.__init__( self, vertical_motor=vertical_motor, vertical_shift_per_tomogram=vertical_shift_per_tomogram, sample_height=sample_height, start_position_vertical=start_position_vertical) async def _get_vertical_velocity(self): shift = await self.get_vertical_shift_per_tomogram() fps = await self._camera.get_frame_rate() num = await self.get_num_projections() return shift * fps / num async def _prepare_radios(self): """ Called before the projection images are acquired. The :py:meth:`ContinuousTomography._prepare_radios()` is called and the vertical motor is moved to vertica_start_position. """ await asyncio.gather( ContinuousTomography._prepare_radios(self), self._vertical_motor.set_position( await self.get_start_position_vertical())) async def _finish_radios(self): """ Called after the projection images are required. First this stops sample exposure, stops the tomography_motor, stops the vertical_motor. Then the *motion_velocity* of tomography_motor and vertical_motor is restored. """ if self._finished: return await asyncio.gather(self.stop_sample_exposure(), self._tomography_motor.stop(), self._vertical_motor.stop()) if 'motion_velocity' in self._tomography_motor: await self._tomography_motor['motion_velocity'].restore() if 'motion_velocity' in self._vertical_motor: await self._vertical_motor['motion_velocity'].restore() await asyncio.gather( self._tomography_motor.set_position(await self.get_start_angle()), self._vertical_motor.set_position( await self.get_start_position_vertical())) self._finished = True async def _take_radios(self): """ Generator for the projection images. The :py:meth:`_prepare_radios()` is called. Then the *motion_velocity* of the vertical_motor is stashed. The velocities of the tomography_motor and the vertical_motor are set- """ try: await self._prepare_radios() if 'motion_velocity' in self._vertical_motor: await self._vertical_motor['motion_velocity'].stash() await self._tomography_motor.set_velocity(await self.get_velocity()) await self._vertical_motor.set_velocity( await self.get_vertical_velocity()) num_projections = await self.get_num_projections( ) * await self.get_num_tomograms() async for frame in self._produce_frames(num_projections): yield frame finally: await self._finish_radios()
class ContinuousTomography(Tomography): """ Continuous Tomography This implements a tomography with a continuous rotation of the sample. The camera must record frames with a constant rate. """ velocity = Quantity(q.deg / q.s) """Velocity of the *tomography_motor* in the continuous scan.""" def __init__(self, walker, flat_motor, tomography_motor, radio_position, flat_position, camera, num_flats=200, num_darks=200, num_projections=3000, angular_range=180 * q.deg, start_angle=0 * q.deg, separate_scans=True): """ :param walker: Walker for storing experiment data. :type walker: concert.storage.Walker :param flat_motor: LinearMotor for moving sample in and out of the beam. :type flat_motor: concert.devices.motors.base.LinearMotor :param tomography_motor: ContinuousRotationMotor for tomography scan. :type tomography_motor: concert.devices.motors.base.ContinuousRotationMotor :param radio_position: Position of *flat_motor* that the sample is positioned in the beam. :type radio_position: q.mm :param flat_position: Position of *flat_motor* that the sample is positioned out of the beam. :type flat_position: q.mm :param camera: Camera to acquire the images. :type camera: concert.devices.camera.base.Camera :param num_flats: Number of images for flatfield correction. :type num_flats: int :param num_darks: Number of images for dark correction. :type num_darks: int :param num_projections: Number of projections. :type num_projections: int :param angular_range: Range for the scan of the *tomography_motor*. :type angular_range: q.deg :param start_angle: Start position of *tomography_motor* for the first projection. :type start_angle: q.deg """ Tomography.__init__(self, walker=walker, flat_motor=flat_motor, tomography_motor=tomography_motor, radio_position=radio_position, flat_position=flat_position, camera=camera, num_flats=num_flats, num_darks=num_darks, num_projections=num_projections, angular_range=angular_range, start_angle=start_angle, separate_scans=separate_scans) async def _get_velocity(self): angular_range = await self.get_angular_range() num_projections = await self.get_num_projections() fps = await self._camera.get_frame_rate() return fps * angular_range / num_projections async def _prepare_radios(self): """ Called before projection images are acquired. Calls :py:meth:`.Tomography._prepare_radios()` and stashed the *motion_velocity* property of the *tomography_motor*. """ await super()._prepare_radios() if 'motion_velocity' in self._tomography_motor: await self._tomography_motor['motion_velocity'].stash() async def _finish_radios(self): """ Called after the projections are acquired. Stops the continuous motion of the *tomography_motor* and restores the *motion_velocity*. Calls :py:meth:`.Tomography._finish_radios()` . """ if self._finished: return await self._tomography_motor.stop() if 'motion_velocity' in self._tomography_motor: await self._tomography_motor['motion_velocity'].restore() await super()._finish_radios() self._finished = True async def _take_radios(self): """ Generator for the projection images. The :py:meth:`_prepare_radios()` is called. Afterwards the velocity property of the tomography_motor is set to correct velocity. Then :py:meth:`_produce_frames()` will generate the images. At the end :py:meth:`_finish_radios()` is called. """ try: await self._prepare_radios() await self._tomography_motor.set_velocity(await self.get_velocity()) async for frame in self._produce_frames( await self.get_num_projections()): yield frame finally: await self._finish_radios()
class Camera(Device): """Base class for remotely controllable cameras. .. py:attribute:: frame-rate Frame rate of acquisition in q.count per time unit. """ trigger_modes = Bunch(['AUTO', 'SOFTWARE', 'EXTERNAL']) state = State(default='standby') frame_rate = Quantity(1.0 / q.second, help="Frame frequency") trigger_mode = Parameter(help="Trigger mode") def __init__(self): super(Camera, self).__init__() self.convert = identity @check(source='standby', target='recording') def start_recording(self): """Start recording frames.""" self._record_real() @check(source='recording', target='standby') def stop_recording(self): """Stop recording frames.""" self._stop_real() @contextlib.contextmanager def recording(self): """ A context manager for starting and stopping the camera. In general it is used with the ``with`` keyword like this:: with camera.recording(): frame = camera.grab() """ self.start_recording() try: yield finally: self.stop_recording() def trigger(self): """Trigger a frame if possible.""" self._trigger_real() def grab(self): """Return a NumPy array with data of the current frame.""" return self.convert(self._grab_real()) @async def grab_async(self): return self.grab() @async def stream(self, consumer): """Grab frames continuously and send them to *consumer*, which is a coroutine. """ self.trigger_mode = self.trigger_modes.AUTO self.start_recording() while self.state == 'recording': consumer.send(self.grab()) def _get_trigger_mode(self): raise AccessorNotImplementedError def _set_trigger_mode(self, mode): raise AccessorNotImplementedError def _record_real(self): raise AccessorNotImplementedError def _stop_real(self): raise AccessorNotImplementedError def _trigger_real(self): raise AccessorNotImplementedError def _grab_real(self): raise AccessorNotImplementedError