"""
NeuroTools.parameters
=====================
A module for dealing with model parameters.
Classes
-------
Parameter
ParameterRange - for specifying a list of possible values for a given parameter.
ParameterReference - specify a parameter in terms of the value of another parameter.
ParameterSet - for representing/managing hierarchical parameter sets.
ParameterTable - a sub-class of ParameterSet that can represent a table of parameters.
ParameterSpace - a collection of ParameterSets, representing multiple points in
parameter space.
**Imported from NeuroTools.parameters.validators**
ParameterSchema - A sub-class of ParameterSet against which other ParameterSets can be validated
against using a Validator as found in the sub-package
NeuroTools.parameters.validators
CongruencyValidator - A CongruencyValidator validates a ParameterSet against a ParameterSchema
via member "validate(parameter_set,parameter_schema)".
ValidationError - The Exception raised when validation fails
SchemaBase - The base class of all "active" Schema objects to be placed in a ParameterSchema.
-> Sublass - Validates the same-path ParameterSet value if it is of the specified type.
-> Eval - Validates the same-path ParameterSet value if the provided expression
evaluates ("eval") to True.
Functions
---------
nesteddictwalk - Walk a nested dict structure, using a generator.
nesteddictflatten - Return a flattened version of a nested dict structure.
string_table - Convert a table written as a multi-line string into a dict of dicts.
Sub-Packages
------------
validators - A module implementing validation of ParameterSets against ParameterSchema.
"""
# import copy
import warnings
import math
import numpy
import operator
from functools import wraps
try:
from urllib2 import build_opener, install_opener, urlopen, ProxyHandler # Python 2
from urlparse import urlparse
except ImportError:
from urllib.request import build_opener, install_opener, urlopen, ProxyHandler # Python 3
from urllib.parse import urlparse
from NeuroTools.random import ParameterDist, GammaDist, UniformDist, NormalDist
from os import environ, path
import random
from copy import copy
try:
basestring
except NameError:
basestring = str
try:
next # Python 3
except NameError:
def next(obj): # Python 2
return obj.next()
__version__ = '0.2.1'
if 'HTTP_PROXY' in environ:
HTTP_PROXY = environ['HTTP_PROXY'] # user has to define it
''' next lines are for communication to urllib of proxy information '''
proxy_support = ProxyHandler({"https": HTTP_PROXY})
opener = build_opener(proxy_support, HTTPHandler)
install_opener(opener)
[docs]def isiterable(x):
return (hasattr(x, '__iter__') and not isinstance(x, basestring))
[docs]def contains_instance(collection, cls):
return any(isinstance(o, cls) for o in collection)
[docs]def nesteddictwalk(d, separator='.'):
"""
Walk a nested dict structure, using a generator.
Composite keys are created by joining each key to the key of the parent dict
using `separator`.
"""
for key1, value1 in d.items():
if isinstance(value1, dict):
for key2, value2 in nesteddictwalk(value1, separator): # recurse into subdict
yield "%s%s%s" % (key1, separator, key2), value2
else:
yield key1, value1
[docs]def nesteddictflatten(d, separator='.'):
"""
Return a flattened version of a nested dict structure.
Composite keys are created by joining each key to the key of the parent dict
using `separator`.
"""
flatd = {}
for k, v in nesteddictwalk(d, separator):
flatd[k] = v
return flatd
# --- Parameters, and ranges and distributions of them -------------------
[docs]class Parameter(object):
def __init__(self, value, units=None, name=""):
self.name = name
self.value = value
self.units = units
self.type = type(value)
def __repr__(self):
s = "%s = %s" % (self.name, self.value)
if self.units is not None:
s += " %s" % self.units
return s
[docs]class ParameterRange(Parameter):
"""
A class for specifying a list of possible values for a given parameter.
The value must be an iterable. It acts like a Parameter, but .next() can be
called to iterate through the values
"""
def __init__(self, value, units=None, name="", shuffle=False):
if not isiterable(value):
raise TypeError("A ParameterRange value must be iterable")
Parameter.__init__(self, next(value.__iter__()), units, name)
self._values = copy(value)
self._iter_values = self._values.__iter__()
if shuffle:
random.shuffle(self._values)
def __repr__(self):
units_str = ''
if self.units:
units_str = ', units="%s"' % self.units
return 'ParameterRange(%s%s)' % (self._values.__repr__(), units_str)
def __iter__(self):
self._iter_values = self._values.__iter__()
return self._iter_values
def __next__(self):
self._value = next(self._iter_values)
return self._value
[docs] def next(self):
return self.__next__()
def __len__(self):
return len(self._values)
def __eq__(self, o):
if (type(self) == type(o) and
self.name == o.name and
self._values == o._values and
self.units == o.units):
return True
else:
return False
# --- ReferenceParameter
[docs]def reverse(func):
"""Given a function f(a, b), returns f(b, a)"""
@wraps(func)
def reversed_func(a, b):
return func(b, a)
reversed_func.__doc__ = "Reversed argument form of %s" % func.__doc__
reversed_func.__name__ = "reversed %s" % func.__name__
return reversed_func
[docs]def lazy_operation(name, reversed=False):
def op(self, val):
f = getattr(operator, name)
if reversed:
f = reverse(f)
self.operations.append((f, val))
return self
return op
[docs]class ParameterReference(object):
"""
This class provides a place-holder for a reference parameter that will
later be replaced with the value of the parameter pointed to by the
reference. This class also allows for lazy application of operations,
meaning that one can use the reference in simple formulas that will get
evaluated at the moment the reference is replaced.
Check below which operations are supported.
"""
def __init__(self,reference):
object.__init__(self)
self.reference_path = reference
self.operations = []
def _apply_operations(self, x):
for f, arg in self.operations:
try:
if arg is None:
x = f(x)
else:
x = f(x, arg)
except TypeError:
raise TypeError("ParameterReference: error applying operation " + str(f) + " with argument " + str(arg) + " to " + str(x))
return x
[docs] def evaluate(self,parameter_set):
"""
This function evaluetes the reference, using the ParameterSet in parameter_set as the source.
"""
ref_value = parameter_set[self.reference_path]
if isinstance(ref_value,ParameterSet):
if self.operations == []:
return ref_value.tree_copy()
else:
raise ValueError("ParameterReference: lazy operations cannot be applied to argument of type ParameterSet> %s" % self.reference_path)
elif isinstance(ref_value,ParameterReference):
#lets wait until the refe
return self
else:
return self._apply_operations(ref_value)
[docs] def copy(self):
pr = ParameterReference(self.reference_path)
for f, arg in self.operations:
if isinstance(arg,ParameterReference):
pr.operations.append((f,arg.copy()))
else:
pr.operations.append((f,arg))
return pr
__add__ = lazy_operation('add')
__radd__ = __add__
__sub__ = lazy_operation('sub')
__rsub__ = lazy_operation('sub', reversed=True)
__mul__ = lazy_operation('mul')
__rmul__ = __mul__
__div__ = lazy_operation('div')
__rdiv__ = lazy_operation('div', reversed=True)
__truediv__ = lazy_operation('truediv')
__rtruediv__ = lazy_operation('truediv', reversed=True)
__pow__ = lazy_operation('pow')
[docs]def load_parameters(parameter_url, modified_parameters):
"""
This is a function that should be used to load a ParameterSet from a url.
`modified_parameters` should be a dictionary of parameters and their values.
These will be replaced in the loaded parameter set before the references are
expanded.
"""
parameters = ParameterSet(parameter_url)
parameters.replace_values(**modified_parameters)
parameters.replace_references()
return parameters
[docs]class ParameterSet(dict):
"""
A class to manage hierarchical parameter sets.
Usage example::
>>> sim_params = ParameterSet({'dt': 0.1, 'tstop': 1000.0})
>>> exc_cell_params = ParameterSet("http://neuralensemble.org/svn/NeuroTools/example.params")
>>> inh_cell_params = ParameterSet({'tau_m': 15.0, 'cm': 0.5})
>>> network_params = ParameterSet({'excitatory_cells': exc_cell_params, 'inhibitory_cells': inh_cell_params})
>>> P = ParameterSet({'sim': sim_params, 'network': network_params})
>>> P.sim.dt
0.1
>>> P.network.inhibitory_cells.tau_m
15.0
>>> print P.pretty()
"""
non_parameter_attributes = ['_url', 'label', 'names', 'parameters', 'flat',
'flatten', 'non_parameter_attributes']
invalid_names = ['parameters', 'names'] # should probably add dir(dict)
@staticmethod
[docs] def read_from_str(s, update_namespace=None):
"""
`ParameterSet` definition `s` should be a Python dict definition
string, containing objects of types `int`, `float`, `str`, `list`,
`dict` plus the classes defined in this module, `Parameter`,
`ParameterRange`, etc. No other object types are allowed,
except the function `url('some_url')` or `ref('point.delimited.path')`,
e.g.::
{ 'a' : {'A': 3, 'B': 4},
'b' : [1,2,3],
'c' : 'hello world',
'd' : url('http://example.com/my_cool_parameter_set')
'e' : ref('level1_param_name.level2_param_name.level3_param_name') }
This is largely the JSON (www.json.org) format, but with
extra keywords in the namespace such as `ParameterRange`, `GammaDist`, etc.
"""
global_dict = dict(ref=ParameterReference,
url=ParameterSet,
ParameterSet=ParameterSet,
ParameterRange=ParameterRange,
ParameterTable=ParameterTable,
GammaDist=GammaDist,
UniformDist=UniformDist,
NormalDist=NormalDist,
pi=math.pi,
true=True, # these are for reading JSON
false=False, # files
)
if update_namespace:
global_dict.update(update_namespace)
D = None
try:
if 'file://' in s:
path = s.split('file://')[1]
ifile = open(path, 'r')
content = ifile.read()
ifile.close()
D = eval(content, global_dict)
else:
D = eval(s, global_dict)
except SyntaxError as e:
raise SyntaxError(
"Invalid string for ParameterSet definition: %s\n%s" % (s, e))
except TypeError as e:
raise SyntaxError(
"Invalid string for ParameterSet definition: %s" % e)
return D or {}
@staticmethod
[docs] def check_validity(k):
"""docstring missing"""
if k in ParameterSet.invalid_names:
raise Exception("'%s' is not allowed as a parameter name." % k)
def __init__(self, initialiser, label=None, update_namespace=None):
def walk(d, label):
# Iterate through the dictionary `d`, replacing `dict`s by
# `ParameterSet` objects.
for k, v in d.items():
ParameterSet.check_validity(k)
if isinstance(v, ParameterSet):
d[k] = v
elif isinstance(v, dict):
d[k] = walk(v, k)
else:
d[k] = v
return ParameterSet(d, label)
self._url = None
if isinstance(initialiser, basestring): # url or str
if path.exists(initialiser):
f = open(initialiser, 'r')
pstr = f.read()
self._url = initialiser
f.close()
else:
try:
f = urlopen(initialiser)
pstr = f.read().decode()
self._url = initialiser
except IOError as e:
pstr = initialiser
self._url = None
else:
f.close()
# is it a yaml url?
if self._url:
o = urlparse(self._url)
base, ext = path.splitext(o.path)
if ext in ['.yaml', '.yml']:
import yaml
initialiser = yaml.load(pstr)
else:
initialiser = ParameterSet.read_from_str(pstr,
update_namespace)
else:
initialiser = ParameterSet.read_from_str(pstr,
update_namespace)
# By this stage, `initialiser` should be a dict. Iterate through it,
# copying its contents into the current instance, and replacing dicts by
# ParameterSet objects.
if isinstance(initialiser, dict):
for k, v in initialiser.items():
ParameterSet.check_validity(k)
if isinstance(v, ParameterSet):
self[k] = v
elif isinstance(v, dict):
self[k] = walk(v, k)
else:
self[k] = v
else:
raise TypeError(
"`initialiser` must be a `dict`, a `ParameterSet` object, a string, or a valid URL")
# Set the label
if hasattr(initialiser, 'label'):
self.label = label or initialiser.label # if initialiser was a ParameterSet, keep the existing label if the label arg is None
else:
self.label = label
# Define some aliases, allowing, e.g.:
# for name, value in P.parameters():
# for name in P.names():
self.names = self.keys
self.parameters = self.items
[docs] def flat(self):
__doc__ = nesteddictwalk.__doc__
return nesteddictwalk(self)
[docs] def flatten(self):
__doc__ = nesteddictflatten.__doc__
return nesteddictflatten(self)
def __getattr__(self, name):
"""Allow accessing parameters using dot notation."""
try:
return self[name]
except KeyError:
return self.__getattribute__(name)
def __setattr__(self, name, value):
"""Allow setting parameters using dot notation."""
if name in self.non_parameter_attributes:
object.__setattr__(self, name, value)
else:
# should we check the parameter type hasn't changed?
self[name] = value
def __getitem__(self, name):
""" Modified get that detects dots '.' in the names and goes down the
nested tree to find it"""
split = name.split('.', 1)
if len(split) == 1:
return dict.__getitem__(self, name)
# nested get
return dict.__getitem__(self, split[0])[split[1]]
[docs] def flat_add(self, name, value):
""" Like `__setitem__`, but it will add `ParameterSet({})` objects
into the namespace tree if needed. """
split = name.split('.', 1)
if len(split) == 1:
dict.__setitem__(self, name, value)
else:
# nested set
try:
ps = dict.__getitem__(self, split[0])
except KeyError:
# setting nested name without parent existing
# create parent
ps = ParameterSet({})
dict.__setitem__(self, split[0], ps)
# and try again
ps.flat_add(split[1], value)
def __setitem__(self, name, value):
""" Modified set that detects dots '.' in the names and goes down the
nested tree to set it """
split = name.split('.', 1)
if len(split) == 1:
dict.__setitem__(self, name, value)
else:
# nested set
dict.__getitem__(self, split[0])[split[1]] = value
[docs] def update(self, E, **F):
"""docstring missing"""
if hasattr(E, "has_key"):
for k in E:
self[k] = E[k]
else:
for (k, v) in E:
self[k] = v
for k in F:
self[k] = F[k]
# should __len__() be the usual dict length, or the flattened length? Probably the former for consistency with dicts
# can always use len(ps.flatten())
# what about __contains__()? Should we drill down to lower levels in the
# hierarchy? I think so.
def __getstate__(self):
"""For pickling."""
return self
[docs] def save(self, url=None, expand_urls=False):
"""
Write the parameter set to a text file.
The text file syntax is open to discussion. My idea is that it should be
valid Python code, preferably importable as a module.
If `url` is `None`, try to save to `self._url` (if it is not `None`),
otherwise save to `url`.
"""
# possible solution for HTTP PUT: http://inamidst.com/proj/put/put.py
if not url:
url = self._url
assert url != ''
if not self._url:
self._url = url
scheme, netloc, path, parameters, query, fragment = urlparse(url)
if scheme == 'file' or (scheme == '' and netloc == ''):
f = open(path, 'w')
f.write(self.pretty(expand_urls=expand_urls))
f.close()
else:
if scheme:
raise Exception(
"Saving using the %s protocol is not implemented" % scheme)
else:
raise Exception("No protocol (http, ftp, etc) specified.")
[docs] def pretty(self, indent=' ', expand_urls=False):
"""
Return a unicode string representing the structure of the `ParameterSet`.
evaluating the string should recreate the object.
"""
def walk(d, indent, ind_incr):
s = []
for k, v in d.items():
if hasattr(v, 'items'):
if expand_urls is False and hasattr(v, '_url') and v._url:
s.append('%s"%s": url("%s"),' % (indent, k, v._url))
else:
s.append('%s"%s": {' % (indent, k))
s.append(walk(v, indent+ind_incr, ind_incr))
s.append('%s},' % indent)
elif isinstance(v, basestring):
s.append('%s"%s": "%s",' % (indent, k, v))
else: # what if we have a dict or ParameterSet inside a list? currently they are not expanded. Should they be?
s.append('%s"%s": %s,' % (indent, k, v))
return '\n'.join(s)
return '{\n' + walk(self, indent, indent) + '\n}'
[docs] def tree_copy(self):
"""Return a copy of the `ParameterSet` tree structure.
Nodes are not copied, but re-referenced."""
tmp = ParameterSet({})
for key in self:
value = self[key]
if isinstance(value, ParameterSet):
tmp[key] = value.tree_copy()
elif isinstance(value,ParameterReference):
tmp[key] = value.copy()
else:
tmp[key] = value
if tmp._is_space():
tmp = ParameterSpace(tmp)
return tmp
[docs] def as_dict(self):
"""Return a copy of the `ParameterSet` tree structure
as a nested dictionary"""
tmp = {}
for key in self:
value = self[key]
if isinstance(value, ParameterSet):
# recurse
tmp[key] = value.as_dict()
else:
tmp[key] = value
return tmp
def __sub__(self, other):
"""
Return the difference between this `ParameterSet` and another.
Not yet properly implemented.
"""
self_keys = set(self)
other_keys = set(other)
intersection = self_keys.intersection(other_keys)
difference1 = self_keys.difference(other_keys)
difference2 = other_keys.difference(self_keys)
result1 = dict([(key, self[key]) for key in difference1])
result2 = dict([(key, other[key]) for key in difference2])
# Now need to check values for intersection....
for item in intersection:
if isinstance(self[item], ParameterSet):
d1, d2 = self[item] - other[item]
if d1:
result1[item] = d1
if d2:
result2[item] = d2
elif self[item] != other[item]:
result1[item] = self[item]
result2[item] = other[item]
if len(result1) + len(result2) == 0:
assert self == other, "Error in ParameterSet.diff()"
return result1, result2
def _is_space(self):
"""
Check for the presence of `ParameterRanges` or `ParameterDists` to
determine if this is a `ParameterSet` or a `ParameterSpace`.
"""
for k, v in self.flat():
if isinstance(v, ParameterRange) or isinstance(v, ParameterDist):
return True
return False
[docs] def export(self, filename, format='latex', **kwargs):
"""
docstring missing
"""
if format == 'latex':
from .export import parameters_to_latex
parameters_to_latex(filename, self, **kwargs)
[docs] def replace_references(self):
while True:
refs = self.find_references()
if len(refs) == 0:
break
for s, k, v in refs:
s[k] = v.evaluate(self)
[docs] def find_references(self):
l = []
for k, v in self.items():
if isinstance(v, ParameterReference):
l += [(self, k, v)]
elif isinstance(v, ParameterSet):
l += v.find_references()
return l
[docs] def replace_values(self,**args):
"""
This expects its arguments to be in the form path=value, where path is a
. (dot) delimited path to a parameter in the parameter tree rooted in
this ParameterSet instance.
This function replaces the values of each parameter in the args with the
corresponding values supplied in the arguments.
"""
for k in args.keys():
self[k] = args[k]
[docs]class ParameterSpace(ParameterSet):
"""
A collection of `ParameterSets`, representing multiple points in
parameter space. Created by putting `ParameterRange` and/or `ParameterDist`
objects within a `ParameterSet`.
"""
[docs] def iter_range_key(self, range_key):
""" An iterator of the `ParameterSpace` which yields the
`ParameterSet` with the `ParameterRange` given by `range_key` replaced with
each of its values"""
tmp = self.tree_copy()
for val in self[range_key]:
tmp[range_key] = val
yield tmp
[docs] def iter_inner_range_keys(self, keys, copy=False):
""" An iterator of the `ParameterSpace` which yields
`ParameterSets` with all combinations of `ParameterRange` elements
which are given by the `keys` list.
Note: each newly yielded value is one and the same object
so storing the returned values results in a collection
of many of the lastly yielded object.
`copy=True` causes each yielded object to be a newly
created object, but be careful because this is
spawning many dictionaries!
"""
if len(keys) == 0:
# return an iterator over 1 copy for modifying
yield self.tree_copy()
return
if not copy:
# recursively iterate over remaining keys
for tmp in self.iter_inner_range_keys(keys[1:]):
# iterator over range of our present attention
for val in self[keys[0]]:
tmp[keys[0]] = val
if not tmp._is_space():
tmp = ParameterSet(tmp)
yield tmp
else:
# Each yielded ParameterSet is a tree_copy of self
# recursively iterate over remaining keys
for tmp in self.iter_inner_range_keys(keys[1:]):
# iterator over range of our present attention
for val in self[keys[0]]:
tmp_copy = tmp.tree_copy()
tmp_copy[keys[0]] = val
if not tmp_copy._is_space():
tmp = ParameterSet(tmp)
yield tmp_copy
[docs] def range_keys(self):
"""Return the list of keys for those elements which are `ParameterRanges`."""
return [key for key, value in self.flat() if isinstance(value, ParameterRange)]
[docs] def iter_inner(self, copy=False):
"""An iterator of the `ParameterSpace` which yields
`ParameterSets` with all combinations of `ParameterRange` elements"""
return self.iter_inner_range_keys(self.range_keys(), copy)
[docs] def num_conditions(self):
"""Return the number of `ParameterSets` that will be returned by the
`iter_inner()` method."""
# Not properly tested
n = 1
for key in self.range_keys():
n *= len(self[key])
return n
[docs] def dist_keys(self):
"""Return the list of keys for those elements which are `ParameterDists`."""
def is_or_contains_dist(value):
return isinstance(value, ParameterDist) or (
isiterable(value) and contains_instance(value, ParameterDist))
return [key for key, value in self.flat() if is_or_contains_dist(value)]
[docs] def realize_dists(self, n=1, copy=False):
"""For each `ParameterDist`, realize the distribution and yield the result.
If `copy==True`, causes each yielded object to be a newly
created object, but be careful because this is
spawning many dictionaries!"""
def next(item, n):
if isinstance(item, ParameterDist):
return item.next(n)
else:
return [item]*n
# pre-generate random numbers
rngs = {}
for key in self.dist_keys():
if isiterable(self[key]):
rngs[key] = [next(item, n) for item in self[key]]
else:
rngs[key] = self[key].next(n)
# get a copy to fill in the rngs
if copy:
tmp = self.tree_copy()
for i in range(n):
for key in rngs:
if isiterable(self[key]):
tmp[key] = [rngs[key][j][i]
for j in range(len(rngs[key]))]
else:
tmp[key] = rngs[key][i]
yield tmp.tree_copy()
else:
tmp = self.tree_copy()
for i in range(n):
for key in rngs:
if isiterable(self[key]):
tmp[key] = [rngs[key][j][i]
for j in range(len(rngs[key]))]
else:
tmp[key] = rngs[key][i]
yield tmp
[docs] def parameter_space_dimension_labels(self):
"""
Return the dimensions and labels of the keys for those elements which are `ParameterRanges`.
`range_keys` are sorted to ensure the same ordering each time.
"""
range_keys = self.range_keys()
range_keys.sort()
dim = []
label = []
for key in range_keys:
label.append(key)
dim.append(len(eval('self.'+key)))
return dim, label
[docs] def parameter_space_index(self, current_experiment):
"""
Return the index of the current experiment in the dimension of the parameter space
i.e. parameter space dimension: [2,3]
i.e. index: (1,0)
Example::
p = ParameterSet({})
p.b = ParameterRange([1,2,3])
p.a = ParameterRange(['p','y','t','h','o','n'])
results_dim, results_label = p.parameter_space_dimension_labels()
results = numpy.empty(results_dim)
for experiment in p.iter_inner():
index = p.parameter_space_index(experiment)
results[index] = 2.
"""
index = []
range_keys = self.range_keys()
range_keys.sort()
for key in range_keys:
value = eval('current_experiment.'+key)
try:
value_index = list(eval('self.'+key)._values).index(value)
except ValueError:
raise ValueError(
"The ParameterSet provided is not within the ParameterSpace")
index.append(value_index)
return tuple(index)
[docs] def get_ranges_values(self):
"""
Return a dict with the keys and values of the parameters with `ParameterRanges`
Example::
>>> p = ParameterSpace({})
>>> p.b = ParameterRange([1,2,3])
>>> p.a = ParameterRange(['p','y','t','h','o','n'])
>>> data = p.get_ranges_values()
>>> data
{'a': ['p', 'y', 't', 'h', 'o', 'n'], 'b': [1, 2, 3]}
"""
data = {}
range_keys = self.range_keys()
range_keys.sort()
for key in range_keys:
data[key] = eval('self.'+key)._values
return data
[docs]def string_table(tablestring):
"""Convert a table written as a multi-line string into a dict of dicts."""
tabledict = {}
rows = tablestring.strip().split('\n')
column_headers = rows[0].split()
for row in rows[1:]:
row = row.split()
row_header = row[0]
tabledict[row_header] = {}
for col_header, item in zip(column_headers[1:], row[1:]):
tabledict[row_header][col_header] = float(item)
return tabledict
[docs]class ParameterTable(ParameterSet):
"""
A sub-class of `ParameterSet` that can represent a table of parameters.
i.e., it is limited to one-level of nesting, and each sub-dict must have
the same keys. In addition to the possible initialisers for ParameterSet,
a ParameterTable can be initialised from a multi-line string, e.g.::
>>> pt = ParameterTable('''
... # col1 col2 col3
... row1 1 2 3
... row2 4 5 6
... row3 7 8 9
... ''')
>>> pt.row2.col3
6.0
>>> pt.column('col1')
{'row1': 1.0, 'row2': 4.0, 'row3': 7.0}
>>> pt.transpose().col3.row2
6.0
"""
non_parameter_attributes = ParameterSet.non_parameter_attributes + \
['row', 'rows', 'row_labels',
'column', 'columns', 'column_labels']
def __init__(self, initialiser, label=None):
if isinstance(initialiser, basestring): # url or table string
tabledict = string_table(initialiser)
# if initialiser is a URL, string_table() should return an empty dict
# since URLs do not contain spaces.
if tabledict: # string table
initialiser = tabledict
ParameterSet.__init__(self, initialiser, label)
# Now need to check that the contents actually define a table, i.e.
# two levels of nesting and each sub-dict has the same keys
self._check_is_table()
self.rows = self.items
# self.rows.__doc__ = "Return a list of (row_label, row) pairs, as 2-tuples."""
self.row_labels = self.keys
# self.row_labels.__doc__ = "Return a list of row labels."
def _check_is_table(self):
"""
Checks that the contents actually define a table, i.e.
one level of nesting and each sub-dict has the same keys.
Raises an `Exception` if these requirements are violated.
"""
# to be implemented
pass
[docs] def row(self, row_label):
"""Return a `ParameterSet` object containing the requested row."""
return self[row_label]
[docs] def column(self, column_label):
"""Return a `ParameterSet` object containing the requested column."""
col = {}
for row_label, row in self.rows():
col[row_label] = row[column_label]
return ParameterSet(col)
[docs] def columns(self):
"""Return a list of `(column_label, column)` pairs, as 2-tuples."""
return [(column_label, self.column(column_label)) for column_label in self.column_labels()]
[docs] def column_labels(self):
"""Return a list of column labels."""
sample_row = self[list(self.row_labels())[0]]
return sample_row.keys()
[docs] def transpose(self):
"""
Return a new `ParameterTable` object with the same data as the current
one but with rows and columns swapped.
"""
new_table = ParameterTable({})
for column_label, column in self.columns():
new_table[column_label] = column
return new_table
[docs] def table_string(self):
"""
Returns the table as a string, suitable for being used as the
initialiser for a new `ParameterTable`.
"""
# formatting could definitely be improved
column_labels = self.column_labels()
lines = ["#\t " + "\t".join(column_labels)]
for row_label, row in self.rows():
lines.append(
row_label + "\t" + "\t".join(["%s" % row[col]
for col in column_labels]))
return "\n".join(lines)