Skip to content

Commit

Permalink
Add .lower and .upper soft limits to Parameter
Browse files Browse the repository at this point in the history
This should fix #26.

Note: the changes require either PR 31 to be merged or supersede the PR.
  • Loading branch information
Matthias Vogelgesang committed Aug 19, 2013
1 parent 417bba9 commit 33d7f37
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 163 deletions.
60 changes: 47 additions & 13 deletions concert/base.py
Expand Up @@ -31,10 +31,10 @@
Parameter objects are not only used to communicate with a device but also carry
meta data information about the parameter. The most important ones are
:attr:`Parameter.name`, :attr:`Parameter.unit` and :attr:`Parameter.limiter` as
well as the doc string describing the parameter. Moreover, parameters can be
queried for access rights using :meth:`Parameter.is_readable` and
:meth:`Parameter.is_writable`.
:attr:`Parameter.name`, :attr:`Parameter.unit` and
:attr:`Parameter.in_hard_limit` as well as the doc string describing the
parameter. Moreover, parameters can be queried for access rights using
:meth:`Parameter.is_readable` and :meth:`Parameter.is_writable`.
To get all parameters of an object, you can iterate over the device itself ::
Expand All @@ -57,13 +57,19 @@ class UnitError(ValueError):
pass


class LimitError(Exception):
class LimitError(ValueError):

"""Raised when an operation is passed a value that exceeds a limit."""
pass


class HardlimitError(LimitError):
class SoftLimitError(LimitError):

"""Raised when a soft limit is hit on the device."""
pass


class HardLimitError(LimitError):

"""Raised when a hard limit is hit on the device."""
pass
Expand Down Expand Up @@ -147,7 +153,7 @@ def parameter_name_valid(name):
class Parameter(object):

"""
A parameter with a *name* and an optional *unit* and *limiter*.
A parameter with a *name* and an optional *unit* and *in_hard_limit*.
.. py:attribute:: name
Expand All @@ -158,34 +164,57 @@ class Parameter(object):
The unit that is expected when setting a value and that is returned. If
a unit is not compatible, a :class:`.UnitError` will be raised.
.. py:attribute:: limiter
.. py:attribute:: in_hard_limit
A callable that receives the value and returns True or False, depending
if the value is out of limits or not.
.. py:attribute:: upper
Upper soft limit that is checked before setting the value.
.. py:attribute:: lower
Lower soft limit that is checked before setting the value.
"""

CHANGED = 'changed'

def __init__(self, name, fget=None, fset=None,
unit=None, limiter=None,
unit=None, in_hard_limit=None,
upper=None, lower=None,
doc=None, owner_only=False):

if not parameter_name_valid(name):
raise ValueError('{0} is not a valid parameter name'.format(name))

self.name = name
self.unit = unit
self.limiter = limiter
self.in_hard_limit = in_hard_limit
self.owner = None
self._owner_only = owner_only
self._fset = fset
self._fget = fget
self._value = None
self.__doc__ = doc

self.upper = upper or float('Inf')
self.lower = lower or -float('Inf')

if unit and not upper:
self.upper = self.upper * unit

if unit and not lower:
self.lower = self.lower * unit

def __lt__(self, other):
return str(self) <= str(other)

def update_unit(self, unit):
self.unit = unit
self.upper = self.upper.magnitude * unit
self.lower = self.lower.magnitude * unit

@async
def get(self):
"""
Expand Down Expand Up @@ -225,9 +254,10 @@ def set(self, value, owner=None):
msg = "`{0}' can only receive values of unit {1} but got {2}"
raise UnitError(msg.format(self.name, self.unit, value))

if self.limiter and not self.limiter(value):
msg = "{0} for `{1}' is out of range"
raise LimitError(msg.format(value, self.name))
if not self.lower <= value <= self.upper:
msg = "{0} for `{1}' is out of range [{2}, {3}]"
raise SoftLimitError(msg.format(value, self.name,
self.lower, self.upper))

def log_access(what):
"""Log access."""
Expand All @@ -242,6 +272,10 @@ def log_access(what):
else:
self._fset(value)

if self.in_hard_limit and self.in_hard_limit():
msg = "{0} for `{1}' is out of range"
raise HardLimitError(msg.format(value, self.name))

log_access('set')
self.notify()

Expand Down
6 changes: 3 additions & 3 deletions concert/devices/motors/ankatango.py
Expand Up @@ -23,11 +23,11 @@ class Motor(base.Motor):

"""A motor based on ANKA Tango motor interface."""

def __init__(self, device, calibration, _position_limit=None):
super(Motor, self).__init__(calibration)
def __init__(self, device, calibration):
super(Motor, self).__init__(calibration, self._in_hard_limit)
self._device = device

def in_hard_limit(self):
def _in_hard_limit(self):
return self._device.BackwardLimitSwitch or \
self._device.ForwardLimitSwitch

Expand Down
45 changes: 18 additions & 27 deletions concert/devices/motors/base.py
Expand Up @@ -17,7 +17,7 @@
"""
import logbook
from concert.quantities import q
from concert.base import HardlimitError
from concert.base import HardLimitError
from concert.devices.base import Device, Parameter, LinearCalibration
from concert.asynchronous import async

Expand All @@ -43,12 +43,12 @@ class Motor(Device):
MOVING = 'moving'
LIMIT = 'limit'

def __init__(self, calibration=None, limiter=None):
params = [Parameter('position',
self._get_calibrated_position,
self._set_calibrated_position,
q.m, limiter,
"Position of the motor")]
def __init__(self, calibration=None, in_hard_limit=None):
params = [Parameter(name='position',
fget=self._get_calibrated_position,
fset=self._set_calibrated_position,
unit=q.m, in_hard_limit=in_hard_limit,
doc="Position of the motor")]

super(Motor, self).__init__(params)

Expand All @@ -59,7 +59,7 @@ def __init__(self, calibration=None, limiter=None):
calibration_unit = calibration.user_unit

if calibration_unit != self['position'].unit:
self['position'].unit = calibration_unit
self['position'].update_unit(calibration_unit)

self._states = \
self._states.union(set([self.STANDBY, self.MOVING, self.LIMIT]))
Expand Down Expand Up @@ -89,23 +89,12 @@ def home(self):
"""
self._home()

@classmethod
def in_hard_limit(cls):
"""Return *True* if motor device is in a limit state, otherwise
*False*."""
return False

def _get_calibrated_position(self):
return self._calibration.to_user(self._get_position())

def _set_calibrated_position(self, position):
self._set_state(self.MOVING)
self._set_position(self._calibration.to_device(position))

if self.in_hard_limit():
self._set_state(self.LIMIT)
raise HardlimitError("Hard limit reached")

self._set_state(self.STANDBY)

def _get_position(self):
Expand All @@ -130,15 +119,17 @@ class ContinuousMotor(Motor):
"""

def __init__(self, position_calibration, velocity_calibration,
position_limiter=None, velocity_limiter=None):
in_position_hard_limit=None,
in_velocity_hard_limit=None):
super(ContinuousMotor, self).__init__(position_calibration,
position_limiter)

param = Parameter('velocity',
self._get_calibrated_velocity,
self._set_calibrated_velocity,
q.m / q.s, velocity_limiter,
"Velocity of the motor")
in_position_hard_limit)

param = Parameter(name='velocity',
fget=self._get_calibrated_velocity,
fset=self._set_calibrated_velocity,
unit=q.m / q.s,
in_hard_limit=in_velocity_hard_limit,
doc="Velocity of the motor")

self.add_parameter(param)
self._velocity_calibration = velocity_calibration
Expand Down
46 changes: 17 additions & 29 deletions concert/devices/motors/dummy.py
Expand Up @@ -5,50 +5,38 @@
from concert.devices.base import LinearCalibration


class DummyLimiter(object):

"""Dummy Limiter class implementation."""

def __init__(self, low, high):
self.low = low
self.high = high

def __call__(self, value):
return self.low < value < self.high


class Motor(base.Motor):

"""Dummy Motor class implementation."""

def __init__(self, calibration=None,
limiter=None, position=None, hard_limits=None):
super(Motor, self).__init__(calibration, limiter)
if hard_limits is None:
self._hard_limits = -100, 100
else:
self._hard_limits = hard_limits
if not limiter:
self._position = random.uniform(self._hard_limits[0],
self._hard_limits[1]) * q.count
def __init__(self, calibration=None, position=None, hard_limits=None):
super(Motor, self).__init__(calibration=calibration,
in_hard_limit=self._in_hard_limit)

if hard_limits:
self.lower, self.upper = hard_limits
else:
self.lower, self.upper = -100, 100

if position:
self._position = position
else:
self._position = random.uniform(self.lower, self.upper) * q.count

def in_hard_limit(self):
return self._position < self._hard_limits[0] or not\
self._position < self._hard_limits[1]
def _in_hard_limit(self):
return self._position < self.lower or not self._position < self.upper

def _stop_real(self):
pass

def _set_position(self, position):
if position < self._hard_limits[0]:
self._position = self._hard_limits[0] * q.count
elif not position < self._hard_limits[1]:
if position < self.lower:
self._position = self.lower * q.count
elif not position < self.upper:
# We do this funny comparison because pint is able to compare
# "position < something" but not the other way around. See
# https://github.com/hgrecco/pint/issues/40
self._position = self._hard_limits[1] * q.count
self._position = self.upper * q.count
else:
self._position = position

Expand Down
2 changes: 1 addition & 1 deletion concert/processes/alignment.py
Expand Up @@ -30,5 +30,5 @@ def get_measure():

camera.start_recording()
f = maximizer.run()
f.add_done_callback(lambda: camera.stop_recording())
f.add_done_callback(lambda unused: camera.stop_recording())
return f
6 changes: 4 additions & 2 deletions concert/processes/scan.py
Expand Up @@ -76,8 +76,10 @@ class Scanner(Process):
"""

def __init__(self, param, feedback):
params = [Parameter('minimum', doc="Left bound of the interval"),
Parameter('maximum', doc="Right bound of the interval"),
params = [Parameter('minimum', unit=param.unit,
doc="Left bound of the interval"),
Parameter('maximum', unit=param.unit,
doc="Right bound of the interval"),
Parameter('intervals', doc="Number of intervals")]

super(Scanner, self).__init__(params)
Expand Down

0 comments on commit 33d7f37

Please sign in to comment.