Skip to content

Commit

Permalink
Merge pull request #336 from ufo-kit/improve-scan
Browse files Browse the repository at this point in the history
Improve scan
  • Loading branch information
tfarago committed Oct 13, 2014
2 parents 6f93d2d + 7aab612 commit 3bea2ec
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 79 deletions.
15 changes: 15 additions & 0 deletions concert/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,19 @@ def _check_limit(self, value):
format(self._parameter.unit))


class SelectionValue(ParameterValue):

"""Descriptor for :class:`.Selection` class."""

def __init__(self, instance, selection):
super(SelectionValue, self).__init__(instance, selection)

@property
def values(self):
"""Selection values."""
return tuple(self._parameter.iterable)


class Parameterizable(object):

"""
Expand Down Expand Up @@ -810,6 +823,8 @@ def install_parameters(self, params):
def _install_parameter(self, param):
if isinstance(param, Quantity):
value = QuantityValue(self, param)
elif isinstance(param, Selection):
value = SelectionValue(self, param)
else:
value = ParameterValue(self, param)

Expand Down
19 changes: 18 additions & 1 deletion concert/devices/dummy.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Dummy"""
from concert.base import Parameter
from concert.base import Parameter, Selection
from concert.devices.base import Device
from concert.async import async

Expand All @@ -26,3 +26,20 @@ def _set_value(self, value):
def do_nothing(self):
"""Do nothing."""
pass


class SelectionDevice(Device):

"""A dummy device with a selection."""

selection = Selection(range(3))

def __init__(self):
super(SelectionDevice, self).__init__()
self._selection = 0

def _get_selection(self):
return self._selection

def _set_selection(self, selection):
self._selection = selection
26 changes: 13 additions & 13 deletions concert/helpers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import inspect
import numpy as np


class Command(object):
Expand Down Expand Up @@ -188,25 +187,26 @@ def __init__(self, dimension, units=None):
self.units = units


class Range(object):
class Region(object):

"""A Range holds a :class:`~concert.base.Parameter` and a (minimum, maximum, intervals) range
assigned to it.
"""A Region holds a :class:`~concert.base.Parameter` and *values* which are the x-values of a
scan. You can create the values e.g. by numpy's *linspace* function::
import numpy as np
# minimum=0, maximum=10, intervals=100
values = np.linspace(0, 10, 100) * q.mm
"""

def __init__(self, parameter, minimum, maximum, intervals=64):
def __init__(self, parameter, values):
self.parameter = parameter
self.minimum = minimum
self.maximum = maximum
self.intervals = intervals
self.values = values

def __iter__(self):
"""Return the range."""
return (x for x in np.linspace(self.minimum, self.maximum, self.intervals))
"""Return region's iterator over its *values*."""
return iter(self.values)

def __repr__(self):
return 'Range({})'.format(str(self))
return 'Region({})'.format(str(self))

def __str__(self):
return '{}, {}, {}, {}'.format(self.parameter.name, self.minimum, self.maximum,
self.intervals)
return '{}: {}'.format(self.parameter.name, self.values)
67 changes: 48 additions & 19 deletions concert/processes.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,38 +22,63 @@ def _pull_first(tuple_list):
yield tup[0]


def scan(feedback, *ranges):
"""A multidimensional scan. *feedback* is a callable which takes no arguments and it provides
feedback after some parameter is changed. *ranges* specify the scanned parameters, they are
instances of :class:`concert.helpers.Range` class. The fastest changing parameter is the last
one specified. One would use it like this::
scan(feedback, Range(motor['position'], 0 * q.mm, 10 * q.mm, 10),
Range(camera['frame_rate'], 1 / q.s, 100 / q.s, 100))
def scan(feedback, regions, callbacks=None):
"""A multidimensional scan. *feedback* is a callable which takes no arguments and provides
feedback after some parameter is changed. *regions* specifies the scanned parameter, it is
either a :class:`concert.helpers.Region` or a list of those for multidimensional scan. The
fastest changing parameter is the last one specified. *callbacks* is a dictionary in the form
{region: function}, where *function* is a callable with no arguments (just like *feedback*) and
is called every time the parameter in *region* is changed. One would use a scan for example like
this::
import numpy as np
from concert.async import resolve
from concert.helpers import Region
def take_flat_field():
# Do something here
pass
exp_region = Region(camera['exposure_time'], np.linspace(1, 100, 100) * q.ms)
position_region = Region(motor['position'], np.linspace(0, 180, 1000) * q.deg)
callbacks = {exp_region: take_flat_field}
# This is a 2D scan with position_region in the inner loop. It acquires a tomogram, changes
# the exposure time and continues like this until all exposure times are exhausted.
# Take_flat_field is called every time the exposure_time of the camera is changed
# (in this case after every tomogram) and you can use it to correct the acquired images.
for result in resolve(scan(camera.grab, [exp_region, position_region], callbacks=callbacks)):
# Do something real instead of just a print
print result
From the execution order it is equivalent to (in reality there is more for making the code
asynchronous)::
for position in np.linspace(0 * q.mm, 10 * q.mm, 10):
for frame_rate in np.linspace(1 / q.s, 100 / q.s, 100):
for exp_time in np.linspace(1, 100, 100) * q.ms:
for position in np.linspace(0, 180, 1000) * q.deg:
yield feedback()
"""
changes = []
if not isinstance(regions, (list, tuple, np.ndarray)):
regions = [regions]

if callbacks is None:
callbacks = {}

# Changes store the indices at which parameters change, e.g. for two parameters and interval
# lengths 2 for first and 3 for second changes = [3, 1], i. e. first parameter is changed when
# the flattened iteration index % 3 == 0, second is changed every iteration.
# we do this because parameter setting might be expensive even if the value does not change
current_mul = 1
for i in range(len(ranges))[::-1]:
for i in range(len(regions))[::-1]:
changes.append(current_mul)
current_mul *= ranges[i].intervals
current_mul *= len(regions[i].values)
changes.reverse()

def get_changed(index):
"""Returns a tuple of indices of changed parameters at given iteration *index*."""
return [i for i in range(len(ranges)) if index % changes[i] == 0]
return [i for i in range(len(regions)) if index % changes[i] == 0]

@async
def get_value(index, tup, previous):
Expand All @@ -67,28 +92,32 @@ def get_value(index, tup, previous):
changed = get_changed(index)
futures = []
for i in changed:
futures.append(ranges[i].parameter.set(tup[i]))
futures.append(regions[i].parameter.set(tup[i]))
wait(futures)

for i in changed:
if regions[i] in callbacks:
callbacks[regions[i]]()

return tup + (feedback(),)

future = None

for i, tup in enumerate(product(*ranges)):
for i, tup in enumerate(product(*regions)):
future = get_value(i, tup, future)
yield future


def scan_param_feedback(scan_param_range, feedback_param):
def scan_param_feedback(scan_param_regions, feedback_param, callbacks=None):
"""
Convenience function to scan one parameter and measure another.
Convenience function to scan some parameters and measure another parameter.
Scan the *scan_param_range* object and measure *feedback_param*.
Scan the *scan_param_regions* parameters and measure *feedback_param*.
"""
def feedback():
return feedback_param.get().result()

return scan(feedback, scan_param_range)
return scan(feedback, scan_param_regions, callbacks=callbacks)


def ascan(param_list, n_intervals, handler, initial_values=None):
Expand Down
32 changes: 25 additions & 7 deletions concert/tests/integration/test_scan.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from itertools import product
import numpy as np
from concert.quantities import q
from concert.tests import assert_almost_equal, TestCase
from concert.devices.motors.dummy import LinearMotor
from concert.processes import scan, ascan, dscan, scan_param_feedback
from concert.async import resolve
from concert.helpers import Range
from concert.helpers import Region


def compare_sequences(first_sequence, second_sequence, assertion):
Expand Down Expand Up @@ -46,28 +47,32 @@ def test_process(self):
def feedback():
return self.motor.position

param_range = Range(self.motor['position'], 1 * q.mm, 10 * q.mm, 12)
values = np.linspace(1, 10, 12) * q.mm
param_range = Region(self.motor['position'], values)

x, y = zip(*resolve(scan(feedback, param_range)))
x, y = zip(*list(resolve(scan(feedback, param_range))))
compare_sequences(x, y, self.assertEqual)

def test_scan_param_feedback(self):
p = self.motor['position']
scan_param = Range(p, 1 * q.mm, 10 * q.mm, 10)
values = np.linspace(1, 10, 10) * q.mm
scan_param = Region(p, values)

x, y = zip(*resolve(scan_param_feedback(scan_param, p)))
compare_sequences(x, y, self.assertEqual)

def test_multiscan(self):
"""A 2D scan."""
other = LinearMotor()
range_0 = Range(self.motor['position'], 0 * q.mm, 10 * q.mm, 2)
range_1 = Range(other['position'], 5 * q.mm, 10 * q.mm, 3)
values_0 = np.linspace(0, 10, 2) * q.mm
values_1 = np.linspace(5, 10, 3) * q.mm
range_0 = Region(self.motor['position'], values_0)
range_1 = Region(other['position'], values_1)

def feedback():
return self.motor.position, other.position

gen = resolve(scan(feedback, range_0, range_1))
gen = resolve(scan(feedback, [range_0, range_1]))
p_0, p_1, result = zip(*gen)
result_x, result_y = zip(*result)

Expand All @@ -82,3 +87,16 @@ def feedback():
# feedback result is a tuple in this case, test both parts
compare_sequences(result_x, p_0_exp, assert_almost_equal)
compare_sequences(result_y, p_1_exp, assert_almost_equal)

def test_callback(self):
called = []
motor = LinearMotor()
values = np.linspace(0, 2, 3) * q.mm
qrange = Region(motor['position'], values)

def callback():
called.append(motor.position.to(q.mm).magnitude)

list(resolve(scan(lambda: None, qrange, callbacks={qrange: callback})))

np.testing.assert_almost_equal(called, range(3))
6 changes: 3 additions & 3 deletions concert/tests/unit/test_measures.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from concert.tests.util.rotationaxis import SimulationCamera
from concert.processes import scan
from concert.measures import rotation_axis
from concert.helpers import Range
from concert.helpers import Region


class TestRotationAxisMeasure(TestCase):
Expand All @@ -29,8 +29,8 @@ def setUp(self):
def make_images(self, x_angle, z_angle, intervals=10):
self.x_motor.position = z_angle
self.z_motor.position = x_angle
prange = Range(self.y_motor["position"], minimum=0 * q.rad, maximum=2 * np.pi * q.rad,
intervals=intervals)
values = np.linspace(0, 2 * np.pi, intervals) * q.rad
prange = Region(self.y_motor["position"], values)
result = [f.result()[1] for f in scan(self.image_source.grab, prange)]

return result
Expand Down
27 changes: 10 additions & 17 deletions concert/tests/unit/test_parameter.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import time
import numpy as np
from concert.quantities import q
from concert.tests import TestCase
from concert.base import (Parameterizable, Parameter, Quantity, State, Selection,
transition, check,
SoftLimitError, LockError, ParameterError,
UnitError, WriteAccessError)
from concert.base import (Parameterizable, Parameter, Quantity, State, transition, check,
SoftLimitError, LockError, ParameterError, UnitError,
WriteAccessError)
from concert.devices.dummy import SelectionDevice
from concert.async import WaitError


Expand Down Expand Up @@ -63,17 +64,6 @@ def _get_foo(self):
return self._value


class SelectionDevice(Parameterizable):

something = Selection([1, 2, 3])

def _get_something(self):
return 1

def _set_something(self, value):
pass


class TestDescriptor(TestCase):

def setUp(self):
Expand Down Expand Up @@ -272,8 +262,11 @@ def setUp(self):

def test_correct_access(self):
for i in range(3):
self.device.something = i + 1
self.device.selection = i

def test_wrong_access(self):
with self.assertRaises(WriteAccessError):
self.device.something = 4
self.device.selection = 4

def test_iterable_access(self):
np.testing.assert_equal(self.device['selection'].values, range(3))

0 comments on commit 3bea2ec

Please sign in to comment.