Skip to content
Closed
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
1 change: 1 addition & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mock
nose
pylint
ruamel.yaml

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If regular yaml isn't being really developed anymore, lets just kill it and move to using ruamel.yaml

3 changes: 2 additions & 1 deletion doc/source/intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ $ pip install -r requirements.txt
- `python-vxi11`_
- `PyUSB`_
- `python-usbtmc`_
- `PyYAML`_
- `PyYAML`_ or `ruamel.yaml`_

Optional Dependencies
~~~~~~~~~~~~~~~~~~~~~
Expand All @@ -47,6 +47,7 @@ Optional Dependencies
.. _enum34: https://pypi.python.org/pypi/enum34
.. _future: https://pypi.python.org/pypi/future
.. _PyYAML: https://bitbucket.org/xi/pyyaml
.. _ruamel.yaml: http://yaml.readthedocs.io
.. _PyUSB: http://sourceforge.net/apps/trac/pyusb/
.. _PyVISA: http://pyvisa.sourceforge.net/
.. _python-usbtmc: https://pypi.python.org/pypi/python-usbtmc
Expand Down
6 changes: 5 additions & 1 deletion instruments/abstract_instruments/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,8 @@ def binblockread(self, data_width, fmt=None):
# CLASS METHODS #

URI_SCHEMES = ["serial", "tcpip", "gpib+usb",
"gpib+serial", "visa", "file", "usbtmc", "vxi11"]
"gpib+serial", "visa", "file", "usbtmc", "vxi11",
"test"]

@classmethod
def open_from_uri(cls, uri):
Expand All @@ -330,6 +331,7 @@ def open_from_uri(cls, uri):
gpib+serial:///dev/ttyACM0/15 # Currently non-functional.
visa://USB::0x0699::0x0401::C0000001::0::INSTR
usbtmc://USB::0x0699::0x0401::C0000001::0::INSTR
test://

For the ``serial`` URI scheme, baud rates may be explicitly specified
using the query parameter ``baud=``, as in the example
Expand Down Expand Up @@ -415,6 +417,8 @@ def open_from_uri(cls, uri):
# vxi11://192.168.1.104
# vxi11://TCPIP::192.168.1.105::gpib,5::INSTR
return cls.open_vxi11(parsed_uri.netloc, **kwargs)
elif parsed_uri.scheme == "test":
return cls.open_test(**kwargs)
else:
raise NotImplementedError("Invalid scheme or not yet "
"implemented.")
Expand Down
71 changes: 63 additions & 8 deletions instruments/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,18 @@
import warnings

try:
import yaml
import ruamel_yaml as yaml
except ImportError:
yaml = None
try:
import yaml
except ImportError:
yaml = None

import quantities as pq

from future.builtins import str

from instruments.util_fns import setattr_expression, split_unit_str

# FUNCTIONS ###################################################################

Expand All @@ -37,15 +46,29 @@ def walk_dict(d, path):
# Treat as a base case that the path is empty.
if not path:
return d
if isinstance(path, str):
if not isinstance(path, list):
path = path.split("/")

if not path[0]:
# If the first part of the path is empty, do nothing.
return walk_dict(d, path[1:])
else:
# Otherwise, resolve that segment and recurse.
return walk_dict(d[path[0]], path[1:])

def quantity_constructor(loader, node):
"""
Constructs a `pq.Quantity` instance from a PyYAML
node tagged as ``!Q``.
"""
# Follows the example of http://stackoverflow.com/a/43081967/267841.
value = loader.construct_scalar(node)
return pq.Quantity(*split_unit_str(value))

# We avoid having to register !Q every time by doing as soon as the
# relevant constructor is defined.
if yaml:
yaml.add_constructor(u'!Q', quantity_constructor)

def load_instruments(conf_file_name, conf_path="/"):
"""
Expand All @@ -63,6 +86,28 @@ def load_instruments(conf_file_name, conf_path="/"):
the form
``{'ddg': instruments.srs.SRSDG645.open_from_uri('gpib+usb://COM7/15')}``.

Each instrument configuration section can also specify one or more attributes
to set. These attributes are specified using a ``attrs`` section as well as the
required ``class`` and ``uri`` sections. For instance, the following
dictionary creates a ThorLabs APT motor controller instrument with a single motor
model configured::

rot_stage:
class: !!python/name:instruments.thorabsapt.APTMotorController
uri: serial:///dev/ttyUSB0?baud=115200
attrs:
channel[0].motor_model: PRM1-Z8

Unitful attributes can be specified by using the ``!Q`` tag to quickly create
instances of `pq.Quantity`. In the example above, for instance, we can set a motion
timeout as a unitful quantity::

attrs:
motion_timeout: !Q 1 minute

When using the ``!Q`` tag, any text before a space is taken to be the magnitude
of the quantity, and text following is taken to be the unit specification.

By specifying a path within the configuration file, one can load only a part
of the given file. For instance, consider the configuration::

Expand All @@ -78,7 +123,7 @@ def load_instruments(conf_file_name, conf_path="/"):
all other keys in the YAML file.

:param str conf_file_name: Name of the configuration file to load
instruments from.
instruments from. Alternatively, a file-like object may be provided.
:param str conf_path: ``"/"`` separated path to the section in the
configuration file to load.

Expand All @@ -98,20 +143,30 @@ def load_instruments(conf_file_name, conf_path="/"):
raise ImportError("Could not import PyYAML, which is required "
"for this function.")

with open(conf_file_name, 'r') as f:
conf_dict = yaml.load(f)
if isinstance(conf_file_name, str):
with open(conf_file_name, 'r') as f:
conf_dict = yaml.load(f)
else:
conf_dict = yaml.load(conf_file_name)

conf_dict = walk_dict(conf_dict, conf_path)

inst_dict = {}
for name, value in conf_dict.iteritems():
for name, value in conf_dict.items():
try:
inst_dict[name] = value["class"].open_from_uri(value["uri"])

if 'attrs' in value:
# We have some attrs we can set on the newly created instrument.
for attr_name, attr_value in value['attrs'].items():
setattr_expression(inst_dict[name], attr_name, attr_value)

except IOError as ex:
# FIXME: need to subclass Warning so that repeated warnings
# aren't ignored.
warnings.warn("Exception occured loading device URI "
warnings.warn("Exception occured loading device with URI "
"{}:\n\t{}.".format(value["uri"], ex), RuntimeWarning)
inst_dict[name] = None


return inst_dict
64 changes: 64 additions & 0 deletions instruments/tests/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Module containing tests for util_fns.py
"""

# IMPORTS ####################################################################

from __future__ import absolute_import, unicode_literals

from io import StringIO

import quantities as pq

from instruments import Instrument
from instruments.config import (
load_instruments, yaml
)

# TEST CASES #################################################################

# pylint: disable=protected-access,missing-docstring

def test_load_test_instrument():
config_data = StringIO(u"""
test:
class: !!python/name:instruments.Instrument
uri: test://
""")
insts = load_instruments(config_data)
assert isinstance(insts['test'], Instrument)

def test_load_test_instrument_subtree():
config_data = StringIO(u"""
instruments:
test:
class: !!python/name:instruments.Instrument
uri: test://
""")
insts = load_instruments(config_data, conf_path="/instruments")
assert isinstance(insts['test'], Instrument)

def test_yaml_quantity_tag():
yaml_data = StringIO(u"""
a:
b: !Q 37 tesla
c: !Q 41.2 inches
d: !Q 98
""")
data = yaml.load(yaml_data)
assert data['a']['b'] == pq.Quantity(37, 'tesla')
assert data['a']['c'] == pq.Quantity(41.2, 'inches')
assert data['a']['d'] == 98

def test_load_test_instrument_setattr():
config_data = StringIO(u"""
test:
class: !!python/name:instruments.Instrument
uri: test://
attrs:
foo: !Q 111 GHz
""")
insts = load_instruments(config_data)
assert insts['test'].foo == pq.Quantity(111, 'GHz')
45 changes: 44 additions & 1 deletion instruments/tests/test_util_fns.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@

from instruments.util_fns import (
ProxyList,
assume_units, convert_temperature
assume_units, convert_temperature,
setattr_expression
)

# TEST CASES #################################################################
Expand Down Expand Up @@ -169,3 +170,45 @@ def test_temperater_conversion_failure():
@raises(ValueError)
def test_assume_units_failures():
assume_units(1, 'm').rescale('s')

def test_setattr_expression_simple():
class A(object):
x = 'x'
y = 'y'
z = 'z'

a = A()
setattr_expression(a, 'x', 'foo')
assert a.x == 'foo'

def test_setattr_expression_index():
class A(object):
x = ['x', 'y', 'z']

a = A()
setattr_expression(a, 'x[1]', 'foo')
assert a.x[1] == 'foo'

def test_setattr_expression_nested():
class B(object):
x = 'x'
class A(object):
b = None
def __init__(self):
self.b = B()

a = A()
setattr_expression(a, 'b.x', 'foo')
assert a.b.x == 'foo'

def test_setattr_expression_both():
class B(object):
x = 'x'
class A(object):
b = None
def __init__(self):
self.b = [B()]

a = A()
setattr_expression(a, 'b[0].x', 'foo')
assert a.b[0].x == 'foo'
37 changes: 36 additions & 1 deletion instruments/thorlabs/thorlabsapt.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import re
import struct
import logging
import warnings

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's split this into another PR after the yaml stuff


from builtins import range
import quantities as pq
Expand Down Expand Up @@ -443,6 +444,8 @@ class MotorChannel(ThorLabsAPT.APTChannel):

# INSTANCE VARIABLES #

_motor_model = None

#: Sets the scale between the encoder counts and physical units
#: for the position, velocity and acceleration parameters of this
#: channel. By default, set to dimensionless, indicating that the proper
Expand Down Expand Up @@ -496,7 +499,7 @@ class MotorChannel(ThorLabsAPT.APTChannel):

# UNIT CONVERSION METHODS #

def set_scale(self, motor_model):
def _set_scale(self, motor_model):
"""
Sets the scale factors for this motor channel, based on the model
of the attached motor and the specifications of the driver of which
Expand All @@ -517,6 +520,38 @@ def set_scale(self, motor_model):
logger.warning("Scale factors for controller %s and motor %s are "
"unknown", self._apt.model_number, motor_model)

# We copy the docstring below, so it's OK for this method
# to not have a docstring of its own.
# pylint: disable=missing-docstring
def set_scale(self, motor_model):
warnings.warn(
"The set_scale method has been deprecated in favor "
"of the motor_model property.",
DeprecationWarning
)
return self._set_scale(motor_model)

set_scale.__doc__ = _set_scale.__doc__

@property
def motor_model(self):
"""
Gets or sets the model name of the attached motor.
Note that the scale factors for this motor channel are based on the model
of the attached motor and the specifications of the driver of which
this is a channel, such that setting a new motor model will update
the scale factors accordingly.

:type: `str` or `None`
"""
return self._motor_model

@motor_model.setter
def motor_model(self, newval):
self._set_scale(newval)
self._motor_model = newval


# MOTOR COMMANDS #

@property
Expand Down
Loading