Skip to content

Commit

Permalink
New AMI430 driver (#700)
Browse files Browse the repository at this point in the history
* Added environment.yml file

* Added requirements to environment.yml

* Added requirements to environment.yml

* Works under linux now

* 1) added american instrument driver from the main qcodes repo
2) added a mock IP module to mock IP instruments
3) added a debug module to debug the AMI430 instrument

* the mock IP instrument now remembers the set point

* added test for spherical coordinates. Making test for cylindrical coordinates. Work in progress

* Added a module for handy converting between spherical, cartesian and cylindrical coodinates. By keeping a prepresnetation of the vector in all three systems, there is less risk of loss of information so that e.g. the field strength can be ramped up given previous supplied phi and theta.

The spherical and cylindrical tests seem to work.

* additional work

* 1) The driver is adhering to the qcodes specs now
2) Made proper tests

* 1) The driver is adhering to the qcodes specs now
2) Made proper tests

* 1) Made tests to ensure that the set points correctly reach the instruments
2) Made test to verify that the correct error is raised if we intentionally exceed the field limits

* Added a test to verify that ramp downs are performed first

* Added the ability to define more complex regions for safe set points. Tests to verify this are also included.

* Re-wrote the instrument mocker so we have a cleaner design.

* changing how mocking works

* Mocking no longer requires sockets and threading. Major rewrite of the mocking mechanism

* Mocking no longer requires sockets and threading. Major rewrite of the mocking mechanism

* further clean up of the mocking mechanism.

* Solved a bug which raised a value error when asking for spherical coordinates. Added tests to verify if we can get spherical coordinates. Also, when getting single field values, return a single float instead of an array of length one

* 1) Added tests for the field vector
2) Added test to verify that the correct warnings are raised if we change the default maximum ramp rate
3) Added test to verify that an error is raised if the ramp rate larger then the maximum

* Added a notebook showing the results of the testing of the new AMI430 driver

* * Processed Jens comments, except changing the doc string format and pep8 conventions. Will do that tomorrow

* Processed the rest of Jens comments
  • Loading branch information
sohailc authored and jenshnielsen committed Sep 4, 2017
1 parent acb3b6e commit 5acbeeb
Show file tree
Hide file tree
Showing 13 changed files with 1,784 additions and 472 deletions.
6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

454 changes: 454 additions & 0 deletions docs/examples/driver_examples/Qcodes example with AMI430.ipynb

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions environment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: qcodes
channels:
- defaults
- conda-forge
dependencies:
- h5py=2.6.0
- matplotlib=2.0.2
- pyqtgraph
- python=3.6
- jsonschema
- pyvisa
- QtPy=1.2.0
- sip
- pyqt=5.6.0
- numpy
- jupyter=1.0.0
- typing
- hypothesis
- pytest
- pytest-runner
64 changes: 50 additions & 14 deletions qcodes/instrument/base.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
"""Instrument base class."""
import logging
import numpy as np
import time
import warnings
import weakref
from typing import Sequence

from qcodes.utils.metadata import Metadatable
import numpy as np

from qcodes.utils.helpers import DelegateAttributes, strip_attrs, full_class
from qcodes.utils.metadata import Metadatable
from qcodes.utils.validators import Anything
from .parameter import StandardParameter
from .function import Function
from .parameter import StandardParameter


class InstrumentBase(Metadatable, DelegateAttributes):
Expand Down Expand Up @@ -38,13 +39,38 @@ class InstrumentBase(Metadatable, DelegateAttributes):
such as channel lists or logical groupings of parameters.
Usually populated via ``add_submodule``
"""
def __init__(self, name, **kwargs):

def __init__(self, name, testing=False, **kwargs):
self.name = str(name)
self._testing = testing

if testing:
if hasattr(type(self), "mocker_class"):
mocker_class = type(self).mocker_class
self.mocker = mocker_class(name)
else:
raise ValueError("Testing turned on but no mocker class defined")

self.parameters = {}
self.functions = {}
self.submodules = {}
super().__init__(**kwargs)

def is_testing(self):
"""Return True if we are testing"""
return self._testing

def get_mock_messages(self):
"""
For testing purposes we might want to get log messages from the mocker.
Returns:
mocker_messages: list, str
"""
if not self._testing:
raise ValueError("Cannot get mock messages if not in testing mode")
return self.mocker.get_log_messages()

def add_parameter(self, name, parameter_class=StandardParameter,
**kwargs):
"""
Expand Down Expand Up @@ -150,12 +176,13 @@ def snapshot_base(self, update: bool=False,
Returns:
dict: base snapshot
"""
snap = {'functions': dict((name, func.snapshot(update=update))
for name, func in self.functions.items()),
'submodules': dict((name, subm.snapshot(update=update))
for name, subm in self.submodules.items()),
'__class__': full_class(self),
}

snap = {
"functions": {name: func.snapshot(update=update) for name, func in self.functions.items()},
"submodules": {name: subm.snapshot(update=update) for name, subm in self.submodules.items()},
"__class__": full_class(self)
}

snap['parameters'] = {}
for name, param in self.parameters.items():
update = update
Expand Down Expand Up @@ -335,12 +362,12 @@ class Instrument(InstrumentBase):

_all_instruments = {}

def __init__(self, name, **kwargs):
def __init__(self, name, testing=False, **kwargs):
self._t0 = time.time()
if kwargs.pop('server_name', False):
warnings.warn("server_name argument not supported any more",
stacklevel=0)
super().__init__(name, **kwargs)
super().__init__(name, testing=testing, **kwargs)

self.add_parameter('IDN', get_cmd=self.get_idn,
vals=Anything())
Expand Down Expand Up @@ -576,7 +603,10 @@ def write(self, cmd):
including the command and the instrument.
"""
try:
self.write_raw(cmd)
if self._testing:
self.mocker.write(cmd)
else:
self.write_raw(cmd)
except Exception as e:
e.args = e.args + ('writing ' + repr(cmd) + ' to ' + repr(self),)
raise e
Expand Down Expand Up @@ -615,7 +645,13 @@ def ask(self, cmd):
including the command and the instrument.
"""
try:
return self.ask_raw(cmd)
if self._testing:
answer = self.mocker.ask(cmd)
else:
answer = self.ask_raw(cmd)

return answer

except Exception as e:
e.args = e.args + ('asking ' + repr(cmd) + ' to ' + repr(self),)
raise e
Expand Down
11 changes: 9 additions & 2 deletions qcodes/instrument/ip.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ class IPInstrument(Instrument):
"""

def __init__(self, name, address=None, port=None, timeout=5,
terminator='\n', persistent=True, write_confirmation=True,
terminator='\n', persistent=True, write_confirmation=True, testing=False,
**kwargs):
super().__init__(name, **kwargs)
super().__init__(name, testing=testing, **kwargs)

self._address = address
self._port = port
Expand Down Expand Up @@ -88,7 +88,14 @@ def set_persistent(self, persistent):
else:
self._disconnect()

def flush_connection(self):
if not self._testing:
self._recv()

def _connect(self):
if self._testing:
return

if self._socket is not None:
self._disconnect()

Expand Down
Empty file.
217 changes: 217 additions & 0 deletions qcodes/instrument/mockers/ami430.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import re
import time
from datetime import datetime


class MockAMI430:
states = {
"RAMPING to target field/current": "1",
"HOLDING at the target field/current": "2",
"PAUSED": "3",
"Ramping in MANUAL UP mode": "4",
"Ramping in MANUAL DOWN mode": "5",
"ZEROING CURRENT (in progress)": "6",
"Quench detected": "7",
"At ZERO current": "8",
"Heating persistent switch": "9",
"Cooling persistent switch": "10"
}

field_units = {
"tesla": "1",
"kilogauss": "0"
}

ramp_rate_units = {
"A/s": "0",
"A/min": "1"
}

quench_state = {False: "0", True: "1"}

def __init__(self, name):

self.name = name
self.log_messages = []

self._field_mag = 0
self._field_target = 0
self._state = MockAMI430.states["HOLDING at the target field/current"]

self.handlers = {
"RAMP:RATE:UNITS": {
"get": MockAMI430.ramp_rate_units["A/s"],
"set": None
},
"FIELD:UNITS": {
"get": MockAMI430.field_units["tesla"],
"set": None
},
"*IDN": {
"get": "v0.1 Mock",
"set": None
},
"STATE": {
"get": self._getter("_state"),
"set": self._setter("_state")
},
"FIELD:MAG": {
"get": self._getter("_field_mag"),
"set": None
},
"QU": {
"get": MockAMI430.quench_state[False], # We are never in a quenching state so always return the
# same value
"set": None
},
"PERS": {
"get": "0",
"set": None
},
"PAUSE": {
"get": self._is_paused,
"set": self._do_pause
},
"CONF:FIELD:TARG": {
"get": None, # To get the field target, send a message "FIELD:TARG?"
"set": self._setter("_field_target")
},
"FIELD:TARG": {
"get": self._getter("_field_target"),
"set": None
},
"PS": {
"get": "0", # The heater is off
"set": None
},
"RAMP": {
"set": self._do_ramp,
"get": None
},
"RAMP:RATE:CURRENT": {
"get": "0.1000,50.0000",
"set": None
},
"COIL": {
"get": "1",
"set": None
}
}

@staticmethod
def message_parser(gs, msg_str, key):
"""
* If gs = "get":
Let suppose key = "RAMP:RATE:UNITS", then if we get msg_str = "RAMP:RATE:UNITS?" then match will be True and
args = None. If msg_str = "RAMP:RATE:UNITS:10?" then match = True and args = "10". On the other hand if
key = "RAMP" then both "RAMP:RATE:UNITS?" and "RAMP:RATE:UNITS:10?" will cause match to be False
* If gs = "set"
If key = "STATE" and msg_str = "STATE 2,1" then match = True and args = "2,1". If key="STATE" and
msg_str = STATE:ELSE 2,1 then match is False.
Consult [1] for a complete description of the AMI430 protocol.
[1] http://www.americanmagnetics.com/support/manuals/mn-4Q06125PS-430.pdf
Parameters:
gs (string): "get", or "set"
msg_str (string): the message string the mock instrument gets.
key (string): one of the keys in self.handlers
Returns:
match (bool): if the key and the msg_str match, then match = True
args (string): if any arguments are present in the message string these will be passed along. This is
always None when match = False
"""
if msg_str == key: # If the message string matches a key exactly we have a match with no arguments
return True, None

# We use regular expressions to find out if the message string and the key match. We need to replace reserved
# regular expression characters in the key. For instance replace "*IDN" with "\*IDN".
reserved_re_characters = "\^${}[]().*+?|<>-&"
for c in reserved_re_characters:
key = key.replace(c, "\{}".format(c))

s = {"get": "(:[^:]*)?\?$", "set": "([^:]+)"}[gs] # Get and set messages use different regular expression
# patterns to determine a match
search_string = "^" + key + s
r = re.search(search_string, msg_str)
match = r is not None

args = None
if match:
args = r.groups()[0]
if args is not None:
args = args.strip(":")

return match, args

def _getter(self, attribute):
return lambda _: getattr(self, attribute)

def _setter(self, attribute):
return lambda value: setattr(self, attribute, value)

def _log(self, msg):

now = datetime.now()
log_line = "[{}] {}: {}".format(now.strftime("%d:%m:%Y-%H:%M:%S.%f"), self.name, msg)
self.log_messages.append(log_line)

def _handle_messages(self, msg):
"""
Parameters:
msg (string): a message received through the socket communication layer
Returns:
rval (string or None): If the type of message requests a value (a get message) then this value is returned
by this function. A set message will return a None value.
"""

gs = {True: "get", False: "set"}[msg.endswith("?")] # A "get" message ends with a "?" and will invoke the get
# part of the handler defined in self.handlers.

rval = None
handler = None

for key in self.handlers: # Find which handler is suitable to handle the message
match, args = MockAMI430.message_parser(gs, msg, key)
if not match:
continue

handler = self.handlers[key][gs]
if callable(handler):
rval = handler(args)
else:
rval = handler

break

if handler is None:
self._log("Command {} unknown".format(msg))

return rval

def _do_pause(self, _):
self._state = MockAMI430.states["PAUSED"]

def _is_paused(self):
return self._state == MockAMI430.states["PAUSED"]

def _do_ramp(self, _):
self._log("Ramping to {}".format(self._field_target))
self._state = MockAMI430.states["RAMPING to target field/current"]
time.sleep(0.1) # Lets pretend to be ramping for a bit
self._field_mag = self._field_target
self._state = MockAMI430.states["HOLDING at the target field/current"]

def get_log_messages(self):
return self.log_messages

def ask(self, msg):
return self._handle_messages(msg)

def write(self, msg):
self._handle_messages(msg)
Loading

0 comments on commit 5acbeeb

Please sign in to comment.