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,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