engforge 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- engforge/__init__.py +26 -0
- engforge/_testing_components.py +864 -0
- engforge/analysis.py +257 -0
- engforge/attr_dynamics.py +222 -0
- engforge/attr_plotting.py +811 -0
- engforge/attr_signals.py +116 -0
- engforge/attr_slots.py +211 -0
- engforge/attr_solver.py +443 -0
- engforge/attributes.py +326 -0
- engforge/common.py +76 -0
- engforge/component_collections.py +276 -0
- engforge/components.py +55 -0
- engforge/configuration.py +673 -0
- engforge/dataframe.py +185 -0
- engforge/datastores/__init__.py +116 -0
- engforge/datastores/data.py +519 -0
- engforge/datastores/gdocs.py +2650 -0
- engforge/datastores/reporting.py +935 -0
- engforge/datastores/secrets.py +549 -0
- engforge/dynamics.py +662 -0
- engforge/eng/__init__.py +0 -0
- engforge/eng/costs.py +1459 -0
- engforge/eng/fluid_material.py +279 -0
- engforge/eng/geometry.py +1060 -0
- engforge/eng/pipes.py +1211 -0
- engforge/eng/prediction.py +366 -0
- engforge/eng/solid_materials.py +445 -0
- engforge/eng/structure.py +2228 -0
- engforge/eng/structure_beams.py +821 -0
- engforge/eng/thermodynamics.py +271 -0
- engforge/engforge_attributes.py +380 -0
- engforge/env_var.py +214 -0
- engforge/locations.py +18 -0
- engforge/logging.py +290 -0
- engforge/patterns.py +449 -0
- engforge/problem_context.py +2251 -0
- engforge/properties.py +501 -0
- engforge/reporting.py +595 -0
- engforge/solveable.py +964 -0
- engforge/solver.py +316 -0
- engforge/solver_utils.py +726 -0
- engforge/system.py +143 -0
- engforge/system_reference.py +299 -0
- engforge/tabulation.py +288 -0
- engforge/test/__init__.py +2 -0
- engforge/test/_pre_test_structures.py +221 -0
- engforge/test/report_testing.py +158 -0
- engforge/test/test_airfilter.py +91 -0
- engforge/test/test_analysis.py +3 -0
- engforge/test/test_comp_iter.py +196 -0
- engforge/test/test_composition.py +139 -0
- engforge/test/test_costs.py +338 -0
- engforge/test/test_dynamics.py +109 -0
- engforge/test/test_dynamics_spaces.py +39 -0
- engforge/test/test_four_bar.py +97 -0
- engforge/test/test_modules.py +72 -0
- engforge/test/test_performance.py +93 -0
- engforge/test/test_pipes.py +49 -0
- engforge/test/test_problem.py +411 -0
- engforge/test/test_problem_deepscoping.py +68 -0
- engforge/test/test_slider_crank.py +51 -0
- engforge/test/test_solver.py +343 -0
- engforge/test/test_tabulation.py +233 -0
- engforge/typing.py +92 -0
- engforge-0.3.0.dist-info/METADATA +392 -0
- engforge-0.3.0.dist-info/RECORD +69 -0
- engforge-0.3.0.dist-info/WHEEL +5 -0
- engforge-0.3.0.dist-info/licenses/LICENSE +7 -0
- engforge-0.3.0.dist-info/top_level.txt +1 -0
engforge/attr_solver.py
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
"""solver defines a SolverMixin for use by System.
|
|
2
|
+
|
|
3
|
+
Additionally the Solver attribute is defined to add complex behavior to a system as well as add constraints and transient integration.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import attrs
|
|
7
|
+
import uuid
|
|
8
|
+
import numpy
|
|
9
|
+
import scipy.optimize as scopt
|
|
10
|
+
from contextlib import contextmanager
|
|
11
|
+
import copy
|
|
12
|
+
import datetime
|
|
13
|
+
import typing
|
|
14
|
+
|
|
15
|
+
from engforge.attributes import ATTR_BASE, AttributeInstance
|
|
16
|
+
from engforge.system_reference import Ref, maybe_attr_inst, maybe_ref
|
|
17
|
+
from engforge.properties import *
|
|
18
|
+
|
|
19
|
+
import itertools
|
|
20
|
+
|
|
21
|
+
INTEGRATION_MODES = ["euler", "trapezoid", "implicit"]
|
|
22
|
+
SOLVER_OPTIONS = ["root", "minimize"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AttrSolverLog(LoggingMixin):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
log = AttrSolverLog()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
indep_type = typing.Union[str]
|
|
33
|
+
dep_type = typing.Union[system_property, callable]
|
|
34
|
+
ref_type = typing.Union[str, system_property, callable, float, int]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Solver minimizes residual by changing vars
|
|
38
|
+
class SolverInstance(AttributeInstance):
|
|
39
|
+
"""A decoupled signal instance to perform operations on a system instance"""
|
|
40
|
+
|
|
41
|
+
system: "System"
|
|
42
|
+
solver: "Solver"
|
|
43
|
+
|
|
44
|
+
# compiled info
|
|
45
|
+
obj: "Ref"
|
|
46
|
+
var: "Ref"
|
|
47
|
+
lhs: "Ref"
|
|
48
|
+
rhs: "Ref"
|
|
49
|
+
const_f: "Ref"
|
|
50
|
+
|
|
51
|
+
_active: bool
|
|
52
|
+
_constraints: list
|
|
53
|
+
|
|
54
|
+
def __init__(self, solver: "Solver", system: "System", **kw) -> None:
|
|
55
|
+
"""kwargs passed to compile"""
|
|
56
|
+
self.class_attr = self.solver = solver
|
|
57
|
+
self.system = system
|
|
58
|
+
self.compile(**kw)
|
|
59
|
+
|
|
60
|
+
def compile(self, **kwargs):
|
|
61
|
+
"""establishes the references for the solver to use directly"""
|
|
62
|
+
if self.solver.slvtype == "var":
|
|
63
|
+
self.var = self.system.locate_ref(self.solver.var)
|
|
64
|
+
self.system.debug(f"solving with indpendent: {self.var}")
|
|
65
|
+
|
|
66
|
+
elif self.solver.slvtype in ["ineq", "eq"]:
|
|
67
|
+
self.lhs = self.system.locate_ref(self.solver.lhs)
|
|
68
|
+
|
|
69
|
+
if hasattr(self.solver, "rhs") and (
|
|
70
|
+
isinstance(self.solver.rhs, str) or callable(self.solver.rhs)
|
|
71
|
+
):
|
|
72
|
+
self.rhs = self.system.locate_ref(self.solver.rhs)
|
|
73
|
+
elif hasattr(self.solver, "rhs") and self.solver.rhs is not None:
|
|
74
|
+
# raise Exception(f"bad rhs: {self.solver.rhs}")
|
|
75
|
+
self.rhs = self.solver.rhs
|
|
76
|
+
|
|
77
|
+
m = " = " if self.solver.slvtype == "eq" else " >= "
|
|
78
|
+
self.system.debug(f"solving {self.lhs}{m}{self.rhs}")
|
|
79
|
+
|
|
80
|
+
if hasattr(self, "rhs") and self.rhs is not None:
|
|
81
|
+
mult = 1
|
|
82
|
+
bias = None
|
|
83
|
+
power = None
|
|
84
|
+
# careful with kw vs positional args here
|
|
85
|
+
fun = lambda sys, prob: maybe_ref(
|
|
86
|
+
self.lhs,
|
|
87
|
+
SolverInstance,
|
|
88
|
+
sys=sys,
|
|
89
|
+
prob=prob,
|
|
90
|
+
mult=mult,
|
|
91
|
+
bias=bias,
|
|
92
|
+
power=power,
|
|
93
|
+
) - maybe_ref(
|
|
94
|
+
self.rhs,
|
|
95
|
+
SolverInstance,
|
|
96
|
+
sys=sys,
|
|
97
|
+
prob=prob,
|
|
98
|
+
mult=mult,
|
|
99
|
+
bias=bias,
|
|
100
|
+
power=power,
|
|
101
|
+
)
|
|
102
|
+
fun.__name__ = f"{self.lhs}_{self.rhs}_{self.solver.slvtype}"
|
|
103
|
+
objfunc = Ref(self.system, fun)
|
|
104
|
+
else:
|
|
105
|
+
objfunc = self.lhs
|
|
106
|
+
|
|
107
|
+
if log.log_level <= 6:
|
|
108
|
+
self.system.debug(f"const defined: {objfunc}|{self.solver}")
|
|
109
|
+
|
|
110
|
+
self.const_f = objfunc
|
|
111
|
+
|
|
112
|
+
elif self.solver.slvtype == "obj":
|
|
113
|
+
self.obj_ref = self.system.locate_ref(self.solver.obj)
|
|
114
|
+
if self.solver.kind == "max":
|
|
115
|
+
mult = None
|
|
116
|
+
bias = None
|
|
117
|
+
power = None
|
|
118
|
+
else:
|
|
119
|
+
mult = None
|
|
120
|
+
bias = None
|
|
121
|
+
power = None
|
|
122
|
+
|
|
123
|
+
fun = lambda sys, prob: maybe_ref(
|
|
124
|
+
self.obj_ref,
|
|
125
|
+
SolverInstance,
|
|
126
|
+
sys=sys,
|
|
127
|
+
prob=prob,
|
|
128
|
+
mult=mult,
|
|
129
|
+
bias=bias,
|
|
130
|
+
power=power,
|
|
131
|
+
)
|
|
132
|
+
fun.__name__ = f"obj_{self.solver.obj}_{self.solver.kind}"
|
|
133
|
+
self.obj = Ref(self.system, fun)
|
|
134
|
+
# self.obj = self.system.locate_ref(fun)
|
|
135
|
+
self.system.debug(f"solving with obj: {self.obj}")
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def constraints(self):
|
|
139
|
+
if hasattr(self, "_constraints"):
|
|
140
|
+
return self._constraints
|
|
141
|
+
if self.solver.slvtype in ["var", "obj"]:
|
|
142
|
+
return self.solver.constraints
|
|
143
|
+
else:
|
|
144
|
+
# just me
|
|
145
|
+
return [
|
|
146
|
+
{
|
|
147
|
+
"type": self.slvtype,
|
|
148
|
+
"var": self.solver.lhs,
|
|
149
|
+
"value": self.const_f,
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def slvtype(self):
|
|
155
|
+
return self.solver.slvtype
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def combos(self):
|
|
159
|
+
return self.solver.combos
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def normalize(self):
|
|
163
|
+
return self.solver.normalize
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def active(self):
|
|
167
|
+
if hasattr(self, "_active"):
|
|
168
|
+
return self._active
|
|
169
|
+
return self.solver.active
|
|
170
|
+
|
|
171
|
+
def get_alias(self, pre):
|
|
172
|
+
if self.solver.slvtype == "var":
|
|
173
|
+
return self.var.key # direct ref
|
|
174
|
+
return super().get_alias(pre) # default
|
|
175
|
+
|
|
176
|
+
def as_ref_dict(self):
|
|
177
|
+
out = {"var": None, "eq": None, "ineq": None, "obj": None}
|
|
178
|
+
|
|
179
|
+
if self.solver.slvtype == "var":
|
|
180
|
+
out["var"] = self.var
|
|
181
|
+
elif self.solver.slvtype == "obj":
|
|
182
|
+
out["obj"] = self.obj
|
|
183
|
+
elif self.solver.slvtype == "eq":
|
|
184
|
+
out["eq"] = self.const_f
|
|
185
|
+
elif self.solver.slvtype == "ineq":
|
|
186
|
+
out["ineq"] = self.const_f
|
|
187
|
+
|
|
188
|
+
return out
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class Solver(ATTR_BASE):
|
|
192
|
+
"""solver creates subclasses per solver balance"""
|
|
193
|
+
|
|
194
|
+
obj: dep_type = None
|
|
195
|
+
var: indep_type = None
|
|
196
|
+
rhs: ref_type = None
|
|
197
|
+
lhs: ref_type = None
|
|
198
|
+
slvtype: str
|
|
199
|
+
constraints: dict
|
|
200
|
+
combos: list = None
|
|
201
|
+
|
|
202
|
+
normalize: ref_type
|
|
203
|
+
allow_constraint_override: bool = True
|
|
204
|
+
|
|
205
|
+
attr_prefix = "Solver"
|
|
206
|
+
active: bool
|
|
207
|
+
instance_class = SolverInstance
|
|
208
|
+
|
|
209
|
+
@classmethod
|
|
210
|
+
def configure_for_system(cls, name, config_class, cb=None, **kwargs):
|
|
211
|
+
"""add the config class, and perform checks with `class_validate)
|
|
212
|
+
:returns: [optional] a dictionary of options to be used in the make_attribute method
|
|
213
|
+
"""
|
|
214
|
+
pre_name = cls.name # random attr name
|
|
215
|
+
super(Solver, cls).configure_for_system(name, config_class, cb, **kwargs)
|
|
216
|
+
|
|
217
|
+
# change name of constraint var if
|
|
218
|
+
if cls.slvtype == "var":
|
|
219
|
+
# provide defaults ot constraints, and update combo_var with attribut name
|
|
220
|
+
for const in cls.constraints:
|
|
221
|
+
if "combo_var" in const and const["combo_var"] == pre_name:
|
|
222
|
+
const["combo_var"] = name # update me
|
|
223
|
+
elif "combo_var" not in const:
|
|
224
|
+
const["combo_var"] = name # update me
|
|
225
|
+
|
|
226
|
+
if "combos" not in const:
|
|
227
|
+
const["combos"] = "default"
|
|
228
|
+
|
|
229
|
+
# TODO: add normalize attribute to tune optimizations
|
|
230
|
+
@classmethod
|
|
231
|
+
def declare_var(cls, var: str, **kwargs):
|
|
232
|
+
"""
|
|
233
|
+
Defines a solver variable for optimization in the class, constraints defined on this solver will correspond to the limits of that variable
|
|
234
|
+
:param var: The var attribute for the solver variable.
|
|
235
|
+
:param combos: The combinations of the solver variable.
|
|
236
|
+
:return: The setup class for the solver variable.
|
|
237
|
+
"""
|
|
238
|
+
assert "," not in var, f"var cannot have commas: {var}"
|
|
239
|
+
|
|
240
|
+
# Create A New Signals Class
|
|
241
|
+
active = kwargs.get("active", True)
|
|
242
|
+
combos = kwargs.get("combos", f"default,{var}") # default + var_name
|
|
243
|
+
|
|
244
|
+
new_name = f"Solver_var_{var}".replace(".", "_")
|
|
245
|
+
bkw = {"var": var, "value": None}
|
|
246
|
+
constraints = [{"type": "min", **bkw}, {"type": "max", **bkw}]
|
|
247
|
+
new_dict = dict(
|
|
248
|
+
name=new_name, # until configured for system, it gets the assigned name
|
|
249
|
+
active=active,
|
|
250
|
+
var=var,
|
|
251
|
+
slvtype="var",
|
|
252
|
+
constraints=constraints,
|
|
253
|
+
combos=cls.process_combos(combos),
|
|
254
|
+
default_options=cls.default_options.copy(),
|
|
255
|
+
)
|
|
256
|
+
return cls._setup_cls(new_name, new_dict)
|
|
257
|
+
|
|
258
|
+
@classmethod
|
|
259
|
+
def objective(cls, obj: str, **kwargs):
|
|
260
|
+
"""
|
|
261
|
+
Defines a solver variable for optimization in the class, constraints defined on this solver will correspond to the limits of that variable
|
|
262
|
+
|
|
263
|
+
:param obj: The var attribute for the solver variable.
|
|
264
|
+
:param combos: The combinations of the solver variable.
|
|
265
|
+
:vara kind: the kind of optimization, either min or max
|
|
266
|
+
:return: The setup class for the solver variable.
|
|
267
|
+
"""
|
|
268
|
+
# Create A New Signals Class
|
|
269
|
+
active = kwargs.get("active", True)
|
|
270
|
+
combos = kwargs.get("combos", "default")
|
|
271
|
+
kind = kwargs.get("kind", "min")
|
|
272
|
+
assert kind in ("min", "max")
|
|
273
|
+
|
|
274
|
+
new_name = f"Solver_obj_{obj}_{kind}".replace(".", "_")
|
|
275
|
+
bkw = {"var": obj, "value": None}
|
|
276
|
+
new_dict = dict(
|
|
277
|
+
name=new_name,
|
|
278
|
+
active=active,
|
|
279
|
+
obj=obj,
|
|
280
|
+
slvtype="obj",
|
|
281
|
+
kind=kind,
|
|
282
|
+
constraints=[],
|
|
283
|
+
combos=cls.process_combos(combos),
|
|
284
|
+
default_options=cls.default_options.copy(),
|
|
285
|
+
)
|
|
286
|
+
return cls._setup_cls(new_name, new_dict)
|
|
287
|
+
|
|
288
|
+
obj = objective
|
|
289
|
+
|
|
290
|
+
@classmethod
|
|
291
|
+
def constraint_equality(
|
|
292
|
+
cls, lhs: "system_property", rhs: "system_property" = 0, **kwargs
|
|
293
|
+
):
|
|
294
|
+
"""Defines an equality constraint based on a required lhs of equation, and an optional rhs, the difference of which will be driven to zero"""
|
|
295
|
+
combos = kwargs.get("combos", "default")
|
|
296
|
+
active = kwargs.get("active", True)
|
|
297
|
+
|
|
298
|
+
# Create A New Signals Class
|
|
299
|
+
new_name = f"Solver_coneq_{lhs}_{rhs}".replace(".", "_")
|
|
300
|
+
new_dict = dict(
|
|
301
|
+
name=new_name,
|
|
302
|
+
active=active,
|
|
303
|
+
lhs=lhs,
|
|
304
|
+
rhs=rhs,
|
|
305
|
+
slvtype="eq",
|
|
306
|
+
constraints=[],
|
|
307
|
+
combos=cls.process_combos(combos),
|
|
308
|
+
default_options=cls.default_options.copy(),
|
|
309
|
+
)
|
|
310
|
+
return cls._setup_cls(new_name, new_dict)
|
|
311
|
+
|
|
312
|
+
con_eq = constraint_equality
|
|
313
|
+
|
|
314
|
+
@classmethod
|
|
315
|
+
def constraint_inequality(cls, lhs: ref_type, rhs: ref_type = 0, **kwargs):
|
|
316
|
+
"""Defines an inequality constraint"""
|
|
317
|
+
combos = kwargs.get("combos", "default")
|
|
318
|
+
active = kwargs.get("active", True)
|
|
319
|
+
|
|
320
|
+
# Create A New Signals Class
|
|
321
|
+
new_name = f"Solver_conineq_{lhs}_{rhs}".replace(".", "_")
|
|
322
|
+
new_dict = dict(
|
|
323
|
+
name=new_name,
|
|
324
|
+
active=active,
|
|
325
|
+
lhs=lhs,
|
|
326
|
+
rhs=rhs,
|
|
327
|
+
slvtype="ineq",
|
|
328
|
+
constraints=[],
|
|
329
|
+
combos=cls.process_combos(combos),
|
|
330
|
+
default_options=cls.default_options.copy(),
|
|
331
|
+
)
|
|
332
|
+
return cls._setup_cls(new_name, new_dict)
|
|
333
|
+
|
|
334
|
+
con_ineq = constraint_inequality
|
|
335
|
+
|
|
336
|
+
# TODO: add minimize / maximize / objective options
|
|
337
|
+
|
|
338
|
+
@classmethod
|
|
339
|
+
def class_validate(cls, instance, **kwargs):
|
|
340
|
+
from engforge.properties import system_property
|
|
341
|
+
|
|
342
|
+
system = cls.config_cls
|
|
343
|
+
|
|
344
|
+
# TODO: normalize references to ref or constant here
|
|
345
|
+
# TODO: add a check for the solver to be a valid solver type
|
|
346
|
+
|
|
347
|
+
if cls.slvtype == "var":
|
|
348
|
+
var_type = system.locate(cls.var)
|
|
349
|
+
if var_type is None:
|
|
350
|
+
raise Exception(f"var not found: {cls.var}")
|
|
351
|
+
assert isinstance(
|
|
352
|
+
var_type, attrs.Attribute
|
|
353
|
+
), f"bad var {cls.var} not attribute: {var_type}"
|
|
354
|
+
assert var_type.type in (
|
|
355
|
+
int,
|
|
356
|
+
float,
|
|
357
|
+
), f"bad var {cls.var} not numeric"
|
|
358
|
+
|
|
359
|
+
elif cls.slvtype in ["ineq", "eq"]:
|
|
360
|
+
driv_type = system.locate(cls.lhs)
|
|
361
|
+
if driv_type is None:
|
|
362
|
+
raise Exception(f"LHS not found: {cls.lhs}")
|
|
363
|
+
assert isinstance(
|
|
364
|
+
driv_type, system_property
|
|
365
|
+
), f"bad LHS {cls.lhs} type: {driv_type}"
|
|
366
|
+
if cls.rhs is not None:
|
|
367
|
+
driv_type = system.locate(cls.rhs)
|
|
368
|
+
if driv_type is None:
|
|
369
|
+
raise Exception(f"RHS not found: {cls.rhs}")
|
|
370
|
+
assert isinstance(
|
|
371
|
+
driv_type, system_property
|
|
372
|
+
), f"bad RHS {cls.rhs} type: {driv_type}"
|
|
373
|
+
|
|
374
|
+
else:
|
|
375
|
+
raise Exception(f"bad slvtype: {cls.slvtype}")
|
|
376
|
+
|
|
377
|
+
@classmethod
|
|
378
|
+
def create_instance(cls, system: "System") -> SolverInstance:
|
|
379
|
+
return SolverInstance(cls, system)
|
|
380
|
+
|
|
381
|
+
@classmethod
|
|
382
|
+
def add_var_constraint(cls, value, kind="min", **kwargs):
|
|
383
|
+
"""adds a `type` constraint to the solver. If value is numeric it is used as a bound with `scipy` optimize.
|
|
384
|
+
|
|
385
|
+
If value is a function it should be of the form value(Xarray) and will establish an inequality constraint that var var must be:
|
|
386
|
+
1. less than for max
|
|
387
|
+
2. more than for min
|
|
388
|
+
|
|
389
|
+
During the evaluation of the limit function system.X should be set, and pre_execute() have already run.
|
|
390
|
+
|
|
391
|
+
:param type: str, must be either min or max with var value comparison, or with a function additionally eq,ineq (same as max(0)) can be used
|
|
392
|
+
:value: either a numeric (int,float), or a function, f(system)
|
|
393
|
+
"""
|
|
394
|
+
assert cls is not Solver, f"must set constraint on Solver subclass"
|
|
395
|
+
# assert not cls.constraint_exists(type=kind,var=var), f"constraint already exists!"
|
|
396
|
+
assert isinstance(value, (int, float)) or callable(
|
|
397
|
+
value
|
|
398
|
+
), f"only int,float or callables allow. Callables must take system as argument"
|
|
399
|
+
|
|
400
|
+
var = cls.var
|
|
401
|
+
assert var is not None, "must provide var on non-var solvers"
|
|
402
|
+
assert cls.slvtype == "var", "only Solver.declare_var can have constraints"
|
|
403
|
+
assert kind in ("min", "max")
|
|
404
|
+
|
|
405
|
+
combo_dflt = "default,lim"
|
|
406
|
+
cmbs = kwargs.get("combos", "")
|
|
407
|
+
combos = cls.process_combos(cmbs, combo_dflt, combo_dflt)
|
|
408
|
+
if isinstance(combos, str):
|
|
409
|
+
combos = combos.split(",")
|
|
410
|
+
active = kwargs.get("active", True)
|
|
411
|
+
const = {
|
|
412
|
+
"type": kind,
|
|
413
|
+
"value": value,
|
|
414
|
+
"var": var,
|
|
415
|
+
"active": active,
|
|
416
|
+
"combos": combos,
|
|
417
|
+
"combo_var": cls.name,
|
|
418
|
+
}
|
|
419
|
+
# print(const,cls.__dict__)
|
|
420
|
+
|
|
421
|
+
cinx = cls.constraint_exists(type=kind, var=var)
|
|
422
|
+
inix = cinx is not None
|
|
423
|
+
if cls.allow_constraint_override and inix:
|
|
424
|
+
log.debug(f"replacing constraint {cinx} with {kind} {value} {var}")
|
|
425
|
+
constraint = cls.constraints[cinx]
|
|
426
|
+
constraint.update(const)
|
|
427
|
+
elif not cls.allow_constraint_override and inix:
|
|
428
|
+
cnx = cls.constraints[cinx]
|
|
429
|
+
raise Exception(f"constraint already exists! {cnx}")
|
|
430
|
+
else:
|
|
431
|
+
cls.constraints.append(const)
|
|
432
|
+
|
|
433
|
+
@classmethod
|
|
434
|
+
def constraint_exists(cls, **kw):
|
|
435
|
+
"""check constraints on the system, return its index position if found, else None."""
|
|
436
|
+
for i, c in enumerate(cls.constraints):
|
|
437
|
+
if all([c[k] == v for k, v in kw.items()]):
|
|
438
|
+
return i
|
|
439
|
+
return None
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
# Support Previous SnakeCase
|
|
443
|
+
Solver = Solver
|