Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Param parse #25

Merged
merged 7 commits into from
Feb 9, 2016
Merged
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
6 changes: 3 additions & 3 deletions docs/examples/toymodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def __init__(self, name, model):
label='Gate Channel {} (mV)'.format(i),
get_cmd=cmdbase + '?',
set_cmd=cmdbase + ' {:.4f}',
parse_function=float,
get_parser=float,
vals=Numbers(-100, 100))

self.add_function('reset', call_cmd='rst')
Expand All @@ -93,7 +93,7 @@ def __init__(self, name, model):
label='Source Amplitude (\u03bcV)',
get_cmd='ampl?',
set_cmd='ampl {:.4f}',
parse_function=float,
get_parser=float,
vals=Numbers(0, 10),
sweep_step=0.1,
sweep_delay=0.05)
Expand All @@ -106,7 +106,7 @@ def __init__(self, name, model):
self.add_parameter('amplitude',
label='Current (nA)',
get_cmd='ampl?',
parse_function=float)
get_parser=float)


class AverageGetter(Parameter):
Expand Down
39 changes: 30 additions & 9 deletions qcodes/instrument/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

class Function(Metadatable):
def __init__(self, instrument, name, call_cmd=None, async_call_cmd=None,
parameters=[], parse_function=None, **kwargs):
parameters=[], parameter_parser=None, return_parser=None,
parse_function=None, **kwargs):
'''
defines a function (with arbitrary parameters) that this instrument
can execute.
Expand All @@ -17,24 +18,41 @@ def __init__(self, instrument, name, call_cmd=None, async_call_cmd=None,

instrument: an instrument that handles this function
name: the local name of this parameter

call_cmd: command to execute on instrument
- a string (with positional fields to .format, "{}" or "{0}" etc)
- a function (with parameter count matching parameters list)
async_call_cmd: an async function to use for call_async, or for both
sync and async if call_cmd is missing or None.

parameters: list of Validator objects,
one for each parameter to the Function
parse_function: function to parse the return value of cmd,
may be a type casting function like int or float.
If None, will not wait for or read any response

parameter_parser: function to transform the input parameter(s)
to encoded value(s) sent to the instrument.
If there are multiple arguments, this function should accept all
the arguments in order, and return a tuple of values.
return_parser: function to transform the response from the instrument
to the final output value.
may be a type casting function like `int` or `float`.
If None (default), will not wait for or read any response
NOTE: parsers only apply if call_cmd is a string. The function forms
of call_cmd and async_call_cmd should do their own parsing.

parse_function: DEPRECATED - use return_parser instead
'''
super().__init__(**kwargs)

self._instrument = instrument
self.name = name

# push deprecated parse_function argument to get_parser
if return_parser is None:
return_parser = parse_function

self._set_params(parameters)
self._set_call(call_cmd, async_call_cmd, parse_function)
self._set_call(call_cmd, async_call_cmd,
parameter_parser, return_parser)

def _set_params(self, parameters):
for param in parameters:
Expand All @@ -43,16 +61,19 @@ def _set_params(self, parameters):
self._parameters = parameters
self._param_count = len(parameters)

def _set_call(self, call_cmd, async_call_cmd, parse_function):
def _set_call(self, call_cmd, async_call_cmd,
parameter_parser, return_parser):
ask_or_write = self._instrument.write
ask_or_write_async = self._instrument.write_async
if isinstance(call_cmd, str) and parse_function:
if isinstance(call_cmd, str) and return_parser:
ask_or_write = self._instrument.ask
ask_or_write_async = self._instrument.ask_async

self._call, self._call_async = syncable_command(
self._param_count, call_cmd, async_call_cmd,
ask_or_write, ask_or_write_async, parse_function)
param_count=self._param_count,
cmd=call_cmd, acmd=async_call_cmd,
exec_str=ask_or_write, aexec_str=ask_or_write_async,
input_parser=parameter_parser, output_parser=return_parser)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

syncable_command has too many parameters... much more comprehensible AND robust to call it with only kwargs.


def validate(self, args):
'''
Expand Down
73 changes: 59 additions & 14 deletions qcodes/instrument/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from qcodes.utils.helpers import permissive_range, wait_secs
from qcodes.utils.metadata import Metadatable
from qcodes.utils.sync_async import syncable_command, NoCommandError
from qcodes.utils.validators import Validator, Numbers, Ints
from qcodes.utils.validators import Validator, Numbers, Ints, Enum
from qcodes.instrument.sweep_values import SweepFixedValues


Expand Down Expand Up @@ -176,35 +176,72 @@ def __getitem__(self, keys):

class InstrumentParameter(Parameter):
def __init__(self, instrument, name,
get_cmd=None, async_get_cmd=None, parse_function=None,
set_cmd=None, async_set_cmd=None,
get_cmd=None, async_get_cmd=None, get_parser=None,
parse_function=None, val_mapping=None,
set_cmd=None, async_set_cmd=None, set_parser=None,
sweep_step=None, sweep_delay=None, max_val_age=3600,
**kwargs):
vals=None, **kwargs):
'''
defines one measurement parameter

instrument: an instrument that handles this parameter
name: the local name of this parameter

get_cmd: a string or function to get this parameter
async_get_cmd: a function to use for async get, or for both sync
and async if get_cmd is missing or None
parse_function: function to transform the response from get
get_parser: function to transform the response from get
to the final output value.
NOTE: only applies if get_cmd is a string. The function forms
of get_cmd and async_get_cmd should do their own parsing
See also val_mapping

set_cmd: command to set this parameter, either:
- a string (containing one field to .format, like "{}" etc)
- a function (of one parameter)
async_set_cmd: a function to use for async set, or for both sync
and async if set_cmd is missing or None
set_parser: function to transform the input set value to an encoded
value sent to the instrument.
NOTE: only applies if set_cmd is a string. The function forms
of set_cmd and async_set_cmd should do their own parsing
See also val_mapping

parse_function: DEPRECATED - use get_parser instead

val_mapping: a bidirectional map from data/readable values to
instrument codes, expressed as a dict {data_val: instrument_code}
For example, if the instrument uses '0' to mean 1V and '1' to mean
10V, set val_mapping={1: '0', 10: '1'} and on the user side you
only see 1 and 10, never the coded '0' and '1'

If vals is omitted, will also construct a matching Enum validator.
NOTE: only applies to get if get_cmd is a string, and to set if
set_cmd is a string.

vals: a Validator object for this parameter

sweep_step: max increment of parameter value - larger changes
are broken into steps this size
sweep_delay: time (in seconds) to wait after each sweep step
max_val_age: max time (in seconds) to trust a saved value from
this parameter as the starting point of a sweep
'''
super().__init__(name=name, **kwargs)
# handle val_mapping before super init because it impacts
# vals / validation in the base class
if val_mapping:
if vals is None:
vals = Enum(*val_mapping.keys())

if get_parser is None:
self._get_mapping = {v: k for k, v in val_mapping.items()}
get_parser = self._get_mapping.__getitem__

if set_parser is None:
self._set_mapping = val_mapping
set_parser = self._set_mapping.__getitem__

super().__init__(name=name, vals=vals, **kwargs)

self._instrument = instrument

Expand All @@ -218,8 +255,12 @@ def __init__(self, instrument, name,
self.has_get = False
self.has_set = False

self._set_get(get_cmd, async_get_cmd, parse_function)
self._set_set(set_cmd, async_set_cmd)
# push deprecated parse_function argument to get_parser
if get_parser is None:
get_parser = parse_function

self._set_get(get_cmd, async_get_cmd, get_parser)
self._set_set(set_cmd, async_set_cmd, set_parser)
self.set_sweep(sweep_step, sweep_delay, max_val_age)

if not (self.has_get or self.has_set):
Expand Down Expand Up @@ -251,20 +292,24 @@ def get_async(self):
self._save_val(value)
return value

def _set_get(self, get_cmd, async_get_cmd, parse_function):
def _set_get(self, get_cmd, async_get_cmd, get_parser):
self._get, self._get_async = syncable_command(
0, get_cmd, async_get_cmd, self._instrument.ask,
self._instrument.ask_async, parse_function, no_func)
param_count=0, cmd=get_cmd, acmd=async_get_cmd,
exec_str=self._instrument.ask,
aexec_str=self._instrument.ask_async,
output_parser=get_parser, no_cmd_function=no_func)

if self._get is not no_func:
self.has_get = True

def _set_set(self, set_cmd, async_set_cmd):
def _set_set(self, set_cmd, async_set_cmd, set_parser):
# note: this does not set the final setter functions. that's handled
# in self.set_sweep, when we choose a swept or non-swept setter.
self._set, self._set_async = syncable_command(
1, set_cmd, async_set_cmd, self._instrument.write,
self._instrument.write_async, no_cmd_function=no_func)
param_count=1, cmd=set_cmd, acmd=async_set_cmd,
exec_str=self._instrument.write,
aexec_str=self._instrument.write_async,
input_parser=set_parser, no_cmd_function=no_func)

if self._set is not no_func:
self.has_set = True
Expand Down
51 changes: 44 additions & 7 deletions qcodes/tests/test_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from qcodes.instrument.base import Instrument
from qcodes.instrument.mock import MockInstrument
from qcodes.utils.validators import Numbers, Ints, Strings, MultiType
from qcodes.utils.validators import Numbers, Ints, Strings, MultiType, Enum
from qcodes.utils.sync_async import wait_for_async, NoCommandError


Expand All @@ -16,12 +16,16 @@ class AMockModel(object):
def __init__(self):
self._gates = [0.0, 0.0, 0.0]
self._excitation = 0.1
self._memory = {}

def write(self, instrument, parameter, value):
if instrument == 'gates' and parameter[0] == 'c':
self._gates[int(parameter[1:])] = float(value)
elif instrument == 'gates' and parameter == 'rst':
self._gates = [0.0, 0.0, 0.0]
elif instrument == 'gates' and parameter[:3] == 'mem':
slot = int(parameter[3:])
self._memory[slot] = value
elif instrument == 'source' and parameter == 'ampl':
try:
self._excitation = float(value)
Expand All @@ -37,6 +41,9 @@ def ask(self, instrument, parameter):

if instrument == 'gates' and parameter[0] == 'c':
v = gates[int(parameter[1:])]
elif instrument == 'gates' and parameter[:3] == 'mem':
slot = int(parameter[3:])
return self._memory[slot]
elif instrument == 'source' and parameter == 'ampl':
v = self._excitation
elif instrument == 'meter' and parameter == 'ampl':
Expand All @@ -63,29 +70,29 @@ def setUp(self):
cmdbase = 'c{}'.format(i)
self.gates.add_parameter('chan{}'.format(i), get_cmd=cmdbase + '?',
set_cmd=cmdbase + ' {:.4f}',
parse_function=float,
get_parser=float,
vals=Numbers(-10, 10))
self.gates.add_parameter('chan{}step'.format(i),
get_cmd=cmdbase + '?',
set_cmd=cmdbase + ' {:.4f}',
parse_function=float,
get_parser=float,
vals=Numbers(-10, 10),
sweep_step=0.1, sweep_delay=0.005)
self.gates.add_function('reset', call_cmd='rst')

self.source = MockInstrument('source', model=self.model, delay=0.001)
self.source.add_parameter('amplitude', get_cmd='ampl?',
set_cmd='ampl {:.4f}', parse_function=float,
set_cmd='ampl {:.4f}', get_parser=float,
vals=Numbers(0, 1),
sweep_step=0.2, sweep_delay=0.005)

self.meter = MockInstrument('meter', model=self.model, delay=0.001,
read_response=self.read_response)
self.meter.add_parameter('amplitude', get_cmd='ampl?',
parse_function=float)
get_parser=float)
self.meter.add_function('echo', call_cmd='echo {:.2f}?',
parameters=[Numbers(0, 1000)],
parse_function=float)
return_parser=float)

self.init_ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')

Expand Down Expand Up @@ -224,7 +231,7 @@ def test_sweep_steps_edge_case(self):
# but we should handle it
source = self.source
source.add_parameter('amplitude2', get_cmd='ampl?',
set_cmd='ampl {}', parse_function=float,
set_cmd='ampl {}', get_parser=float,
vals=MultiType(Numbers(0, 1), Strings()),
sweep_step=0.2, sweep_delay=0.005)
self.assertEqual(len(source.history), 0)
Expand Down Expand Up @@ -290,6 +297,36 @@ def test_set_sweep_errors(self):
sweep_step=0.1, sweep_delay=0.01,
max_val_age=-1)

def test_val_mapping(self):
gates = self.gates
mem = self.model._memory

# memraw has no mappings - it just sets and gets what the instrument
# uses to encode this parameter
gates.add_parameter('memraw', set_cmd='mem0 {}', get_cmd='mem0?',
vals=Enum('zero', 'one'))

# memcoded maps the instrument codes ('zero' and 'one') into nicer
# user values 0 and 1
gates.add_parameter('memcoded', set_cmd='mem0 {}', get_cmd='mem0?',
val_mapping={0: 'zero', 1: 'one'})
Copy link
Contributor Author

Choose a reason for hiding this comment

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

FYI tests can often be good examples of usage. To test val_mapping I make two parameters, one that interacts with the instrument with no parsing, and another connected to the same instrument state variable but mapped to different values on the user end.


gates.memcoded.set(0)
self.assertEqual(gates.memraw.get(), 'zero')
self.assertEqual(gates.memcoded.get(), 0)
self.assertEqual(mem[0], 'zero')

gates.memraw.set('one')
self.assertEqual(gates.memcoded.get(), 1)
self.assertEqual(gates.memraw.get(), 'one')
self.assertEqual(mem[0], 'one')

with self.assertRaises(ValueError):
gates.memraw.set(0)

with self.assertRaises(ValueError):
gates.memcoded.set('zero')

def test_snapshot(self):
self.assertEqual(self.meter.snapshot(), {
'parameters': {'amplitude': {}},
Expand Down
Loading