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.
Files changed (69) hide show
  1. engforge/__init__.py +26 -0
  2. engforge/_testing_components.py +864 -0
  3. engforge/analysis.py +257 -0
  4. engforge/attr_dynamics.py +222 -0
  5. engforge/attr_plotting.py +811 -0
  6. engforge/attr_signals.py +116 -0
  7. engforge/attr_slots.py +211 -0
  8. engforge/attr_solver.py +443 -0
  9. engforge/attributes.py +326 -0
  10. engforge/common.py +76 -0
  11. engforge/component_collections.py +276 -0
  12. engforge/components.py +55 -0
  13. engforge/configuration.py +673 -0
  14. engforge/dataframe.py +185 -0
  15. engforge/datastores/__init__.py +116 -0
  16. engforge/datastores/data.py +519 -0
  17. engforge/datastores/gdocs.py +2650 -0
  18. engforge/datastores/reporting.py +935 -0
  19. engforge/datastores/secrets.py +549 -0
  20. engforge/dynamics.py +662 -0
  21. engforge/eng/__init__.py +0 -0
  22. engforge/eng/costs.py +1459 -0
  23. engforge/eng/fluid_material.py +279 -0
  24. engforge/eng/geometry.py +1060 -0
  25. engforge/eng/pipes.py +1211 -0
  26. engforge/eng/prediction.py +366 -0
  27. engforge/eng/solid_materials.py +445 -0
  28. engforge/eng/structure.py +2228 -0
  29. engforge/eng/structure_beams.py +821 -0
  30. engforge/eng/thermodynamics.py +271 -0
  31. engforge/engforge_attributes.py +380 -0
  32. engforge/env_var.py +214 -0
  33. engforge/locations.py +18 -0
  34. engforge/logging.py +290 -0
  35. engforge/patterns.py +449 -0
  36. engforge/problem_context.py +2251 -0
  37. engforge/properties.py +501 -0
  38. engforge/reporting.py +595 -0
  39. engforge/solveable.py +964 -0
  40. engforge/solver.py +316 -0
  41. engforge/solver_utils.py +726 -0
  42. engforge/system.py +143 -0
  43. engforge/system_reference.py +299 -0
  44. engforge/tabulation.py +288 -0
  45. engforge/test/__init__.py +2 -0
  46. engforge/test/_pre_test_structures.py +221 -0
  47. engforge/test/report_testing.py +158 -0
  48. engforge/test/test_airfilter.py +91 -0
  49. engforge/test/test_analysis.py +3 -0
  50. engforge/test/test_comp_iter.py +196 -0
  51. engforge/test/test_composition.py +139 -0
  52. engforge/test/test_costs.py +338 -0
  53. engforge/test/test_dynamics.py +109 -0
  54. engforge/test/test_dynamics_spaces.py +39 -0
  55. engforge/test/test_four_bar.py +97 -0
  56. engforge/test/test_modules.py +72 -0
  57. engforge/test/test_performance.py +93 -0
  58. engforge/test/test_pipes.py +49 -0
  59. engforge/test/test_problem.py +411 -0
  60. engforge/test/test_problem_deepscoping.py +68 -0
  61. engforge/test/test_slider_crank.py +51 -0
  62. engforge/test/test_solver.py +343 -0
  63. engforge/test/test_tabulation.py +233 -0
  64. engforge/typing.py +92 -0
  65. engforge-0.3.0.dist-info/METADATA +392 -0
  66. engforge-0.3.0.dist-info/RECORD +69 -0
  67. engforge-0.3.0.dist-info/WHEEL +5 -0
  68. engforge-0.3.0.dist-info/licenses/LICENSE +7 -0
  69. engforge-0.3.0.dist-info/top_level.txt +1 -0
@@ -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
+ #