Skip to content

Commit

Permalink
Merge pull request #8 from klauer/enh_axis_sign_inversion
Browse files Browse the repository at this point in the history
ENH: axis sign inversion
  • Loading branch information
ericdill authored Jun 20, 2016
2 parents 4a08c21 + f0b7557 commit 3a8fcf2
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 49 deletions.
144 changes: 99 additions & 45 deletions hkl/calc.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,30 @@ def __init__(self, msg, pseudo, physical):


class CalcRecip(object):
def __init__(self, dtype, engine='hkl',
sample='main', lattice=None,
degrees=True, units='user',
lock_engine=False):
'''Reciprocal space calculations
Parameters
----------
dtype : str
Diffractometer type (usually specified by a subclass)
engine : str, optional
'hkl', for example
sample : str, optional
Default sample name (default: 'main')
lattice : Lattice, optional
Lattice to use with the default sample
degrees : bool, optional
Use degrees instead of radians (default: True)
units : {'user', }
The type of units to use internally
lock_engine : bool, optional
Don't allow the engine to be changed during the life of this object
inverted_axes : list, optional
Names of axes to invert the sign of
'''
def __init__(self, dtype, engine='hkl', sample='main', lattice=None,
degrees=True, units='user', lock_engine=False,
inverted_axes=None):

self._engine = None # set below with property
self._detector = util.new_detector()
Expand All @@ -79,7 +99,9 @@ def __init__(self, dtype, engine='hkl',
self._units = util.units[self._unit_name]
self._lock_engine = bool(lock_engine)
self._lock = RLock()
self._axis_name_map = None
self._axis_name_to_renamed = {}
self._axis_name_to_original = {}
self._inverted_axes = inverted_axes

try:
self._factory = hkl_module.factories()[dtype]
Expand Down Expand Up @@ -112,7 +134,6 @@ def Position(self):
@property
def wavelength(self):
'''The wavelength associated with the geometry, in nm'''
# TODO hkl lib doesn't expose the getter, only the setter
return self._geometry.wavelength_get(self._units)

@wavelength.setter
Expand Down Expand Up @@ -270,46 +291,83 @@ def parameters(self):

@property
def physical_axis_names(self):
if self._axis_name_map:
return list(self._axis_name_map.values())
if self._axis_name_to_renamed:
return list(self._axis_name_to_renamed.values())
else:
return self._geometry.axis_names_get()

@physical_axis_names.setter
def physical_axis_names(self, axis_name_map):
'''Set a persistent re-map of physical axis names
Parameter
---------
axis_name_map : dict {orig_axis_1: new_name_1, ...}
Resets `inverted_axes`.
Parameters
----------
axis_name_map : dict
{orig_axis_1: new_name_1, ...}
'''
# make sure re-map names are 1-to-1 with the engine's expectations
assert set(axis_name_map.keys()) == set(self.physical_axis_names)
internal_axis_names = self._geometry.axis_names_get()
if set(axis_name_map.keys()) != set(internal_axis_names):
raise ValueError('Every axis name has to have a remapped name')

self._axis_name_to_original = OrderedDict(
(axis_name_map[axis], axis) for axis in internal_axis_names)
self._axis_name_to_renamed = OrderedDict(
(axis, axis_name_map[axis]) for axis in internal_axis_names)

self._axis_name_map = self.physical_axes
for k, v in axis_name_map.items():
self._axis_name_map[k] = v
self._inverted_axes = []

@property
def inverted_axes(self):
'''The physical axis names to invert'''
return self._inverted_axes

@inverted_axes.setter
def inverted_axes(self, to_invert):
for axis in to_invert:
assert axis in self.physical_axis_names

self._inverted_axes = to_invert

def _invert_physical_positions(self, pos):
'''Invert the physical axis positions based on the settings
Parameters
----------
pos : OrderedDict
NOTE: Modified in-place
'''
for axis in self._inverted_axes:
pos[axis] = -pos[axis]
return pos

@property
def physical_positions(self):
return self._geometry.axis_values_get(self._units)
'''Physical (real) motor positions'''
pos = self.physical_axes
if self._inverted_axes:
pos = self._invert_physical_positions(pos)

return self.Position(*pos.values())

@physical_positions.setter
@_locked
def physical_positions(self, positions):
if self._inverted_axes:
pos = self.Position(*positions)._asdict()
pos = self._invert_physical_positions(pos)
positions = list(pos.values())

# Set the physical motor positions and calculate the pseudo ones
self._geometry.axis_values_set(positions, self._units)
self.update()

@property
def physical_axes(self):
if self._axis_name_map:
keys = list(self._axis_name_map.values())
else:
keys = self.physical_axis_names

positions = self.physical_positions
return OrderedDict(zip(keys, positions))
'''Physical (real) motor positions as an OrderedDict'''
return OrderedDict(zip(self.physical_axis_names,
self._geometry.axis_values_get(self._units)))

@property
def pseudo_axis_names(self):
Expand All @@ -323,42 +381,38 @@ def pseudo_positions(self):

@property
def pseudo_axes(self):
'''Dictionary of axis name to position'''
'''Ordered dictionary of axis name to position'''
return self._engine.pseudo_axes

def update(self):
'''Calculate the pseudo axis positions from the real axis positions'''
return self._engine.update()

def _get_parameter(self, param):
return Parameter(param, units=self._unit_name)
def _get_axis_by_name(self, name):
'''Given an axis name, return the HklParameter
Parameters
----------
name : str
If a name map is specified, this is the mapped name.
'''
name = self._axis_name_to_original.get(name, name)
return self._geometry.axis_get(name)

@property
def units(self):
'''The units used for calculations'''
return self._unit_name

def __getitem__(self, axis):
if axis in self.physical_axis_names:
if self._axis_name_map:
for k, v in self._axis_name_map.items():
if axis == v:
param = self._get_parameter(self._geometry.axis_get(k))
# cannot set Parameter.name, so these are as
# provided from below
# param.name = axis
return param

return self._get_parameter(self._geometry.axis_get(axis))
elif axis in self.pseudo_axis_names:
return self._engine[axis]
return Parameter(self._get_axis_by_name(axis),
units=self._unit_name,
name=axis,
inverted=axis in self._inverted_axes)

def __setitem__(self, axis, value):
if axis in self.physical_axis_names:
param = self[axis]
param.value = value
elif axis in self.pseudo_axis_names:
self._engine[axis] = value
param = self[axis]
param.value = value

@_keep_physical_position
def forward_iter(self, start, end, max_iters, *, threshold=0.99,
Expand Down
30 changes: 26 additions & 4 deletions hkl/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,18 @@


class Parameter(object):
def __init__(self, param, units='user'):
def __init__(self, param, units='user', name=None,
inverted=False):
self._param = param
self._unit_name = units
self._units = util.units[units]
self._name = name
self._inverted = inverted

@property
def inverted(self):
'''Is the value inverted internally?'''
return self._inverted

@property
def hkl_parameter(self):
Expand All @@ -27,7 +35,10 @@ def units(self):

@property
def name(self):
return self._param.name_get()
name = self._param.name_get()
if self._name != name:
return '{} (internally: {})'.format(self._name, name)
return name

@property
def value(self):
Expand All @@ -45,6 +56,9 @@ def default_units(self):

@value.setter
def value(self, value):
if self._inverted:
value *= -1.0

self._param.value_set(value, self._units)

@property
Expand All @@ -58,18 +72,26 @@ def fit(self, fit):

@property
def limits(self):
return self._param.min_max_get(self._units)
if self._inverted:
low, high = self._param.min_max_get(self._units)
return [-high, -low]
else:
return self._param.min_max_get(self._units)

@limits.setter
def limits(self, lims):
low, high = lims
self._param.min_max_set(low, high, self._units)
if self._inverted:
self._param.min_max_set(-high, -low, self._units)
else:
self._param.min_max_set(low, high, self._units)

def _repr_info(self):
repr = ['name={!r}'.format(self.name),
'limits={!r}'.format(self.limits),
'value={!r}'.format(self.value),
'fit={!r}'.format(self.fit),
'inverted={!r}'.format(self.inverted),
]

if self._unit_name == 'user':
Expand Down
26 changes: 26 additions & 0 deletions tests/test_tardis.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,32 @@ def test_reachable(tardis, sample):
numpy.testing.assert_almost_equal(tardis.calc.physical_positions, rpos)


def test_inversion(tardis, sample):
constrain(tardis)
ppos = (0, 0, 1.1)
rpos = (101.56806493825435, 0.0, 0.0, 0.0, 42.02226419522791,
# invert gamma for this test:
-176.69158155966787)

tardis.calc.inverted_axes = ['gamma']
tardis.calc.physical_positions = rpos

assert not tardis.calc['omega'].inverted
gamma = tardis.calc['gamma']
assert gamma.inverted
numpy.testing.assert_almost_equal(gamma.limits,
(-180.0, 5.0) # inverted from (-5, 180)
)
gamma.limits = (-180.0, 5.0)
numpy.testing.assert_almost_equal(gamma.limits,
(-180.0, 5.0) # inverted from (-5, 180)
)


numpy.testing.assert_almost_equal(tardis.calc.physical_positions, rpos)
numpy.testing.assert_almost_equal(tardis.calc.inverse(rpos), ppos)


def test_unreachable(tardis, sample):
print('position is', tardis.position)
with pytest.raises(UnreachableError) as exinfo:
Expand Down

0 comments on commit 3a8fcf2

Please sign in to comment.