Skip to content

Commit

Permalink
Rewrite Variables interface. type becomes a separate keyword argument…
Browse files Browse the repository at this point in the history
… (for more explicitness). All type information is stored in the corresponding subcontext (not only variable name). All other values except types no longer remain in context during composition. Compose no longer stores full contexts, but only a list of composed types. Flat is better than nested. latex_name and range have no special meaning (the latter was never used in real analysis). Compose no longer treats functions in kwargs specially; any kwargs transformations can be done by the user.
  • Loading branch information
ynikitenko committed Aug 24, 2023
1 parent 765b59e commit c0b1aa5
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 274 deletions.
12 changes: 1 addition & 11 deletions lena/variables/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,12 @@ def abs(var, name=None, latex_name=None):
>>> list(map(x, data))[1] == (
... -1,
... {'variable':
... {'coordinate': 'x',
... {'coordinate': {'name': 'x'},
... 'name': 'x',
... 'type': 'coordinate'}
... }
... )
True
>>> abs_x = lena.variables.abs(x)
>>> list(map(abs_x, data))[1] == (
... 1, {
... 'variable': {
... 'name': 'abs_x', 'coordinate': 'x',
... 'type': 'coordinate', 'latex_name': '|x|'
... }
... }
... )
True
"""
# originally it was called abs_,
# but there were problems with automatic documentation (autosummary)
Expand Down
200 changes: 80 additions & 120 deletions lena/variables/variable.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,29 @@
>>> x = Variable(
... "x", lambda coord: coord[0], type="coordinate"
... )
>>> neutron = Variable(
... "neutron", latex_name="n",
... getter=lambda double_ev: double_ev[1], type="particle"
>>> positron = Variable(
... "positron", latex_name="e^+",
... getter=lambda double_ev: double_ev[0], type="particle"
... )
>>> x_n = Compose(neutron, x)
>>> x_n(data[0])[0]
1.1
>>> x_n(data[0])[1] == {
... 'variable': {
... 'name': 'neutron_x', 'particle': 'neutron',
... 'latex_name': 'x_{n}', 'coordinate': 'x', 'type': 'coordinate',
... 'compose': {
... 'type': 'particle', 'latex_name': 'n',
... 'name': 'neutron', 'particle': 'neutron'
... },
... }
>>> x_e = Compose(positron, x)
>>> x_e(data[0])[0]
1.05
>>> x_e(data[0])[1] == {
... 'variable': {
... 'name': 'x',
... 'coordinate': {'name': 'x'},
... 'type': 'coordinate',
... 'compose': ['particle', 'coordinate'],
... 'particle': {'name': 'positron', 'latex_name': 'e^+'}
... }
... }
True
:class:`Combine` and :class:`Compose` are subclasses
of a :class:`Variable`.
"""
import copy
from copy import deepcopy

import lena.core
import lena.context
Expand All @@ -51,7 +51,7 @@
class Variable(object):
"""Function of data with context."""

def __init__(self, name, getter, **kwargs):
def __init__(self, name, getter, type="", **kwargs):
"""*name* is variable's name.
*getter* is a Python function (not a :class:`Variable`)
Expand All @@ -63,14 +63,15 @@ def __init__(self, name, getter, **kwargs):
*range*, etc.
*type* is the type of the variable.
It depends on your application, examples are
"coordinate" or "particle_type".
It depends on your application, examples could be
"coordinate" or "particle".
It has a special meaning: if present,
its value is added to variable's
context as a key with variable's name
(see example for this module).
Thus variable type's data is preserved during composition
of different types.
context as a key with the context of this variable
(see the example for this module).
It is recommended to set the type,
otherwise variable's data will be lost after composition
of variables.
**Attributes**
Expand Down Expand Up @@ -109,9 +110,17 @@ def __init__(self, name, getter, **kwargs):
self.var_context = {
"name": self.name,
}
if "type" in kwargs:
self.var_context.update({kwargs["type"]: self.name})
self.var_context.update(**kwargs)
if type:
# to take less space in context; this is obvious.
# self.var_context["type"] = type
varc = self.var_context
varc.update(
{type: deepcopy(varc)}
)
# we store type in this variable context,
# but not in its type subcontext.
varc["type"] = type

def __call__(self, value):
"""Transform a *value*.
Expand Down Expand Up @@ -176,21 +185,35 @@ def _update_context(context, var_context):

# no deep copy of var_context is made
# (do it in user code if needed)
context_var = context.get("variable")
if context_var:
# preserve variable.compose if that is present
# This is variable.compose (not variable.variable),
# because it underlines that variable composition
# is not simply update_nested.
context["variable"]["compose"] = copy.deepcopy(context_var)
# deep copy, because otherwise
# it will be updated during update_recursively

# update recursively, because we need to preserve "type"
# and other not overwritten data
lena.context.update_recursively(
context, {"variable": var_context}
)
cvar = context.get("variable")
# preserve variable composition information if that is present
composed = []
if cvar and ("type" in var_context) and ("type" in cvar):
# If cvar has no "type",
# then no types were in the recent variable or earlier
cur_type = var_context["type"]
if "compose" in cvar:
assert isinstance(cvar["compose"], list)
else:
cvar["compose"] = [cvar["type"]]
if "compose" in var_context:
assert isinstance(var_context["compose"], list)
cvar["compose"].extend(cur_type)
else:
cvar["compose"].append(cur_type)
composed = cvar["compose"]

old_cvar = context.get("variable", {})
context["variable"] = var_context
cvar = context["variable"]
if composed:
# we need to overwrite it,
# because current variable context has set it wrong
context["variable"]["compose"] = composed
# preserve old types, but nothing more.
for type_ in composed:
if type_ not in cvar and type_ in old_cvar:
cvar[type_] = old_cvar[type_]

# could be useful as a chainable method,
# but in general it doesn't return anything.
Expand Down Expand Up @@ -224,15 +247,12 @@ def __init__(self, *args, **kwargs):
If not provided, it is its variables' names joined with '_'.
*context.variable* is updated with *combine*, which is a tuple
of each variable's context.
containing each variable's context.
**Attributes**:
*dim* is the number of variables.
*range*. If all variables have an attribute *range*,
the *range* of this variable is set to a list of them.
All *args* must be *Variables*
and there must be at least one of them,
otherwise :class:`LenaTypeError` is raised.
Expand Down Expand Up @@ -264,11 +284,6 @@ def __init__(self, *args, **kwargs):
copy.deepcopy(var.var_context) for var in self._vars
)

# set range of the combined variables
if all(hasattr(var, "range") for var in self._vars):
range_ = [var.range for var in self._vars]
var_context["range"] = range_

super(Combine, self).__init__(name=name, getter=getter, **var_context)

def __getitem__(self, index):
Expand All @@ -282,32 +297,15 @@ class Compose(Variable):
def __init__(self, *args, **kwargs):
"""*args* are the variables to be composed.
Keyword arguments:
*name* is the name of the composed variable.
If that is missing, it is composed from variables
names joined with underscore.
*latex_name* is LaTeX name of the composed variable.
If that is missing and if there are only two variables,
it is composed from variables' names
(or their LaTeX names if present)
as a subscript in the reverse order *(latex2_{latex1})*.
A keyword argument *name* can set
the name of the composed variable.
If that is missing, it the name of the last variable is used.
*context.variable.compose* contains contexts
of the composed variables
(the first composed variable is most nested).
If any keyword argument is a callable,
it is used to create the corresponding variable attribute.
In this case, all variables must have this attribute,
and the callable is applied to the list of these attributes.
If any attribute is missing,
:exc:`.LenaAttributeError` is raised.
This can be used to create composed attributes
other than *latex_name*.
If there are no variables or if *kwargs* contain *getter*,
If there are no variables or if *kwargs* contains *getter*,
:exc:`.LenaTypeError` is raised.
"""
if not all(isinstance(arg, Variable) for arg in args):
Expand All @@ -328,67 +326,29 @@ def getter(value):
value = var.getter(value)
return value
self._vars = args
if "latex_name" not in kwargs and len(args) == 2:
latex_name_1 = args[0].get("latex_name", args[0].name)
latex_name_2 = args[1].get("latex_name", args[1].name)
if latex_name_1 and latex_name_2:
latex_name = "{}_{{{}}}".format(latex_name_2, latex_name_1)
kwargs["latex_name"] = latex_name
for kw in kwargs:
kwarg = kwargs[kw]
if callable(kwarg):
try:
var_args = [getattr(var, kw) for var in args]
except AttributeError:
raise lena.core.LenaAttributeError(
"all variables must contain {}, ".format(kw) +
"{} given".format(args)
)
else:
kwargs[kw] = kwarg(var_args)

# composition of functions must be almost the same
# as those functions in a sequence.
# The only difference seems to be that only type is copied here,
# and in Variable.__call__ other keys are copied as well
compose = copy.deepcopy(self._vars[0].var_context)
# def get_lowest_compose(d):
# while True:
# if isinstance(d, dict) and "compose" in d:
# d = d["compose"]
# else:
# return d
def collect_types(d):
types = {}
def collect_type_this_level(d):
if "type" in d and d["type"] in d:
return {d["type"]: d[d["type"]]}
else:
return {}
composes = [d]
while "compose" in d:
d = d["compose"]
composes.append(d)
for d in reversed(composes):
types.update(collect_type_this_level(d))
return types
compose = {"variable": copy.deepcopy(self._vars[0].var_context)}

for var in self._vars[1:]:
varc = copy.deepcopy(var.var_context)
lena.context.update_nested("compose", varc, compose)
# get_lowest_compose(varc).update({"compose": compose})
compose = varc
# one Composed variable must be same as a simple variable
var_context = compose
types = collect_types(var_context)
var_context.update(types)
var_context.pop("name")
Variable._update_context(compose, varc)

var_context = compose["variable"]
# otherwise *name* will be given twice in super.__init__
# var_context.pop("name")

if "name" in kwargs:
name = kwargs.pop("name")
else:
name = "_".join(var.name for var in args)
name = self._vars[-1].name
# name = "_".join(var.name for var in args)
var_context.update(kwargs)

super(Compose, self).__init__(
name=name, getter=getter, **var_context
name=name, getter=getter
)
# we can't set it in super.__init__,
# since we've done all work here
self.var_context = var_context
28 changes: 15 additions & 13 deletions tests/variables/test_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def test_abs():
data = [(0, 1), (-1, 1)]
x_context = {'variable': {
'name': 'x',
'coordinate': 'x',
'coordinate': {'name': 'x'},
'type': 'coordinate'
}
}
Expand All @@ -25,14 +25,13 @@ def test_abs():
(-1, x_context)
]

abs_x_context = {'variable':
{'dim': 1, 'compose':
{
'coordinate': 'x', 'dim': 1, 'latex_name': 'x',
'name': 'x', 'type': 'coordinate'
},
'name': 'abs_x', 'coordinate': 'x',
'type': 'coordinate', 'latex_name': '|x|'
abs_x_context = {
'variable': {
'dim': 1,
'name': 'abs_x',
'coordinate': {'name': 'x', 'unit': 'cm'},
'type': 'coordinate',
'latex_name': '|x|'
}
}
list(map(abs_x, data)) == [
Expand All @@ -41,7 +40,8 @@ def test_abs():
]


def test_cm():
# doesn't work. Not sure I need that Cm...
def _test_cm():
data = [(0, 1), (-1, 1)]
# Variable x without a unit
x = Variable(name='x', getter=lambda data: data[0], type='coordinate')
Expand All @@ -52,9 +52,11 @@ def test_cm():
# cm
var_args.update(unit="cm")
x = Variable(**var_args)
x_context = {'variable':
{
'name': 'x', 'coordinate': 'x', 'type': 'coordinate',
x_context = {
'variable': {
'name': 'x',
'coordinate': {'name': 'x', 'unit': 'cm'},
'type': 'coordinate',
'unit': 'cm'
}
}
Expand Down

0 comments on commit c0b1aa5

Please sign in to comment.