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
engforge/system.py ADDED
@@ -0,0 +1,143 @@
1
+ """A System is a Configuration that orchestrates dataflow between components, as well as solving systems of equations in the presense of limits, as well as formatting results of each Component into reporting ready dataframe. System's solver behavior is inspired by NASA's numerical propulsion simulation system (NPSS) to solve systems of inequalities in complex systems.
2
+
3
+ Component or other subsystems are added to a System class with `Slots`:
4
+ ```
5
+ class CustomSystem(System):
6
+ slot_name = Slot.define(Component,ComponentSubclass,System)
7
+ ```
8
+
9
+ Component's data flow is established via `SignalS` that are defined:
10
+ ```
11
+ class CustomSystem(System):
12
+ signal_name = Signal.define(source_attr_or_property, target_attr)
13
+ control_signal = Signal.define(source_attr_or_property, target_attr,control_with='system.attr or slot.attr`)
14
+ ```
15
+ - source_attr: can reference a locally defined slot attribute (a la attr's fields) or any locally defined slot system property
16
+ - target_attr: must be a locally defined slot attribute or system attribute.
17
+
18
+ update description to include solver
19
+
20
+ A system calculates its state upon calling `System.run()`. This executes `pre_execute()` first which will directly update any attributes based on their `Signal` definition between `Slot` components. Once convergence is reached target_attr's are updated in `post_execute()` for cyclic SignalS.
21
+
22
+ If the system encounters a subsystem in its solver routine, the subsystem is evald() and its results used as static in that iteration,ie it isn't included in the system level dependents if cyclic references are found.
23
+
24
+
25
+ The solver uses the root or cobla scipy optimizer results on quick references to internal component references. Upon solving the system
26
+
27
+ SignalS can be limited with constrains via `min or max` values on `NumericProperty` which can be numeric values (int or float) or functions taking one argument of the component it is defined on. Additionally signals may take arguments of `min` or `max` which are numeric values or callbacks which take the system instance as an argument.
28
+ """
29
+
30
+ import attrs
31
+
32
+ from engforge.properties import *
33
+ from engforge.logging import LoggingMixin
34
+ from engforge.configuration import Configuration, forge
35
+ from engforge.components import SolveableInterface
36
+ from engforge.solver import SolverMixin
37
+ from engforge.attr_plotting import PlottingMixin
38
+ from engforge.dynamics import GlobalDynamics
39
+
40
+ import copy
41
+ import collections
42
+ import typing
43
+ import numpy
44
+
45
+
46
+ # make a module logger
47
+ class SystemsLog(LoggingMixin):
48
+ pass
49
+
50
+
51
+ log = SystemsLog()
52
+
53
+
54
+ # NOTE: solver must come before solvable interface since it overrides certain methods
55
+ @forge
56
+ class System(SolverMixin, SolveableInterface, PlottingMixin, GlobalDynamics):
57
+ """A system defines SlotS for Components, and data flow between them using SignalS
58
+
59
+ The system records all attribues to its subcomponents via system_references with scoped keys to references to set or get attributes, as well as observe system properties. These are cached upon first access in an instance.
60
+
61
+ The table is made up of these system references, allowing low overhead recording of systems with many variables.
62
+
63
+ When solving by default the run(revert=True) call will revert the system state to what it was before the system began.
64
+ """
65
+
66
+ # default to nothing
67
+ dynamic_input_vars: list = attrs.field(factory=list)
68
+ dynamic_state_vars: list = attrs.field(factory=list)
69
+ dynamic_output_vars: list = attrs.field(factory=list)
70
+
71
+ _anything_changed_ = True
72
+ _solver_override: bool = (
73
+ False # this comp will run with run_internal_systems when True, otherwise it resolves to global solver behavior, also prevents the solver from reaching into this system
74
+ )
75
+
76
+ # Properties!
77
+ @system_property
78
+ def converged(self) -> int:
79
+ if self.last_context is None:
80
+ return None
81
+ return self.last_context.converged
82
+
83
+ @property
84
+ def _converged(self):
85
+ return self.last_context.converged
86
+
87
+ @_converged.setter
88
+ def _converged(self, inpt):
89
+ self.last_context._converged = inpt
90
+
91
+ @system_property
92
+ def run_id(self) -> int:
93
+ if self.last_context is None:
94
+ return None
95
+ return self.last_context.problem_id
96
+
97
+ @classmethod
98
+ def subclasses(cls, out=None):
99
+ """
100
+ return all subclasses of components, including their subclasses
101
+ :param out: out is to pass when the middle of a recursive operation, do not use it!
102
+ """
103
+
104
+ # init the set by default
105
+ if out is None:
106
+ out = set()
107
+
108
+ for cls in cls.__subclasses__():
109
+ out.add(cls)
110
+ cls.subclasses(out)
111
+
112
+ return out
113
+
114
+ def clone(self):
115
+ """returns a clone of this system, often used to iterate the system without affecting the input values at the last convergence step."""
116
+ return copy.deepcopy(self)
117
+
118
+ @property
119
+ def identity(self):
120
+ if self.run_id:
121
+ return f"{self.name}_{self.run_id}"
122
+ else:
123
+ return f"{self.name}"
124
+
125
+ @property
126
+ def _anything_changed(self):
127
+ """looks at internal components as well as flag for anything chagned."""
128
+ if self._anything_changed_:
129
+ return True
130
+ elif any([c.anything_changed for k, c in self.comp_references().items()]):
131
+ return True
132
+ return False
133
+
134
+ @_anything_changed.setter
135
+ def _anything_changed(self, inpt):
136
+ """allows default functionality with new property system"""
137
+ self.mark_all_comps_changed(inpt)
138
+
139
+ def mark_all_comps_changed(self, inpt: bool):
140
+ """mark all components as changed, useful for forcing a re-run of the system, or for marking data as saved"""
141
+ self._anything_changed_ = inpt
142
+ for k, c in self.comp_references().items():
143
+ c._anything_changed = inpt
@@ -0,0 +1,299 @@
1
+ """Module to define the Ref class and utility methods for working with it.
2
+
3
+ Ref's are designed to create adhoc links between the systems & components of the system, and their properties. This is useful for creating optimization problems, and for creating dynamic links between the state and properties of the system. Care must be used on the component objects changing of state, the recommened procedure is to copy your base system to prevent hysterisis and other issues, then push/pop X values through the optimization methods and change state upon success
4
+
5
+ Ref.component can be a Component,System or dictionary for referencing via keyword. Ref.key can be a string or a callable, if callable the ref will be evaluated as the result of the callable (allow_set=False), this renders the component unused, and disables the ability to set the value of the reference.
6
+ """
7
+
8
+ import attrs
9
+ import inspect
10
+ import numpy as np
11
+ import collections
12
+ import scipy.optimize as sciopt
13
+ from contextlib import contextmanager
14
+ from engforge.properties import *
15
+ import copy
16
+ from difflib import get_close_matches
17
+
18
+
19
+ class RefLog(LoggingMixin):
20
+ pass
21
+
22
+
23
+ log = RefLog()
24
+
25
+
26
+ def refset_input(refs, delta_dict, chk=True, fail=True, warn=True, scope="ref"):
27
+ """change a set of refs with a dictionary of values. If chk is True k will be checked for membership in refs"""
28
+ keys = set(refs.keys())
29
+ for k, v in delta_dict.items():
30
+ if isinstance(k, Ref):
31
+ k.set_value(v)
32
+ continue
33
+
34
+ memb = k in refs
35
+ # if a match or not checking go ahead.
36
+ if not chk or memb:
37
+ refs[k].set_value(v)
38
+
39
+ # TODO: handle setting non-ref values such as dictionaries
40
+
41
+ elif fail and chk and not memb:
42
+ close = get_close_matches(k, keys)
43
+ raise KeyError(f"{scope}| key {k} not in refs. did you mean {close}?")
44
+ elif warn and chk and not memb:
45
+ close = get_close_matches(k, keys)
46
+ log.warning(f"{scope}| key {k} not in refs. did you mean {close}")
47
+
48
+
49
+ def refset_get(refs, *args, **kw):
50
+ out = {}
51
+ scope = kw.get("scope", "ref")
52
+ for k in refs:
53
+ try:
54
+ # print(k,refs[k])
55
+ out[k] = refs[k].value(refs[k].comp, **kw)
56
+ except Exception as e:
57
+ rf = refs[k]
58
+ log.error(e, f"{scope}| issue with ref: {rf}|{rf.key}|{rf.comp}")
59
+
60
+ return out
61
+
62
+
63
+ # def f_root(ResRef: collections.OrderedDict, norm: dict = None):
64
+ # residual = [v.value() / (norm[k] if norm else 1) for k, v in ResRef.items()]
65
+ # res = np.array(residual)
66
+ # return res
67
+ #
68
+ #
69
+ # def f_min(ResRef: collections.OrderedDict, norm: dict = None):
70
+ # res = [v.value() / (norm[k] if norm else 1) for k, v in ResRef.items()]
71
+ # ans = np.linalg.norm(res, 2)
72
+ #
73
+ # if ans < 1:
74
+ # return ans**0.5
75
+ # return ans
76
+
77
+
78
+ def eval_ref(canidate, *args, **kw):
79
+ # print(f'eval_ref {canidate,args,kw}')
80
+ if isinstance(canidate, Ref):
81
+ return canidate.value(*args, **kw)
82
+ elif callable(canidate):
83
+ o = canidate(*args, **kw)
84
+ return o
85
+ return canidate
86
+
87
+
88
+ def scale_val(val, mult=None, bias=None, power=None) -> float:
89
+ if any((mult, bias, power)):
90
+ if power is not None:
91
+ val = val**power
92
+ if mult is not None:
93
+ val = mult * val
94
+ if bias is not None:
95
+ val = bias + val
96
+ return val
97
+
98
+
99
+ def maybe_ref(can, astype=None, mult=None, bias=None, power=None, *args, **kw):
100
+ """returns the value of a ref if it is a ref, otherwise returns the value"""
101
+ # print(f'maybe {can,astype}')
102
+ # print(can,astype,mult,bias,power,args,kw)
103
+ if isinstance(can, Ref):
104
+ val = can.value(*args, **kw)
105
+ return scale_val(val, mult, bias, power)
106
+ elif astype and isinstance(can, astype):
107
+ ref = can.as_ref_dict() # TODO: optimize this
108
+ return scale_val(ref.value(*args, **kw), mult, bias, power)
109
+ return can
110
+
111
+
112
+ def maybe_attr_inst(can, astype=None):
113
+ """returns the ref if is one otherwise convert it, otherwise returns the value"""
114
+ if isinstance(can, Ref):
115
+ return can
116
+ elif astype and isinstance(can, astype):
117
+ return astype.backref.handle_instance(can)
118
+ return can
119
+
120
+
121
+ # Important State Preservation
122
+ # TODO: check for hidden X dependents / circular references ect.
123
+ # TODO: make global storage for Ref's based on the comp,key pair. This
124
+ class Ref:
125
+ """A way to create portable references to system's and their component's properties, ref can also take a key to a zero argument function which will be evald. This is useful for creating an adhoc optimization problems dynamically between variables. However use must be carefully controlled to avoid circular references, and understanding the safe use of the refset_input and refset_get methods (changing state on actual system).
126
+
127
+ A dictionary can be used as a component, and the key will be used to access the value in the dictionary.
128
+
129
+ The key can be a callable of (*args,**kw) only in which case Ref.value(*args,**kw) will take input, and the ref will be evaluated as the result of the callable (allow_set=False), this renders the component unused, and disables the ability to set the value of the reference.
130
+
131
+ The ability to set this ref with another on key input allows for creating a reference that is identical to another reference except for the component provided if it is not None.
132
+ """
133
+
134
+ __slots__ = [
135
+ "comp",
136
+ "key",
137
+ "use_call",
138
+ "use_dict",
139
+ "allow_set",
140
+ "eval_f",
141
+ "key_override",
142
+ "_value_eval",
143
+ "_log_func",
144
+ "hxd",
145
+ "_name",
146
+ "attr_type",
147
+ ]
148
+ comp: "TabulationMixin"
149
+ key: str
150
+ use_call: bool
151
+ use_dict: bool
152
+ allow_set: bool
153
+ eval_f: callable
154
+ key_override: bool
155
+ _value_eval: callable
156
+ _log_func: callable
157
+ attr_type: type
158
+
159
+ def __init__(self, comp, key, use_call=True, allow_set=True, eval_f=None):
160
+ self.set(comp, key, use_call, allow_set, eval_f)
161
+
162
+ def set(self, comp, key, use_call=True, allow_set=True, eval_f=None):
163
+ # key can be a ref, in which case this ref will be identical to the other ref except for the component provided if it is not None
164
+ from engforge.engforge_attributes import AttributedBaseMixin
165
+
166
+ if isinstance(key, Ref):
167
+ self.__dict__.update(key.__dict__)
168
+ if comp is not None:
169
+ self.comp = comp
170
+ return # a monkey patch
171
+
172
+ self.comp = comp
173
+ if isinstance(self.comp, dict):
174
+ self.use_dict = True
175
+ self._name = "dict"
176
+ else:
177
+ self.use_dict = False
178
+
179
+ self.key_override = False
180
+ if callable(key):
181
+ self.key_override = True
182
+ self.key = key # this should take have signature f(system,slv_info)
183
+ self.use_call = False
184
+ self.allow_set = False
185
+ self.eval_f = eval_f
186
+ self._name = "callable"
187
+ else:
188
+ self.key = key
189
+ self.use_call = use_call
190
+ self.allow_set = allow_set
191
+ self.eval_f = eval_f
192
+ if not self.use_dict:
193
+ self._name = self.comp.classname
194
+
195
+ self.attr_type = None
196
+ if self.allow_set and isinstance(self.comp, AttributedBaseMixin):
197
+ fieldz = self.comp.__class__.cls_all_attrs_fields()
198
+ atr_canidate = fieldz.get(self.key, None)
199
+ if isinstance(atr_canidate, attrs.Attribute):
200
+ self.attr_type = atr_canidate.type
201
+
202
+ if not hasattr(self, "_name"):
203
+ self._name = "NULL"
204
+
205
+ self.hxd = str(hex(id(self)))[-6:]
206
+
207
+ # TODO: update with change (pyee?)
208
+ self.setup_calls()
209
+
210
+ def setup_calls(self):
211
+ """caches anonymous functions for value and logging speedup"""
212
+ if (
213
+ self.comp
214
+ and isinstance(self.comp, LoggingMixin)
215
+ and self.comp.log_level <= 2
216
+ ):
217
+ self._log_func = lambda val: self.comp.msg(
218
+ f"REF[get] {str(self):<50} -> {val}"
219
+ )
220
+ else:
221
+ self._log_func = None
222
+
223
+ if self.key_override:
224
+ self._value_eval = lambda *a, **kw: self.key(*a, **kw)
225
+ else:
226
+ # do not cross reference vars!
227
+ # TODO: allo for comp/key changes with events
228
+ if self.key == "": # return the component
229
+ p = lambda *a, **kw: self.comp
230
+ elif self.use_dict:
231
+ p = lambda *a, **kw: self.comp.get(self.key)
232
+ elif self.key in self.comp.__dict__:
233
+ p = lambda *a, **kw: self.comp.__dict__[self.key]
234
+ else:
235
+ p = lambda *a, **kw: getattr(self.comp, self.key)
236
+
237
+ if self.eval_f:
238
+ g = lambda *a, **kw: self.eval_f(p(*a, **kw))
239
+ else:
240
+ g = p
241
+
242
+ self._value_eval = g
243
+
244
+ def copy(self, **changes):
245
+ """copy the reference, and make changes to the copy"""
246
+ if "key" not in changes:
247
+ changes["key"] = self.key
248
+ if "comp" not in changes:
249
+ changes["comp"] = self.comp
250
+ cy = copy.copy(self)
251
+ cy.set(**changes)
252
+ return cy
253
+
254
+ def value(self, *args, **kw):
255
+ if log.log_level <= 10:
256
+ try:
257
+ o = self._value_eval(*args, **kw)
258
+ if self._log_func:
259
+ self._log_func(o)
260
+ return o
261
+ except Exception as e:
262
+ log.error(e, f"issue with ref: {self}|{self.key}|{self.comp}")
263
+ else:
264
+ o = self._value_eval(*args, **kw)
265
+ if self._log_func:
266
+ self._log_func(o)
267
+ return o
268
+
269
+ def set_value(self, val):
270
+ if self.allow_set:
271
+ if self.attr_type not in (str, float, int, bool):
272
+ if self.comp and self.comp.log_level < 10:
273
+ self.comp.msg(f"REF[set] {self} <- {val}")
274
+ return setattr(self.comp, self.key, val)
275
+ elif self.value() != val: # this increases perf. by reducing writes
276
+ if self.comp and self.comp.log_level < 10:
277
+ self.comp.msg(f"REF[set] {self} <- {val}")
278
+ return setattr(self.comp, self.key, val)
279
+ else:
280
+ raise Exception(f"not allowed to set value on {self.key}")
281
+
282
+ @property
283
+ def full_key(self):
284
+ if self.key_override:
285
+ return f"{self._name}.{self.key.__name__}"
286
+ else:
287
+ return f"{self._name}.{self.key}"
288
+
289
+ def __str__(self) -> str:
290
+ if self.use_dict:
291
+ return f"REF[{self.hxd}][DICT.{self.key}]"
292
+ return f"REF[{self.hxd}][{self.full_key}]"
293
+
294
+ def __repr__(self) -> str:
295
+ return f"REF[{self.hxd}][{self.full_key}]"
296
+
297
+ # Utilty Methods
298
+ refset_get = refset_get
299
+ refset_input = refset_input