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/solver_utils.py
ADDED
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
from engforge.system_reference import *
|
|
2
|
+
from engforge.logging import LoggingMixin
|
|
3
|
+
|
|
4
|
+
# from engforge.execution_context import *
|
|
5
|
+
import numpy as np
|
|
6
|
+
import fnmatch
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SolverUtilLog(LoggingMixin):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
log = SolverUtilLog()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Objective functions & Utilities
|
|
17
|
+
def f_lin_min(comp, prob, Xref, Yref, weights=None, *args, **kw):
|
|
18
|
+
"""
|
|
19
|
+
Creates an anonymous function with stored references to comp, Yref, weights, that returns a scipy optimize friendly function of (x, Xref, *a, **kw) x which corresponds to the order of Xref dicts, and the other inputs are up to application.
|
|
20
|
+
|
|
21
|
+
:param comp: the comp object
|
|
22
|
+
:param Xref: a dictionary of reference values for X
|
|
23
|
+
:param Yref: a dictionary of reference values for Y
|
|
24
|
+
:param weights: optional weights for Yref
|
|
25
|
+
:param args: additional positional arguments
|
|
26
|
+
:param kw: additional keyword arguments
|
|
27
|
+
|
|
28
|
+
:return: the anonymous function
|
|
29
|
+
"""
|
|
30
|
+
from engforge.problem_context import ProblemExec
|
|
31
|
+
|
|
32
|
+
# TODO: move these to problem context!!!
|
|
33
|
+
mult_pos = kw.pop("mult_pos", 1)
|
|
34
|
+
exp_pos = kw.pop("exp_pos", 1)
|
|
35
|
+
mult_neg = kw.pop("mult_neg", 1)
|
|
36
|
+
exp_neg = kw.pop("exp_neg", 1)
|
|
37
|
+
gam = norm_base = kw.pop("norm_base", 1)
|
|
38
|
+
inputs = [mult_neg, exp_neg, mult_pos, exp_pos, gam, norm_base]
|
|
39
|
+
is_lin = all(v == 1 for v in inputs)
|
|
40
|
+
|
|
41
|
+
solver_ref = comp.collect_solver_refs()
|
|
42
|
+
solver_types = solver_ref.get("type", {}).get("solver", {})
|
|
43
|
+
base_dict = {
|
|
44
|
+
"comp": comp,
|
|
45
|
+
"Yref": Yref,
|
|
46
|
+
"weights": weights,
|
|
47
|
+
"args": args,
|
|
48
|
+
"kw": kw,
|
|
49
|
+
}
|
|
50
|
+
xkey = "_".join(Xref.keys())
|
|
51
|
+
ykey = "_".join(Yref.keys())
|
|
52
|
+
alias_name = kw.pop("alias_name", f"min_X_{xkey}_Y_[{ykey}]")
|
|
53
|
+
|
|
54
|
+
vars = list(Xref.keys()) # static x_basis
|
|
55
|
+
yvar = list(Yref.keys())
|
|
56
|
+
weights = weights if weights is not None else np.ones(len(Yref))
|
|
57
|
+
|
|
58
|
+
def f(x, *rt_a, **rt_kw):
|
|
59
|
+
# anonymous function
|
|
60
|
+
# get new values based x de
|
|
61
|
+
Xnext = {p: xi for p, xi in zip(vars, x)}
|
|
62
|
+
|
|
63
|
+
# with revert_X(comp,Xref,Xnext=Xnext):
|
|
64
|
+
# this will run all signals and updates selected in outer context
|
|
65
|
+
with ProblemExec(comp, {}, Xnew=Xnext, ctx_fail_new=True) as exc:
|
|
66
|
+
grp = (yvar, weights)
|
|
67
|
+
vals, pos, neg = [], [], []
|
|
68
|
+
for p, n in zip(*grp):
|
|
69
|
+
ty = solver_types.get(p, None)
|
|
70
|
+
if (
|
|
71
|
+
ty is None
|
|
72
|
+
or not hasattr(ty.solver, "kind")
|
|
73
|
+
or ty.solver.kind == "min"
|
|
74
|
+
):
|
|
75
|
+
arry = pos
|
|
76
|
+
elif ty.solver.kind == "max":
|
|
77
|
+
arry = neg
|
|
78
|
+
else:
|
|
79
|
+
comp.warning(f"non minmax obj: {p} {ty.solver.kind}")
|
|
80
|
+
|
|
81
|
+
ref = Yref[p]
|
|
82
|
+
val = eval_ref(ref, ref.comp, prob) * n
|
|
83
|
+
arry.append(val)
|
|
84
|
+
vals.append(val)
|
|
85
|
+
|
|
86
|
+
if not is_lin:
|
|
87
|
+
ps = mult_pos * np.array(pos) ** exp_pos
|
|
88
|
+
# Min-Max Logic
|
|
89
|
+
if neg:
|
|
90
|
+
ns = mult_neg * np.array(neg) ** exp_neg
|
|
91
|
+
ns = np.sum(ns)
|
|
92
|
+
out = np.sum(ps) ** gam - np.sum(ns) ** gam
|
|
93
|
+
else:
|
|
94
|
+
out = mult_pos * np.sum(ps) ** gam
|
|
95
|
+
|
|
96
|
+
if comp.log_level < 5:
|
|
97
|
+
comp.debug(f"obj {alias_name}: {x} -> {vals}")
|
|
98
|
+
|
|
99
|
+
return out # n sized normal residual vector
|
|
100
|
+
else:
|
|
101
|
+
# the faster linear case
|
|
102
|
+
if neg:
|
|
103
|
+
return np.sum(np.array(pos)) - np.sum(np.array(neg))
|
|
104
|
+
else:
|
|
105
|
+
return np.sum(np.array(pos))
|
|
106
|
+
|
|
107
|
+
f.__name__ = f'min_Y_{"_".join(Yref.keys())}_X_{"_".join(Xref.keys())}]'
|
|
108
|
+
|
|
109
|
+
return f
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def ref_to_val_constraint(
|
|
113
|
+
system,
|
|
114
|
+
comp,
|
|
115
|
+
ctx,
|
|
116
|
+
Xrefs,
|
|
117
|
+
var_ref,
|
|
118
|
+
kind,
|
|
119
|
+
val,
|
|
120
|
+
contype="ineq",
|
|
121
|
+
return_ref=False,
|
|
122
|
+
*args,
|
|
123
|
+
**kwargs,
|
|
124
|
+
):
|
|
125
|
+
"""takes a var reference and a value and returns a function that can be used as a constraint for min/max cases. The function will be a function of the comp and the info dictionary. The function will return the difference between the var value and the value."""
|
|
126
|
+
p = var_ref
|
|
127
|
+
if isinstance(val, Ref):
|
|
128
|
+
if kind == "min":
|
|
129
|
+
fun = lambda comp, prob: p.value(comp, prob) - val.value(comp, prob)
|
|
130
|
+
else:
|
|
131
|
+
fun = lambda comp, prob: val.value(comp, prob) - p.value(comp, prob)
|
|
132
|
+
fun.__name__ = f"REF{val.comp}.{kind}.{p.key}"
|
|
133
|
+
ref = Ref(val.comp, fun)
|
|
134
|
+
|
|
135
|
+
# Function Case
|
|
136
|
+
elif callable(val):
|
|
137
|
+
# print('ref to val con', val,kind,p)
|
|
138
|
+
if kind == "min":
|
|
139
|
+
fun = lambda comp, prob: p.value(comp, prob) - val(comp, prob)
|
|
140
|
+
else:
|
|
141
|
+
fun = lambda comp, prob: val(comp, prob) - p.value(comp, prob)
|
|
142
|
+
fun.__name__ = f"REF.{kind}.{val.__name__}"
|
|
143
|
+
ref = Ref(p.comp, fun) # comp shouldn't matter
|
|
144
|
+
|
|
145
|
+
elif isinstance(val, (int, float)):
|
|
146
|
+
if kind == "min":
|
|
147
|
+
fun = lambda comp, prob: p.value(comp, prob) - val
|
|
148
|
+
else:
|
|
149
|
+
fun = lambda comp, prob: val - p.value(comp, prob)
|
|
150
|
+
fun.__name__ = f"REF.{kind}.{val}"
|
|
151
|
+
ref = Ref(p.comp, fun) # comp shouldn't matter
|
|
152
|
+
else:
|
|
153
|
+
raise ValueError(f"bad constraint value: {val}")
|
|
154
|
+
|
|
155
|
+
if return_ref:
|
|
156
|
+
return ref
|
|
157
|
+
|
|
158
|
+
# Make Objective
|
|
159
|
+
return create_constraint(system, comp, Xrefs, contype, ref, ctx, *args, **kwargs)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def create_constraint(
|
|
163
|
+
system, comp, Xref, contype: str, ref, prob, con_args=None, *args, **kwargs
|
|
164
|
+
):
|
|
165
|
+
"""creates a constraint with bounded solver input from a constraint definition in dictionary with type and value. If value is a function it will be evaluated with the extra arguments provided. If var is None, then the constraint is assumed to be not in reference to the comp x vars, otherwise lookups are made to that var.
|
|
166
|
+
|
|
167
|
+
Creates F(x_solver:array) such that the current vars of comp are reverted to after the function has returned, which is used directly by SciPy's optimize.minimize
|
|
168
|
+
|
|
169
|
+
"""
|
|
170
|
+
assert contype in (
|
|
171
|
+
"eq",
|
|
172
|
+
"ineq",
|
|
173
|
+
), f"bad constraint type: {contype}"
|
|
174
|
+
|
|
175
|
+
if comp.log_level < 5:
|
|
176
|
+
comp.debug(f"create constraint {contype} {ref} {args} {kwargs}| {con_args}")
|
|
177
|
+
|
|
178
|
+
# its a function
|
|
179
|
+
_fun = lambda *args, **kw: ref.value(*args, **kw)
|
|
180
|
+
_fun.__name__ = f"const_{contype}_{ref.comp.classname}_{ref.key}"
|
|
181
|
+
fun = objectify(system, _fun, comp, Xref, prob, *args, **kwargs)
|
|
182
|
+
cons = {"type": contype, "fun": fun}
|
|
183
|
+
if con_args:
|
|
184
|
+
cons["args"] = con_args
|
|
185
|
+
return cons
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# signature in solve: refmin_solve(comp,comp,Xref)
|
|
189
|
+
def objectify(system, function, comp, Xrefs, prob, *args, **kwargs):
|
|
190
|
+
"""converts a function f(comp,slv_info:dict) into a function that safely changes states to the desired values and then runs the function. A function is returend as f(x,*args,**kw)"""
|
|
191
|
+
from engforge.problem_context import ProblemExec
|
|
192
|
+
|
|
193
|
+
lvl_name = f'obj_{str(function.__name__.split("<function")[0])}'
|
|
194
|
+
alias_name = kwargs.pop("alias_name", lvl_name)
|
|
195
|
+
|
|
196
|
+
# hello anonymous function
|
|
197
|
+
def f_obj(x, *rt_args, **rt_kwargs):
|
|
198
|
+
new_state = {p: x[i] for i, p in enumerate(Xrefs)}
|
|
199
|
+
# Enter existing problem context
|
|
200
|
+
with ProblemExec(
|
|
201
|
+
system, {}, Xnew=new_state, ctx_fail_new=True, level_name=lvl_name
|
|
202
|
+
) as exc:
|
|
203
|
+
out = function(comp, prob)
|
|
204
|
+
|
|
205
|
+
if comp.log_level <= 10:
|
|
206
|
+
comp.debug(f"obj {alias_name}: {x} -> {out}")
|
|
207
|
+
return out
|
|
208
|
+
|
|
209
|
+
if comp.log_level < 3:
|
|
210
|
+
comp.msg(f"obj setup {function} - > {f_obj}")
|
|
211
|
+
comp.msg(inspect.getsource(function))
|
|
212
|
+
|
|
213
|
+
fo = lambda x, *a, **kw: f_obj(x, prob, *a, **kw)
|
|
214
|
+
fo.__name__ = f"OBJ_{alias_name}"
|
|
215
|
+
return fo
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# TODO: integrate / merge with ProblemExec (all below)
|
|
219
|
+
def refmin_solve(
|
|
220
|
+
comp,
|
|
221
|
+
prob,
|
|
222
|
+
Xref: dict,
|
|
223
|
+
Yref: dict,
|
|
224
|
+
Xo=None,
|
|
225
|
+
weights: np.array = None,
|
|
226
|
+
ffunc=f_lin_min,
|
|
227
|
+
**kw,
|
|
228
|
+
):
|
|
229
|
+
"""minimize the difference between two dictionaries of refernces, x references are changed in place, y will be solved to zero, options ensue for cases where the solution is not ideal
|
|
230
|
+
|
|
231
|
+
:param Xref: dictionary of references to the x values, or independents
|
|
232
|
+
:param Yref: dictionary of references to the value of objectives to be minimized
|
|
233
|
+
:param Xo: initial guess for the x values as a list against Xref order, or a dictionary
|
|
234
|
+
:param weights: a dictionary of values to weights the x values by, list also ok as long as same length and order as Xref
|
|
235
|
+
:param reset: if the solution fails, reset the x values to their original state, if true will reset the x values to their original state on failure overiding doset.
|
|
236
|
+
:param doset: if the solution is successful, set the x values to the solution by default, otherwise follows reset, if not successful reset is checked first, then doset
|
|
237
|
+
"""
|
|
238
|
+
vars = list(Xref.keys()) # static x_basis
|
|
239
|
+
|
|
240
|
+
# The above code is a Python script that includes a TODO comment indicating
|
|
241
|
+
# that x-normalization needs to be incorporated into the code. The code itself
|
|
242
|
+
# is not shown, but it likely involves some operations on a variable or data
|
|
243
|
+
# structure named "x" that require normalization. The TODO comment serves as a
|
|
244
|
+
# reminder for the developer to implement this functionality at a later time.
|
|
245
|
+
# TODO: incorporate x-normilization
|
|
246
|
+
norm_x, weights = handle_normalize(weights, Xref, Yref)
|
|
247
|
+
|
|
248
|
+
# make objective function
|
|
249
|
+
Fc = ffunc(comp, prob, Xref, Yref, weights)
|
|
250
|
+
Fc.__name__ = ffunc.__name__
|
|
251
|
+
|
|
252
|
+
if Xo is None:
|
|
253
|
+
Xo = [Xref[p].value() for p in vars]
|
|
254
|
+
|
|
255
|
+
elif isinstance(Xo, dict):
|
|
256
|
+
Xo = [Xo[p] for p in vars]
|
|
257
|
+
|
|
258
|
+
# TODO: IO for jacobean and state estimations (complex as function definition, requires learning)
|
|
259
|
+
comp.debug(f"minimize! {Fc.__name__,Xo,vars,kw}")
|
|
260
|
+
|
|
261
|
+
# kw.pop('prob',None)
|
|
262
|
+
kw.pop("info", None) # info added from context constraints
|
|
263
|
+
ans = sciopt.minimize(Fc, Xo, **kw)
|
|
264
|
+
|
|
265
|
+
return ans
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def handle_normalize(norm, Xref, Yref):
|
|
269
|
+
vars = list(Xref.keys()) # static x_basis
|
|
270
|
+
if norm is None:
|
|
271
|
+
normX = np.ones(len(vars))
|
|
272
|
+
normY = np.ones(len(Yref))
|
|
273
|
+
elif isinstance(norm, (list, tuple, np.ndarray)):
|
|
274
|
+
assert len(norm) == len(vars), "bad length-X norm input"
|
|
275
|
+
normX = np.array(norm)
|
|
276
|
+
normY = np.ones(len(Yref)) # default to 1
|
|
277
|
+
elif isinstance(norm, (dict)):
|
|
278
|
+
normX = np.array([norm[p] if p in norm else 1 for p in vars])
|
|
279
|
+
normY = np.array([norm[p] if p in norm else 1 for p in Yref])
|
|
280
|
+
|
|
281
|
+
return normX, normY
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# Parsing Utilities
|
|
285
|
+
def arg_var_compare(var, seq):
|
|
286
|
+
if "*" in seq:
|
|
287
|
+
return fnmatch.fnmatch(var, seq)
|
|
288
|
+
else:
|
|
289
|
+
return var == seq
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def str_list_f(out: list, sep=","):
|
|
293
|
+
if isinstance(out, str):
|
|
294
|
+
out = out.split(sep)
|
|
295
|
+
return out
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def ext_str_list(extra_kw, key, default=None):
|
|
299
|
+
if key in extra_kw:
|
|
300
|
+
out = extra_kw[key]
|
|
301
|
+
return str_list_f(out)
|
|
302
|
+
else:
|
|
303
|
+
out = default
|
|
304
|
+
return out
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
SLVR_SCOPE_PARM = [
|
|
308
|
+
"solver.eq",
|
|
309
|
+
"solver.ineq",
|
|
310
|
+
"solver.var",
|
|
311
|
+
"solver.obj",
|
|
312
|
+
"time.var",
|
|
313
|
+
"time.rate",
|
|
314
|
+
"dynamics.state",
|
|
315
|
+
"dynamics.rate",
|
|
316
|
+
]
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def combo_filter(attr_name, var_name, solver_inst, extra_kw, combos=None) -> bool:
|
|
320
|
+
# TODO: allow solver_inst to be None for dyn-classes
|
|
321
|
+
# proceed to filter active items if vars / combos inputs is '*' select all, otherwise discard if not active
|
|
322
|
+
# corresondes to problem_context.slv_dflt_options
|
|
323
|
+
if extra_kw is None:
|
|
324
|
+
extra_kw = {}
|
|
325
|
+
|
|
326
|
+
outa = True
|
|
327
|
+
if extra_kw.get("only_active", True):
|
|
328
|
+
outa = filt_active(var_name, solver_inst, extra_kw=extra_kw, dflt=False)
|
|
329
|
+
if not outa:
|
|
330
|
+
log.msg(
|
|
331
|
+
f"filt not active: {var_name:>10} {attr_name:>15}| C:{False}\tV:{False}\tA:{False}\tO:{False}"
|
|
332
|
+
)
|
|
333
|
+
return False
|
|
334
|
+
|
|
335
|
+
both_match = extra_kw.get("both_match", True)
|
|
336
|
+
# Otherwise look at the combo filter, its its false return that
|
|
337
|
+
outc = filter_combos(var_name, solver_inst, extra_kw, combos)
|
|
338
|
+
outp = None
|
|
339
|
+
|
|
340
|
+
# if the combo filter didn't explicitly fail, check the var filter
|
|
341
|
+
if (outc and both_match) or (not outc and not both_match):
|
|
342
|
+
outp = filter_vals(var_name, solver_inst, extra_kw)
|
|
343
|
+
if log.log_level <= 2:
|
|
344
|
+
log.msg(f"both match? {var_name:>20}| {outp} {outc}")
|
|
345
|
+
|
|
346
|
+
if both_match:
|
|
347
|
+
outr = all((outp, outc))
|
|
348
|
+
else:
|
|
349
|
+
outr = any((outp, outc))
|
|
350
|
+
else:
|
|
351
|
+
if log.log_level <= 2:
|
|
352
|
+
log.msg(f"initial match: {var_name:>20}| {outc}")
|
|
353
|
+
outr = outc
|
|
354
|
+
|
|
355
|
+
fin = bool(outr) and outa
|
|
356
|
+
|
|
357
|
+
if not fin:
|
|
358
|
+
log.debug(
|
|
359
|
+
f"filter: {var_name:>20} {attr_name:>15}| C:{outc}\tV:{outp}\tA:{outa}\tO:{fin}"
|
|
360
|
+
)
|
|
361
|
+
elif fin:
|
|
362
|
+
log.debug(
|
|
363
|
+
f"filter: {var_name:>20} {attr_name:>15}| C:{outc}\tV:{outp}\tA:{outa}\tO:{fin}| {combos}"
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
return fin
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# filters return true for items they want to remove
|
|
370
|
+
def filter_combos(var, inst, extra_kw=None, combos_in=None):
|
|
371
|
+
from engforge.attr_solver import SolverInstance
|
|
372
|
+
from engforge.attr_dynamics import IntegratorInstance
|
|
373
|
+
from engforge.attr_signals import SignalInstance
|
|
374
|
+
|
|
375
|
+
# not considered
|
|
376
|
+
|
|
377
|
+
if log.log_level <= 2:
|
|
378
|
+
log.info(f"checking combos: {var} {inst} {extra_kw} {combos_in}")
|
|
379
|
+
|
|
380
|
+
# gather combos either given or not
|
|
381
|
+
if combos_in is None and not isinstance(
|
|
382
|
+
inst, (SolverInstance, IntegratorInstance, SignalInstance)
|
|
383
|
+
):
|
|
384
|
+
return True
|
|
385
|
+
elif combos_in:
|
|
386
|
+
combos = combos_in
|
|
387
|
+
else:
|
|
388
|
+
combos = str_list_f(getattr(inst, "combos", "default"))
|
|
389
|
+
|
|
390
|
+
# parse extra kwargs
|
|
391
|
+
groups = ext_str_list(extra_kw, "combos", "")
|
|
392
|
+
bm = extra_kw.get("both_match", True)
|
|
393
|
+
if groups is None:
|
|
394
|
+
return bm if bm is True else None
|
|
395
|
+
|
|
396
|
+
igngrp = ext_str_list(extra_kw, "ign_combos", None)
|
|
397
|
+
onlygrp = ext_str_list(extra_kw, "only_combos", None)
|
|
398
|
+
|
|
399
|
+
if not combos:
|
|
400
|
+
log.info(f"no combos for {var} {combos} {groups}")
|
|
401
|
+
return None # no combos to filter to match, its permanent
|
|
402
|
+
|
|
403
|
+
# check values, and return on first match
|
|
404
|
+
for var in combos:
|
|
405
|
+
initial_match = [grp for grp in groups if arg_var_compare(var, grp)]
|
|
406
|
+
if not any(initial_match):
|
|
407
|
+
if log.log_level < 3:
|
|
408
|
+
log.msg(f"skip {var}: nothing ")
|
|
409
|
+
continue # not even
|
|
410
|
+
|
|
411
|
+
if onlygrp and not any(arg_var_compare(var, grp) for grp in onlygrp):
|
|
412
|
+
if log.log_level < 3:
|
|
413
|
+
log.msg(f"skip {var} not in {onlygrp}")
|
|
414
|
+
continue
|
|
415
|
+
|
|
416
|
+
if igngrp and any(arg_var_compare(var, grp) for grp in igngrp):
|
|
417
|
+
if log.log_level < 3:
|
|
418
|
+
log.msg(f"skip {var} in {igngrp}")
|
|
419
|
+
continue
|
|
420
|
+
|
|
421
|
+
return True # you've passed all the filters and found a match
|
|
422
|
+
|
|
423
|
+
return False # no matches, return false
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def filter_vals(var, inst, extra_kw=None):
|
|
427
|
+
from engforge.attr_solver import SolverInstance
|
|
428
|
+
from engforge.attr_dynamics import IntegratorInstance
|
|
429
|
+
from engforge.attr_signals import SignalInstance
|
|
430
|
+
from engforge.dynamics import DynamicsMixin
|
|
431
|
+
|
|
432
|
+
# add_vars = ext_str_list(extra_kw,'add_vars',None)
|
|
433
|
+
groups = ext_str_list(extra_kw, "slv_vars", "")
|
|
434
|
+
if groups is None:
|
|
435
|
+
return True # no var filter!
|
|
436
|
+
|
|
437
|
+
igngrp = ext_str_list(extra_kw, "ign_vars", None)
|
|
438
|
+
onlygrp = ext_str_list(extra_kw, "only_vars", None)
|
|
439
|
+
|
|
440
|
+
if log.log_level <= 2:
|
|
441
|
+
log.info(f"checking vals: {var} {inst} {extra_kw}")
|
|
442
|
+
|
|
443
|
+
# vars not considered
|
|
444
|
+
if not isinstance(
|
|
445
|
+
inst,
|
|
446
|
+
(SolverInstance, IntegratorInstance, SignalInstance, DynamicsMixin),
|
|
447
|
+
):
|
|
448
|
+
return True
|
|
449
|
+
|
|
450
|
+
# add vars overrides all other filters
|
|
451
|
+
# NOTE: add_vars is for variables that aren't defined already, slv_vars is for variables that are defined
|
|
452
|
+
# if add_vars:
|
|
453
|
+
# if any([arg_var_compare(var,avar) for avar in add_vars]):
|
|
454
|
+
# if log.log_level < 3:
|
|
455
|
+
# log.msg(f'adding solver var')
|
|
456
|
+
# return True
|
|
457
|
+
|
|
458
|
+
# check values, and filters
|
|
459
|
+
initial_match = [grp for grp in groups if arg_var_compare(var, grp)]
|
|
460
|
+
if log.log_level < 2:
|
|
461
|
+
log.msg(f"initial match: {var} in {initial_match}")
|
|
462
|
+
|
|
463
|
+
if not any(initial_match):
|
|
464
|
+
if log.log_level < 3:
|
|
465
|
+
log.msg(f"skip {var}: nothing")
|
|
466
|
+
return False
|
|
467
|
+
if onlygrp and not any(arg_var_compare(var, grp) for grp in onlygrp):
|
|
468
|
+
if log.log_level < 3:
|
|
469
|
+
log.msg(f"skip {var} not in {onlygrp}")
|
|
470
|
+
return False
|
|
471
|
+
if igngrp and any(arg_var_compare(var, grp) for grp in igngrp):
|
|
472
|
+
if log.log_level < 3:
|
|
473
|
+
log.msg(f"skip {var} in {igngrp}")
|
|
474
|
+
return False
|
|
475
|
+
return True
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def filt_active(var, inst, extra_kw=None, dflt=False):
|
|
479
|
+
from engforge.attr_solver import SolverInstance
|
|
480
|
+
from engforge.attr_dynamics import IntegratorInstance
|
|
481
|
+
from engforge.attr_signals import SignalInstance
|
|
482
|
+
|
|
483
|
+
# not considered
|
|
484
|
+
if not isinstance(inst, (SolverInstance, IntegratorInstance, SignalInstance)):
|
|
485
|
+
return True
|
|
486
|
+
|
|
487
|
+
activate = ext_str_list(extra_kw, "activate", [])
|
|
488
|
+
deactivate = ext_str_list(extra_kw, "deactivate", [])
|
|
489
|
+
|
|
490
|
+
act = inst.is_active(dflt) # default is inclusion boolean
|
|
491
|
+
|
|
492
|
+
# check for possibilities of activation
|
|
493
|
+
if not act and not activate:
|
|
494
|
+
if log.log_level < 3:
|
|
495
|
+
log.msg(f"skip {var}: not active")
|
|
496
|
+
return False # shortcut
|
|
497
|
+
if activate and any(arg_var_compare(var, grp) for grp in activate):
|
|
498
|
+
if log.log_level < 3:
|
|
499
|
+
log.msg(f"{var} activated!")
|
|
500
|
+
return True
|
|
501
|
+
if deactivate and any(arg_var_compare(var, grp) for grp in deactivate):
|
|
502
|
+
if log.log_level < 3:
|
|
503
|
+
log.msg(f"{var} deactivated!")
|
|
504
|
+
return False
|
|
505
|
+
log.msg(f"{var} is active={act}!")
|
|
506
|
+
return act
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
#
|
|
510
|
+
# def secondary_obj(
|
|
511
|
+
# obj_f, comp,Xrefs,normalize=None,base_func=f_lin_min,*args,**kwargs
|
|
512
|
+
# ):
|
|
513
|
+
# """modifies an objective function with a secondary function that is only considered when the primary function is minimized."""
|
|
514
|
+
# from engforge.problem_context import ProblemExec
|
|
515
|
+
# vars = list(Xrefs.keys()) # static x_basis
|
|
516
|
+
# base_dict = dict(comp=comp,args=args,Xrefs=Xrefs,**kwargs)
|
|
517
|
+
# lvl_name = f'{obj_f.__name__}_scndry'
|
|
518
|
+
# alias_name = kwargs.pop("alias_name", lvl_name)
|
|
519
|
+
#
|
|
520
|
+
# def f(x,*rt_args,**rt_kwargs):
|
|
521
|
+
#
|
|
522
|
+
# new_state = {p: x[i] for i, p in enumerate(Xrefs)}
|
|
523
|
+
# base_call = base_func(comp, Xrefs, normalize)
|
|
524
|
+
# #with revert_X(comp, Xrefs,Xnext=new_state) as x_prev:
|
|
525
|
+
#
|
|
526
|
+
# #Enter existing problem context
|
|
527
|
+
# with ProblemExec(comp,{},Xnew=new_state,ctx_fail_new=True,level_name=lvl_name) as exc:
|
|
528
|
+
# A = base_call(x)
|
|
529
|
+
# # solver_info = base_dict.copy()
|
|
530
|
+
# # solver_info.update(x=x,Xrefs=Xrefs,normalize=normalize,rt_args=rt_args, **rt_kwargs)
|
|
531
|
+
#
|
|
532
|
+
# out = A * (1 + obj_f(comp, prob))
|
|
533
|
+
# if comp.log_level < 5:
|
|
534
|
+
# comp.msg(f'obj {alias_name}: {x} -> {out}')
|
|
535
|
+
# return out
|
|
536
|
+
#
|
|
537
|
+
# if comp.log_level < 18:
|
|
538
|
+
# comp.debug(f'secondary setup {obj_f} - > {f}')
|
|
539
|
+
# comp.debug(inspect.getsource(function))
|
|
540
|
+
#
|
|
541
|
+
# return f
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
# TODO: integrate / merge with ProblemExec (all cmted out)
|
|
545
|
+
# def misc_to_ref(comp,val,*args,**kwargs):
|
|
546
|
+
# """takes a var reference and a value and returns a function that can be used as a constraint for min/max cases. The function will return the difference between the var value and the value.
|
|
547
|
+
# """
|
|
548
|
+
# if isinstance(val,Ref):
|
|
549
|
+
# #fun = lambda *a,**kw:val.value()
|
|
550
|
+
# ref = Ref(val.comp,val)
|
|
551
|
+
# #Function Case
|
|
552
|
+
# elif callable(val):
|
|
553
|
+
# fun = lambda comp,ctx: val(comp,ctx)
|
|
554
|
+
# fun.__name__ = val.__name__
|
|
555
|
+
# ref = Ref(None,fun) #comp shouldn't matter
|
|
556
|
+
# elif isinstance(val,(int,float)):
|
|
557
|
+
# fun = lambda comp,ctx: val
|
|
558
|
+
# fun.__name__ = f'const_{str(val)}'
|
|
559
|
+
# ref = Ref(None,fun) #comp shouldn't matter
|
|
560
|
+
# else:
|
|
561
|
+
# raise ValueError(f"bad constraint value: {val}")
|
|
562
|
+
#
|
|
563
|
+
# return ref
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
# Reference Jacobean Calculation
|
|
567
|
+
# TODO: hessian ect...
|
|
568
|
+
# def calcJacobean(
|
|
569
|
+
# sys, Yrefs: dict, Xrefs: dict, X0: dict = None, pct=0.001, diff=0.0001
|
|
570
|
+
# ):
|
|
571
|
+
# """
|
|
572
|
+
# returns the jacobiean by modifying X' <= X*pct + diff and recording the differences. When abs(x) < pct x' = x*1.1 + diff.
|
|
573
|
+
#
|
|
574
|
+
# jacobean will be ordered by Xrefs/Yrefs, so use ordered dict to keep order
|
|
575
|
+
# """
|
|
576
|
+
#
|
|
577
|
+
# if X0 is None:
|
|
578
|
+
# X0 = refset_get(Xrefs)
|
|
579
|
+
#
|
|
580
|
+
# assert len(Xrefs) == len(X0)
|
|
581
|
+
# assert len(Yrefs) >= 1
|
|
582
|
+
#
|
|
583
|
+
# with sys.revert_X(refs=Xrefs): #TODO: replace with context manager
|
|
584
|
+
# #initalize here
|
|
585
|
+
# refset_input(Xrefs, X0)
|
|
586
|
+
#
|
|
587
|
+
# rows = []
|
|
588
|
+
# dxs = []
|
|
589
|
+
# Fbase = refset_get(Yrefs)
|
|
590
|
+
# for k, v in Xrefs.items():
|
|
591
|
+
# x = v.value()#TODO: add context manager,sys
|
|
592
|
+
# if not isinstance(x, (float, int)):
|
|
593
|
+
# sys.warning(f"var: {k} is not numeric {x}, skpping")
|
|
594
|
+
# continue
|
|
595
|
+
#
|
|
596
|
+
# if abs(x) > pct:
|
|
597
|
+
# new_x = x * (1 + pct) + diff
|
|
598
|
+
# else:
|
|
599
|
+
# new_x = x * (1.1) + diff
|
|
600
|
+
# dx = new_x - x
|
|
601
|
+
# #print(dx, new_x, x)
|
|
602
|
+
# dxs.append(dx)
|
|
603
|
+
#
|
|
604
|
+
# v.set_value(new_x) # set delta
|
|
605
|
+
# sys.pre_execute()
|
|
606
|
+
#
|
|
607
|
+
# F_ = refset_get(Yrefs)
|
|
608
|
+
# Fmod = [(F_[k] - fb) / dx for k, fb in Fbase.items()]
|
|
609
|
+
#
|
|
610
|
+
# rows.append(Fmod)
|
|
611
|
+
# v.set_value(x) # reset value
|
|
612
|
+
#
|
|
613
|
+
# return np.column_stack(rows)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
# def solve_root(
|
|
617
|
+
# self, Xref, Yref, Xreset, vars, output=None, fail=True, **kw
|
|
618
|
+
# ):
|
|
619
|
+
# """
|
|
620
|
+
# Solve the root problem using the given vars.
|
|
621
|
+
#
|
|
622
|
+
# :param Xref: The reference input values.
|
|
623
|
+
# :param Yref: The reference output values.
|
|
624
|
+
# :param Xreset: The reset input values.
|
|
625
|
+
# :param vars: The list of var names.
|
|
626
|
+
# :param output: The output dictionary to store the results. (default: None)
|
|
627
|
+
# :param fail: Flag indicating whether to raise an exception if the solver doesn't converge. (default: True)
|
|
628
|
+
# :param kw: Additional keyword arguments.
|
|
629
|
+
# :return: The output dictionary containing the results.
|
|
630
|
+
# :rtype: dict
|
|
631
|
+
# """
|
|
632
|
+
# if output is None:
|
|
633
|
+
# output = {
|
|
634
|
+
# "Xstart": Xreset,
|
|
635
|
+
# "vars": vars,
|
|
636
|
+
# "Xans": None,
|
|
637
|
+
# "success": None,
|
|
638
|
+
# }
|
|
639
|
+
#
|
|
640
|
+
# assert len(Xref) == len(Yref), "Xref and Xreset must have the same length"
|
|
641
|
+
#
|
|
642
|
+
# self._ans = refroot_solve(self, Xref, Yref, ret_ans=True, **kw)
|
|
643
|
+
# output["ans"] = self._ans
|
|
644
|
+
# if self._ans.success:
|
|
645
|
+
# # Set Values
|
|
646
|
+
# Xa = {p: self._ans.x[i] for i, p in enumerate(vars)}
|
|
647
|
+
# output["Xans"] = Xa
|
|
648
|
+
# Ref.refset_input(Xref, Xa)
|
|
649
|
+
# self.pre_execute()
|
|
650
|
+
# self._converged = True
|
|
651
|
+
# output["success"] = True
|
|
652
|
+
# else:
|
|
653
|
+
# Ref.refset_input(Xref, Xreset)
|
|
654
|
+
# self.pre_execute()
|
|
655
|
+
# self._converged = False
|
|
656
|
+
# if fail:
|
|
657
|
+
# raise Exception(f"solver didnt converge: {self._ans}")
|
|
658
|
+
# output["success"] = False
|
|
659
|
+
#
|
|
660
|
+
# return output
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
#### TODO: solve equillibrium case first using root solver for tough problems
|
|
664
|
+
# # make anonymous function
|
|
665
|
+
# def f_lin_slv(comp, Xref: dict, Yref: dict, normalize=None,slv_info=None):
|
|
666
|
+
# vars = list(Xref.keys()) # static x_basis
|
|
667
|
+
# yvar = list(Yref.keys())
|
|
668
|
+
# def f(x): # anonymous function
|
|
669
|
+
# # set state
|
|
670
|
+
# for p, xi in zip(vars, x):
|
|
671
|
+
# Xref[p].set_value(xi)
|
|
672
|
+
# print(Xref,Yref)
|
|
673
|
+
# grp = (yvar, x, normalize)
|
|
674
|
+
# vals = [eval_ref(Yref[p],comp,slv_info) / n for p, x, n in zip(*grp)]
|
|
675
|
+
# return vals # n sized normal residual vector
|
|
676
|
+
#
|
|
677
|
+
# return f
|
|
678
|
+
#
|
|
679
|
+
# def refroot_solve(
|
|
680
|
+
# comp,
|
|
681
|
+
# Xref: dict,
|
|
682
|
+
# Yref: dict,
|
|
683
|
+
# Xo=None,
|
|
684
|
+
# normalize: np.array = None,
|
|
685
|
+
# reset=True,
|
|
686
|
+
# doset=True,
|
|
687
|
+
# fail=True,
|
|
688
|
+
# ret_ans=False,
|
|
689
|
+
# ffunc=f_lin_slv,
|
|
690
|
+
# **kw,
|
|
691
|
+
# ):
|
|
692
|
+
# """find the input X to ensure the difference between two dictionaries of refernces, x references are changed in place, y will be solved to zero, options ensue for cases where the solution is not ideal
|
|
693
|
+
#
|
|
694
|
+
# :param Xref: dictionary of references to the x values
|
|
695
|
+
# :param Yref: dictionary of references to the y values
|
|
696
|
+
# :param Xo: initial guess for the x values as a list against Xref order, or a dictionary
|
|
697
|
+
# :param normalize: a dictionary of values to normalize the x values by, list also ok as long as same length and order as Xref
|
|
698
|
+
# :param reset: if the solution fails, reset the x values to their original state, if true will reset the x values to their original state on failure overiding doset.
|
|
699
|
+
# :param doset: if the solution is successful, set the x values to the solution by default, otherwise follows reset, if not successful reset is checked first, then doset
|
|
700
|
+
# """
|
|
701
|
+
# vars = list(Xref.keys()) # static x_basis
|
|
702
|
+
# if normalize is None:
|
|
703
|
+
# normalize = np.ones(len(vars))
|
|
704
|
+
# elif isinstance(normalize, (list, tuple, np.ndarray)):
|
|
705
|
+
# assert len(normalize) == len(vars), "bad length normalize"
|
|
706
|
+
# elif isinstance(normalize, (dict)):
|
|
707
|
+
# normalize = np.array([normalize[p] for p in vars])
|
|
708
|
+
#
|
|
709
|
+
# # make objective function
|
|
710
|
+
# f = ffunc(comp, Xref, Yref, normalize)
|
|
711
|
+
#
|
|
712
|
+
# # get state
|
|
713
|
+
# if reset:
|
|
714
|
+
# x_pre = refset_get(Xref) # record before changing
|
|
715
|
+
#
|
|
716
|
+
# if Xo is None:
|
|
717
|
+
# Xo = [Xref[p].value() for p in vars]
|
|
718
|
+
#
|
|
719
|
+
# elif isinstance(Xo, dict):
|
|
720
|
+
# Xo = [Xo[p] for p in vars]
|
|
721
|
+
#
|
|
722
|
+
# # solve
|
|
723
|
+
# ans = sciopt.root(f, Xo, **kw)
|
|
724
|
+
#
|
|
725
|
+
# return process_ans(ans,vars,Xref,x_pre,doset,reset,fail,ret_ans)
|
|
726
|
+
#
|