-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmodello.py
179 lines (150 loc) · 7.2 KB
/
modello.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
#!/usr/bin/env python
"""Module for symbolic modeling of systems."""
import typing
from sympy import Basic, Dummy, Eq, solve
# more verbose path as mypy sees sympy.simplify as a module
from sympy.simplify.simplify import simplify
class ModelloSentinelClass:
"""This class is used for quick type.mro() checks."""
class InstanceDummy(Dummy):
"""Dummy which will create a bound bummy on Modello instantiation."""
def bound(self, model_name: str) -> "BoundInstanceDummy":
"""Return a dummy bound to a modello instance."""
return BoundInstanceDummy(model_name + "_" + self.name, **self.assumptions0)
# # for debugging
# def _sympystr(self, printer):
# return "%s[%s]" % (self.name, self.dummy_index)
class BoundInstanceDummy(InstanceDummy):
"""Dummy associated with a Modello instance."""
class ModelloMetaNamespace(dict):
"""This is so that Modello class definitions implicitly define symbols."""
def __init__(self, name: str, bases: typing.Tuple[type, ...]) -> None:
"""Create a namespace for a Modello class to use."""
self.name = name
# map of attributes to sympy Basic (e.g expression, value) objects
self.attrs: typing.Dict[str, Basic] = {}
# map of attributes to InstanceDummy instances - metadata used by derived classes
self.dummies: typing.Dict[str, InstanceDummy] = {}
# map of attributes to non-modello managed objects
self.other_attrs: typing.Dict[str, object] = {}
# map of dummies to dummies that override them - metadata used by derived classes
self.dummy_overrides: typing.Dict[Dummy, Dummy] = {}
# build up the attributes from the base classes
for base in bases:
if ModelloSentinelClass not in base.mro():
continue
parent_namespace = getattr(base, "_modello_namespace", None)
# TODO: read the following (regarding python's method resolution order) and make sure all is ok:
# http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.19.3910&rep=rep1&type=pdf
if parent_namespace:
# find which dummies are overridden by this base modello
for attr in self.dummies.keys() & parent_namespace.dummies.keys():
override_dummy = parent_namespace.dummies[attr]
base_dummy = self.dummies[attr]
self.dummies[attr] = override_dummy
self.dummy_overrides[base_dummy] = override_dummy
self.attrs.update(parent_namespace.attrs)
self.dummies.update(parent_namespace.dummies)
self.other_attrs.update(parent_namespace.other_attrs)
self.update(parent_namespace)
# substitute overridden dummies in the attributes
if self.dummy_overrides:
for attr, value in self.attrs.items():
self.attrs[attr] = value.subs(self.dummy_overrides)
def __setitem__(self, key: str, value: object) -> None:
"""Manage modello attributes as values are assigned."""
if isinstance(value, Basic):
if key in self:
dummy = self.dummies[key]
elif isinstance(value, InstanceDummy):
dummy = value
else:
dummy = InstanceDummy(key, **value.assumptions0)
self.attrs[key] = simplify(value).subs(self.dummy_overrides)
self.dummies[key] = dummy
value = dummy
elif key in self.attrs:
# cannot overried a part of inherited expressions with a non-expression
raise ValueError(
"Cannot assign %s.%s to a non-expression" % (self.name, key)
)
else:
self.other_attrs[key] = value
super().__setitem__(key, value)
class ModelloMeta(type):
"""Used to make Modello class definitions use dummies."""
@classmethod
def __prepare__(
metacls, __name: str, __bases: typing.Tuple[type, ...], **kwds: typing.Any
) -> typing.Mapping[str, typing.Any]:
"""Return a ModelloMetaNamespace instead of a plain dict to accumlate attributes on."""
return ModelloMetaNamespace(__name, __bases)
def __new__(
mcs,
name: str,
bases: typing.Tuple[type, ...],
meta_namespace: ModelloMetaNamespace,
) -> typing.Any:
"""Return a new class with modello attributes."""
namespace = dict(meta_namespace)
# could follow django's model of _meta? conflicts?
namespace["_modello_namespace"] = meta_namespace
namespace["_modello_class_constraints"] = {
dummy: meta_namespace.attrs[attr]
for attr, dummy in meta_namespace.dummies.items()
if meta_namespace.attrs[attr] is not dummy
}
return super().__new__(mcs, name, bases, namespace)
class Modello(ModelloSentinelClass, metaclass=ModelloMeta):
"""Base class for building symbolic models."""
_modello_namespace: typing.ClassVar[ModelloMetaNamespace] = ModelloMetaNamespace(
"", ()
)
_modello_class_constraints: typing.Dict[InstanceDummy, Basic] = {}
def __init__(self, name: str, **value_map: Basic) -> None:
"""Initialise a model instance and solve for each attribute."""
instance_dummies = {
class_dummy: class_dummy.bound(name)
for class_dummy in self._modello_namespace.dummies.values()
}
self._modello_instance_dummies = instance_dummies
instance_constraints = {}
for attr, value in value_map.items():
value = simplify(value).subs(instance_dummies)
value_map[attr] = value
class_dummy = getattr(self, attr)
instance_dummy = instance_dummies[class_dummy]
instance_constraints[instance_dummy] = value
self._modello_instance_constraints: typing.Dict[
BoundInstanceDummy, Basic
] = instance_constraints
constraints = [
Eq(instance_dummies[class_dummy], value.subs(instance_dummies))
for class_dummy, value in self._modello_class_constraints.items()
]
constraints.extend(
Eq(instance_dummy, value)
for instance_dummy, value in instance_constraints.items()
)
# handy for debugging
self._modello_constraints: typing.List[Eq] = constraints
if constraints:
solutions = solve(constraints, particular=True, dict=True)
if len(solutions) != 1:
raise ValueError("%s solutions" % len(solutions))
solution = solutions[0]
else:
solution = {}
for attr, class_dummy in self._modello_namespace.dummies.items():
instance_dummy = instance_dummies[class_dummy]
if instance_dummy in solution:
value = solution[instance_dummy]
elif instance_dummy in instance_constraints:
value = instance_constraints[instance_dummy]
elif class_dummy in self._modello_class_constraints:
value = self._modello_class_constraints[class_dummy].subs(
instance_dummies
)
else:
value = instance_dummy
setattr(self, attr, value)