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/solveable.py ADDED
@@ -0,0 +1,964 @@
1
+ import attrs, attr
2
+ import uuid
3
+ import numpy
4
+ import numpy as np
5
+ import scipy.optimize as sciopt
6
+ from contextlib import contextmanager
7
+ import copy
8
+ import datetime
9
+ import typing
10
+
11
+
12
+ # from engforge.dynamics import DynamicsMixin
13
+ from engforge.attributes import AttributeInstance
14
+ from engforge.engforge_attributes import AttributedBaseMixin
15
+ from engforge.configuration import Configuration, forge
16
+ from engforge.properties import *
17
+ from engforge.system_reference import *
18
+ from engforge.system_reference import Ref
19
+ from engforge.solver_utils import *
20
+ from engforge.attr_dynamics import IntegratorInstance
21
+
22
+ import collections
23
+ import itertools
24
+
25
+ SOLVER_OPTIONS = ["root", "minimize"]
26
+
27
+
28
+ class SolvableLog(LoggingMixin):
29
+ pass
30
+
31
+
32
+ log = SolvableLog()
33
+
34
+
35
+ # Anonymous Update Funcs (solve scoping issue)
36
+ def _update_func(comp, eval_kw):
37
+ def updt(*args, **kw):
38
+ eval_kw.update(kw)
39
+ if log.log_level <= 12:
40
+ log.info(f"update| {comp.name} ") # =5)
41
+ return comp.update(comp.parent, *args, **eval_kw)
42
+
43
+ if log.log_level <= 12:
44
+ log.info(f"create method| {comp.name}| {eval_kw}")
45
+ updt.__name__ = f"{comp.name}_update"
46
+ return updt
47
+
48
+
49
+ def _post_update_func(comp, eval_kw):
50
+ def updt(*args, **kw):
51
+ eval_kw.update(kw)
52
+ return comp.post_update(comp.parent, *args, **eval_kw)
53
+
54
+ if log.log_level <= 12:
55
+ log.info(f"create post method| {comp.name}| {eval_kw}")
56
+ updt.__name__ = f"{comp.name}_post_update"
57
+ return updt
58
+
59
+
60
+ def _cost_update(comp):
61
+ from engforge.eng.costs import Economics, CostModel
62
+
63
+ if isinstance(comp, Economics):
64
+
65
+ def updt(*args, **kw):
66
+ if log.log_level <= 12:
67
+ log.info(f"update economics {comp.name} | {comp.term_length} ")
68
+ comp.system_properties_classdef(True)
69
+ comp.update(comp.parent, *args, **kw)
70
+
71
+ if log.log_level <= 12:
72
+ log.info(f"economics update cb {comp.name} | {comp.term_length} ")
73
+ updt.__name__ = f"{comp.name}_econ_update"
74
+ else:
75
+
76
+ def updt(*args, **kw):
77
+ if log.log_level <= 12:
78
+ log.info(f"update costs {comp.name} ") # =5)
79
+ comp.system_properties_classdef(True)
80
+ # comp.update(comp.parent, *args, **kw) #called as update without cm
81
+ return comp.update_dflt_costs()
82
+
83
+ if log.log_level <= 12:
84
+ log.info(f"cost update cb {comp.name} ")
85
+ updt.__name__ = f"{comp.name}_cost_update"
86
+
87
+ return updt
88
+
89
+
90
+ dflt_dynamics = {
91
+ "dynamics.state": {},
92
+ "dynamics.input": {},
93
+ "dynamics.rate": {},
94
+ "dynamics.output": {},
95
+ "dynamic_comps": {},
96
+ }
97
+ skipa_attr_names = (
98
+ "index",
99
+ "parent",
100
+ "dynamic_input_vars",
101
+ "dynamic_state_vars",
102
+ "dynamic_output_vars",
103
+ )
104
+
105
+
106
+ class SolveableMixin(AttributedBaseMixin): #'Configuration'
107
+ """commonality for components,systems that identifies subsystems and states for solving.
108
+
109
+ This class defines the update structure of components and systems, and the storage of internal references to the system and its components. It also provides a method to iterate over the internal components and their references.
110
+
111
+ Importantly it defines the references to the system and its components, and the ability to set the system state from a dictionary of values across multiple objects. It also provides a method to iterate over the internal components and their references. There are several helper functions to these ends.
112
+ """
113
+
114
+ # TODO: add parent state storage
115
+ parent: "Configuration"
116
+
117
+ _prv_internal_references: dict
118
+ _prv_internal_components: dict
119
+ _prv_internal_systems: dict
120
+ _prv_internal_tabs: dict
121
+ _prv_system_references: dict
122
+
123
+ # Update Flow
124
+ # TODO: pass the problem vs the parent component, then locate this component in the problem and update any references
125
+ def update(self, parent, *args, **kwargs):
126
+ """Kwargs comes from eval_kw in solver"""
127
+ if log.log_level <= 12:
128
+ log.info(f"void updating {self.__class__.__name__}.{self}")
129
+
130
+ def post_update(self, parent, *args, **kwargs):
131
+ """Kwargs comes from eval_kw in solver"""
132
+ if log.log_level <= 12:
133
+ log.info(f"void post-updating {self.__class__.__name__}.{self}")
134
+
135
+ def collect_update_refs(self, eval_kw=None, ignore=None):
136
+ """checks all methods and creates ref's to execute them later"""
137
+ from engforge.eng.costs import CostModel, Economics
138
+
139
+ updt_refs = {}
140
+ from engforge.components import Component
141
+ from engforge.component_collections import ComponentIter
142
+
143
+ # Ignore
144
+ if ignore is None:
145
+ ignore = set()
146
+ elif self in ignore:
147
+ return
148
+
149
+ key = "top"
150
+ if self.__class__.update != SolveableMixin.update:
151
+ ref = Ref(self, _update_func(self, eval_kw if eval_kw else {}))
152
+ updt_refs[key] = ref
153
+
154
+ if isinstance(self, (CostModel, Economics)):
155
+ ref = Ref(self, _cost_update(self))
156
+ updt_refs[key + "._cost_model_"] = ref
157
+
158
+ for key, comp in self.internal_configurations(False).items():
159
+ # for key,lvl,comp in self.go_through_configurations(check_config=False):
160
+ if ignore is not None and comp in ignore:
161
+ continue
162
+
163
+ if not isinstance(comp, SolveableMixin):
164
+ continue
165
+
166
+ # provide add eval_kw
167
+ if eval_kw and key in eval_kw:
168
+ eval_kw_comp = eval_kw[key]
169
+ else:
170
+ eval_kw_comp = {}
171
+ ekw = eval_kw_comp
172
+
173
+ # Add if its a unique update method (not the passthrough)
174
+ if isinstance(comp, (CostModel, Economics)):
175
+ ref = Ref(comp, _cost_update(comp))
176
+ updt_refs[key + "._cost_model_"] = ref
177
+
178
+ if comp.__class__.update != SolveableMixin.update:
179
+ ref = Ref(comp, _update_func(comp, ekw))
180
+ updt_refs[key] = ref
181
+
182
+ # Cost Models
183
+
184
+ ignore.add(self)
185
+ return updt_refs
186
+
187
+ def collect_post_update_refs(self, eval_kw=None, ignore=None):
188
+ """checks all methods and creates ref's to execute them later"""
189
+ updt_refs = {}
190
+ from engforge.components import Component
191
+ from engforge.component_collections import ComponentIter
192
+
193
+ # Ignore
194
+ if ignore is None:
195
+ ignore = set()
196
+ elif self in ignore:
197
+ return
198
+
199
+ if self.__class__.post_update != SolveableMixin.post_update:
200
+ ref = Ref(self, _post_update_func(self, eval_kw if eval_kw else {}))
201
+ updt_refs["top"] = ref
202
+
203
+ for key, comp in self.internal_configurations(False).items():
204
+ # for key,lvl,comp in self.go_through_configurations(check_config=False):
205
+ if ignore is not None and comp in ignore:
206
+ continue
207
+
208
+ if not isinstance(comp, SolveableMixin):
209
+ continue
210
+
211
+ # provide add eval_kw
212
+ if eval_kw and key in eval_kw:
213
+ eval_kw_comp = eval_kw[key]
214
+ else:
215
+ eval_kw_comp = {}
216
+
217
+ ekw = eval_kw_comp
218
+
219
+ # Add if its a unique update method (not the passthrough)
220
+ if comp.__class__.post_update != SolveableMixin.post_update:
221
+ ref = Ref(comp, _post_update_func(comp, ekw))
222
+ updt_refs[key] = ref
223
+
224
+ ignore.add(self)
225
+ return updt_refs
226
+
227
+ # internals caching
228
+ # instance attributes
229
+
230
+ # TODO: move all system / property & identification to the problem_context or new system_identificaiton class (problem/base)
231
+ @instance_cached
232
+ def signals(self):
233
+ """this is just a record of signals from this solvable. dont use this to get the signals, use high level signals strategy #TODO: add signals strategy"""
234
+ return {k: getattr(self, k) for k in self.signals_attributes()}
235
+
236
+ @instance_cached
237
+ def solvers(self):
238
+ """this is just a record of any solvable attribute. dont use this to get the attribute, use another strategy"""
239
+ return {k: getattr(self, k) for k in self.solvers_attributes()}
240
+
241
+ @instance_cached(allow_set=True)
242
+ def transients(self):
243
+ """this is just a record of any transient attribute. dont use this to get the transient, use another strategy"""
244
+ return {k: getattr(self, k) for k in self.transients_attributes()}
245
+
246
+ def internal_components(self, recache=False) -> dict:
247
+ """get all the internal components"""
248
+ if recache == False and hasattr(self, "_prv_internal_components"):
249
+ return self._prv_internal_components
250
+ from engforge.components import Component
251
+
252
+ o = {k: getattr(self, k) for k in self.slots_attributes()}
253
+ o = {k: v for k, v in o.items() if isinstance(v, Component)}
254
+ self._prv_internal_components = o
255
+ return o
256
+
257
+ def internal_systems(self, recache=False) -> dict:
258
+ """get all the internal components"""
259
+ if recache == False and hasattr(self, "_prv_internal_systems"):
260
+ return self._prv_internal_systems
261
+ from engforge.system import System
262
+
263
+ o = {k: getattr(self, k) for k in self.slots_attributes()}
264
+ o = {k: v for k, v in o.items() if isinstance(v, System)}
265
+ self._prv_internal_systems = o
266
+ return o
267
+
268
+ def internal_tabulations(self, recache=False) -> dict:
269
+ """get all the internal tabulations"""
270
+ from engforge.tabulation import TabulationMixin
271
+
272
+ if recache == False and hasattr(self, "_prv_internal_tabs"):
273
+ return self._prv_internal_tabs
274
+
275
+ o = {k: getattr(self, k) for k in self.slots_attributes()}
276
+ o = {k: v for k, v in o.items() if isinstance(v, TabulationMixin)}
277
+ self._prv_internal_tabs = o
278
+ return o
279
+
280
+ # recursive references
281
+ @instance_cached
282
+ def iterable_components(self) -> dict:
283
+ """Finds ComponentIter internal_components that are not 'wide'"""
284
+ from engforge.component_collections import ComponentIter
285
+
286
+ return {
287
+ k: v
288
+ for k, v in self.internal_components().items()
289
+ if isinstance(v, ComponentIter) and not v.wide
290
+ }
291
+
292
+ def internal_references(self, recache=False, numeric_only=False) -> dict:
293
+ """get references to all internal attributes and values, only saving when complete cache info is requested (vs numeric only)"""
294
+ if not numeric_only and (
295
+ recache == False and hasattr(self, "_prv_internal_references")
296
+ ):
297
+ return self._prv_internal_references
298
+
299
+ out = self._gather_references(numeric_only=numeric_only)
300
+ if not numeric_only:
301
+ self._prv_internal_references = out
302
+ return out
303
+
304
+ def _gather_references(self, numeric_only=False) -> dict:
305
+ out = {}
306
+ out["attributes"] = at = {}
307
+ out["properties"] = pr = {}
308
+
309
+ for key in self.system_properties_classdef():
310
+ pr[key] = Ref(self, key, True, False)
311
+
312
+ if numeric_only:
313
+ for key in self.numeric_fields():
314
+ at[key] = Ref(self, key, False, True)
315
+ else:
316
+ for key in self.input_fields((list, tuple, dict)):
317
+ at[key] = Ref(self, key, False, True)
318
+
319
+ return out
320
+
321
+ def _iterate_components(self):
322
+ """sets the current component for each product combination of iterable_components"""
323
+
324
+ components = self.iterable_components
325
+
326
+ if not components:
327
+ yield # enter once
328
+ else:
329
+
330
+ def _gen(gen, compkey):
331
+ for itemkey, item in gen:
332
+ yield compkey, itemkey
333
+
334
+ iter_vals = {
335
+ cn: _gen(comp._item_gen(), cn) for cn, comp in components.items()
336
+ }
337
+
338
+ for out in itertools.product(*list(iter_vals.values())):
339
+ for ck, ikey in out:
340
+ # TODO: progress bar or print location
341
+ components[ck].current_item = ikey
342
+ yield out
343
+
344
+ # finally reset the data!
345
+ for ck, comp in components.items():
346
+ comp.reset()
347
+
348
+ def comp_references(self, ignore_none_comp=True, **kw):
349
+ """A cached set of recursive references to any slot component
350
+ #FIXME: by instance recache on iterative component change or other signals
351
+ """
352
+ out = {}
353
+ for key, lvl, comp in self.go_through_configurations(parent_level=1, **kw):
354
+ if ignore_none_comp and not isinstance(comp, SolveableMixin):
355
+ self.warning(f"ignoring {key} {lvl}|{comp}")
356
+ continue
357
+ out[key] = comp
358
+ return out
359
+
360
+ # Run & Input
361
+ def _iterate_input_matrix(
362
+ self,
363
+ method,
364
+ cb=None,
365
+ sequence: list = None,
366
+ eval_kw: dict = None,
367
+ sys_kw: dict = None,
368
+ force_solve=False,
369
+ return_results=False,
370
+ method_kw: dict = None,
371
+ print_kw: dict = None,
372
+ **kwargs,
373
+ ):
374
+ """applies a permutation of input vars for vars. runs the system instance by applying input to the system and its slot-components, ensuring that the targeted attributes actualy exist.
375
+
376
+ :param revert: will reset the values of X that were recorded at the beginning of the run.
377
+ :param cb: a callback function that takes the system as an argument cb(system)
378
+ :param sequence: a list of dictionaries that should be run in order per the outer-product of kwargs
379
+ :param eval_kw: a dictionary of keyword arguments to pass to the eval function of each component by their name and a set of keyword args. Use this to set values in the component that are not inputs to the system. No iteration occurs upon these values, they are static and irrevertable
380
+ :param sys_kw: a dictionary of keyword arguments to pass to the eval function of each system by their name and a set of keyword args. Use this to set values in the component that are not inputs to the system. No iteration occurs upon these values, they are static and irrevertable
381
+ :param print_kw: a dictionary of keyword arguments to pass to the print_all_info function of the current context
382
+
383
+ :param kwargs: inputs are run on a product basis asusming they correspond to actual scoped vars (system.var or system.slot.var)
384
+
385
+
386
+ :returns: system or list of systems. If transient a set of systems that have been run with permutations of the input, otherwise a single system with all permutations input run
387
+ """
388
+ from engforge.system import System
389
+ from engforge.problem_context import ProblemExec
390
+
391
+ self.debug(f"running [Solver].{method} {self.identity} with input {kwargs}")
392
+ assert hasattr(ProblemExec.class_cache, "session"), "must be active context!"
393
+ # create iterable null for sequence
394
+ if sequence is None or not sequence:
395
+ sequence = [{}]
396
+
397
+ if method_kw is None:
398
+ method_kw = {}
399
+
400
+ # Create Keys List
401
+ sequence_keys = set()
402
+ for seq in sequence:
403
+ sequence_keys = sequence_keys.union(set(seq.keys()))
404
+
405
+ # RUN when not solved, or anything changed, or arguments or if forced
406
+ if force_solve or not self.solved or self.anything_changed or kwargs:
407
+ _input = self.parse_run_kwargs(**kwargs)
408
+
409
+ output = {}
410
+ inputs = {}
411
+ result = {"output": output, "input_sets": inputs}
412
+
413
+ # Pre Run Callback
414
+ self.pre_run_callback(eval_kw=eval_kw, sys_kw=sys_kw, **kwargs)
415
+
416
+ # Premute the input as per SS or Transient Logic
417
+ ingrp = list(_input.values())
418
+ keys = list(_input.keys())
419
+
420
+ # Iterate over components (they are assigned to the system by call)
421
+ for itercomp in self._iterate_components():
422
+ # Iterate over inputs
423
+ for vars in itertools.product(*ingrp):
424
+ # Set the reference aliases
425
+ cur = {k: v for k, v in zip(keys, vars)}
426
+
427
+ # Iterate over Sequence (or run once)
428
+ for seq in sequence:
429
+ # apply sequence values
430
+ icur = cur.copy()
431
+ # apply sequence values
432
+ if seq:
433
+ icur.update(**seq)
434
+
435
+ # Run The Method with inputs provisioned
436
+ out = method(icur, eval_kw, sys_kw, cb=cb, **method_kw)
437
+
438
+ if (
439
+ print_kw
440
+ and hasattr(self, "last_context")
441
+ and self.last_context
442
+ ):
443
+ self.last_context.print_all_info(**print_kw)
444
+
445
+ if return_results:
446
+ # store the output
447
+ output[max(output) + 1 if output else 0] = out
448
+ # store the input
449
+ inputs[max(inputs) + 1 if inputs else 0] = icur
450
+
451
+ # nice
452
+ # TODO: wrap this with context manager
453
+ self._solved = True
454
+
455
+ # Pre Run Callback with current state
456
+ self.post_run_callback(eval_kw=eval_kw, sys_kw=sys_kw, **kwargs)
457
+
458
+ if return_results:
459
+ return result
460
+
461
+ elif not self.anything_changed:
462
+ self.warning(f"nothing changed, not running {self.identity}")
463
+
464
+ return
465
+ elif self.solved:
466
+ raise Exception("Analysis Already Solved")
467
+
468
+ # IO Functions
469
+ def parse_simulation_input(self, **kwargs):
470
+ """parses the simulation input
471
+
472
+ :param dt: timestep in s, required for transients
473
+ :param endtime: when to end the simulation
474
+ """
475
+ # timestep
476
+ if "dt" not in kwargs:
477
+ raise Exception("transients require `dt` to run")
478
+ # f"transients require timestep input `dt`"
479
+ dt = float(kwargs.pop("dt"))
480
+
481
+ # endtime
482
+ if "endtime" not in kwargs:
483
+ raise Exception("transients require `endtime` to run")
484
+
485
+ # add data
486
+ _trans_opts = {"dt": None, "endtime": None}
487
+ _trans_opts["dt"] = dt
488
+
489
+ # f"transients require `endtime` to specify "
490
+ _trans_opts["endtime"] = endtime = float(kwargs.pop("endtime"))
491
+ _trans_opts["Nrun"] = max(int(endtime / dt) + 1, 1)
492
+
493
+ # TODO: expose integrator choices
494
+ # TODO: add delay and signal & feedback options
495
+
496
+ return _trans_opts
497
+
498
+ def parse_run_kwargs(self, **kwargs):
499
+ """ensures correct input for simulation.
500
+ :returns: first set of input for initalization, and all input dictionaries as tuple.
501
+
502
+ """
503
+ # Validate OTher Arguments By Parameter Or Comp-Recursive
504
+ var_args = {k: v for k, v in kwargs.items() if "." not in k}
505
+ comp_args = {k: v for k, v in kwargs.items() if "." in k}
506
+
507
+ # check vars
508
+ inpossible = set.union(set(self.input_fields()), set(self.slots_attributes()))
509
+ argdiff = set(var_args).difference(inpossible)
510
+ assert not argdiff, f"bad input {argdiff}"
511
+
512
+ # check components
513
+ comps = set([k.split(".")[0] for k in comp_args.keys()])
514
+ compdiff = comps.difference(set(self.slots_attributes()))
515
+ assert not compdiff, f"bad slot references {compdiff}"
516
+
517
+ _input = {}
518
+ test = lambda v, add: isinstance(v, (int, float, str, *add)) or v is None
519
+
520
+ # vars input
521
+ for k, v in kwargs.items():
522
+ # If a slot check the type is applicable
523
+ subslot = self.check_ref_slot_type(k)
524
+ if subslot is not None:
525
+ # log.debug(f'found subslot {k}: {subslot}')
526
+ addty = subslot
527
+ else:
528
+ addty = []
529
+
530
+ # Ensure Its a List
531
+ if isinstance(v, numpy.ndarray):
532
+ v = v.tolist()
533
+
534
+ if not isinstance(v, list):
535
+ assert test(v, addty), f"bad values {k}:{v}"
536
+ v = [v]
537
+ else:
538
+ assert all([test(vi, addty) for vi in v]), f"bad values: {k}:{v}"
539
+
540
+ if k not in _input:
541
+ _input[k] = v
542
+ else:
543
+ _input[k].extend(v)
544
+
545
+ return _input
546
+
547
+ # REFERENCE FUNCTIONS
548
+ # Location Funcitons
549
+ @classmethod
550
+ def locate(cls, key, fail=True) -> type:
551
+ """:returns: the class or attribute by key if its in this system class or a subcomponent. If nothing is found raise an error"""
552
+ # Nested
553
+ log.msg(f"locating {cls.__name__} | key: {key}")
554
+ val = None
555
+
556
+ if "." in key:
557
+ args = key.split(".")
558
+ comp, sub = args[0], ".".join(args[1:])
559
+ assert comp in cls.slots_attributes(), f"invalid {comp} in {key}"
560
+ comp_cls = cls.slots_attributes(attr_type=True)[comp].accepted[0]
561
+ val = comp_cls.locate(sub, fail=True)
562
+
563
+ elif key in cls.input_fields():
564
+ val = cls.input_fields()[key]
565
+
566
+ elif key in cls.system_properties_classdef():
567
+ val = cls.system_properties_classdef()[key]
568
+
569
+ # Fail on comand but otherwise return val
570
+ if val is None:
571
+ if fail:
572
+ raise Exception(f"key {key} not found")
573
+ return None
574
+ return val
575
+
576
+ def locate_ref(self, key, fail=True, **kw):
577
+ """Pass a string of a relative var or property on this system or pass a callable to get a reference to a function. If the key has a `.` in it the comp the lowest level component will be returned, unless a callable is passed in which case this component will be used or the `comp` passed in the kw will be used.
578
+ :param key: the key to locate, or a callable to be used as a reference
579
+ :param comp: the component to use if a callable is passed
580
+ :returns: the instance assigned to this system. If the key has a `.` in it the comp the lowest level component will be returned
581
+ """
582
+
583
+ log.msg(f"locating {self.identity} | key: {key}")
584
+ val = None
585
+
586
+ # Handle callable key override
587
+ if callable(key):
588
+ comp = kw.pop("comp", self)
589
+ func = copy.copy(key)
590
+ return Ref(comp, func, **kw)
591
+ else:
592
+ assert "comp" not in kw, f"comp kwarg not allowed with string key {key}"
593
+
594
+ if "." in key:
595
+ args = key.split(".")
596
+ comp, sub = args[0], ".".join(args[1:])
597
+ assert comp in self.slots_attributes(), f"invalid {comp} in {key}"
598
+ # comp_cls = cls.slots_attributes()[comp].type.accepted[0]
599
+ comp = getattr(self, comp)
600
+ if "." not in key:
601
+ return Ref(comp, sub, **kw)
602
+ if comp:
603
+ return comp.locate_ref(sub, fail=fail, **kw)
604
+ else:
605
+ return None
606
+
607
+ elif key in self.input_fields():
608
+ # val= cls.input_fields()[key]
609
+ return Ref(self, key, **kw)
610
+
611
+ elif key in self.system_properties_classdef():
612
+ # val= cls.system_properties_classdef()[key]
613
+ return Ref(self, key, **kw)
614
+
615
+ elif key in self.internal_configurations() or key in self.slots_attributes():
616
+ return Ref(self, key, **kw)
617
+
618
+ # Fail on comand but otherwise return val
619
+ if val is None:
620
+ if fail:
621
+ raise Exception(f"key {key} not found")
622
+ return None
623
+ return val
624
+
625
+ # Reference Caching
626
+ # TODO: move to problem context
627
+ def system_references(self, recache=False, numeric_only=False, **kw):
628
+ """gather a list of references to attributes and"""
629
+ if (
630
+ not numeric_only
631
+ and not kw
632
+ and (recache == False and hasattr(self, "_prv_system_references"))
633
+ ):
634
+ return self._prv_system_references
635
+
636
+ # TODO: system references are really important and nesting them together complicates the refresh process. Each component should be able to refresh itself and its children on set_state, as well as alert parents to change. Ideally the `Ref` objects could stay the same and no `recache` would need to occur. This would be a huge performance boost and fix a lot of the issues with the current system.
637
+
638
+ out = self.internal_references(recache, numeric_only=numeric_only)
639
+ tatr = out["attributes"]
640
+ tprp = out["properties"]
641
+ comp_dict = {"": self}
642
+ out["components"] = comp_set_ref = {}
643
+
644
+ # component iternals
645
+ for key, comp in self.comp_references(**kw).items():
646
+ if comp is None:
647
+ continue
648
+ sout = comp.internal_references(recache, numeric_only=numeric_only)
649
+ satr = sout["attributes"]
650
+ sprp = sout["properties"]
651
+
652
+ # find the parent component
653
+ key_segs = key.split(".")
654
+ if len(key_segs) == 1:
655
+ parent = ""
656
+ else:
657
+ parent = ".".join(key_segs[:-1])
658
+
659
+ # parent refs for assigning a component
660
+ comp_dict[key] = comp
661
+ if parent in comp_dict:
662
+ attr_name = key.split(".")[-1]
663
+ comp_set_ref[key] = Ref(comp_dict[parent], attr_name, False, True)
664
+
665
+ # Fill in
666
+ for k, v in satr.items():
667
+ if k in skipa_attr_names:
668
+ continue
669
+ tatr[f"{key}.{k}"] = v
670
+
671
+ for k, v in sprp.items():
672
+ tprp[f"{key}.{k}"] = v
673
+
674
+ if not numeric_only and not kw:
675
+ self._prv_system_references = out
676
+ return out
677
+
678
+ def collect_comp_refs(self, conf: "Configuration" = None, **kw):
679
+ """collects all the references for the system grouped by component"""
680
+ if conf is None:
681
+ conf = self
682
+ comp_dict = {}
683
+ attr_dict = {}
684
+ cls_dict = {}
685
+ out = {"comps": comp_dict, "attrs": attr_dict, "type": cls_dict}
686
+ for key, lvl, conf in self.go_through_configurations(**kw):
687
+ comp_dict[key] = conf
688
+ attr_dict[key] = conf.collect_inst_attributes()
689
+
690
+ return out
691
+
692
+ def collect_solver_refs(
693
+ self,
694
+ conf: "Configuration" = None,
695
+ check_atr_f=None,
696
+ check_kw=None,
697
+ check_dynamics=True,
698
+ **kw,
699
+ ):
700
+ """collects all the references for the system grouped by function and prepended with the system key"""
701
+ from engforge.attributes import ATTR_BASE
702
+ from engforge.engforge_attributes import AttributedBaseMixin
703
+
704
+ confobj = conf
705
+ if confobj is None:
706
+ confobj = self
707
+
708
+ comp_dict = {}
709
+ attr_dict = {}
710
+ cls_dict = {}
711
+ skipped = {}
712
+
713
+ out = {
714
+ "comps": comp_dict,
715
+ "attrs": attr_dict,
716
+ "type": cls_dict,
717
+ "skipped": skipped,
718
+ }
719
+
720
+ # Go through all components
721
+ for key, lvl, conf in confobj.go_through_configurations(**kw):
722
+ if conf is None:
723
+ continue
724
+
725
+ if hasattr(conf, "_solver_override") and conf._solver_override:
726
+ continue
727
+
728
+ # Get attributes & attribute instances
729
+ atrs = conf.collect_inst_attributes()
730
+ rawattr = conf.collect_inst_attributes(handle_inst=False)
731
+ key = f"{key}." if key else "" # you need a dot if there's a key
732
+ comp_dict[key] = conf
733
+ # Gather attribute heirarchy and make key.var the dictionary entry
734
+ for atype, aval in atrs.items():
735
+ ck_type = rawattr[atype]
736
+ if atype not in cls_dict:
737
+ cls_dict[atype] = {}
738
+
739
+ if isinstance(aval, dict) and aval:
740
+ for k, pre, val in ATTR_BASE.unpack_atrs(aval, atype):
741
+ # No Room For Components (SLOTS feature)
742
+ if isinstance(val, (AttributedBaseMixin, ATTR_BASE)):
743
+ if conf.log_level <= 5:
744
+ conf.debug(f"skipping comp attr {val}")
745
+ continue
746
+
747
+ if val is None:
748
+ continue
749
+
750
+ if conf.log_level <= 5:
751
+ conf.msg(f"")
752
+ conf.msg(f"got val: {k} {pre} {val}")
753
+
754
+ slv_type = None
755
+ pre_var = pre.split(".")[-1]
756
+ if hasattr(conf, pre_var):
757
+ _var = getattr(conf, pre_var)
758
+ if isinstance(_var, AttributeInstance):
759
+ slv_type = _var
760
+ conf.msg(f"slv type: {conf.classname}.{pre_var} -> {_var}")
761
+
762
+ val_type = ck_type[pre_var]
763
+
764
+ # Otherwise assign the data from last var and the compoenent name
765
+ var_name = pre_var # prer alias
766
+ if slv_type:
767
+ var_name = slv_type.get_alias(pre)
768
+
769
+ # Keep reference to the original type and name
770
+ scope_name = f"{key}{var_name}"
771
+ cls_dict[atype][scope_name] = val_type
772
+
773
+ if conf.log_level <= 5:
774
+ conf.msg(f"rec: {var_name} {k} {pre} {val} {slv_type}")
775
+
776
+ # Check to skip this item
777
+ # keep references even if null
778
+ pre = f"{atype}.{k}" # pre switch
779
+ if pre not in attr_dict:
780
+ attr_dict[pre] = {}
781
+
782
+ if isinstance(val, Ref) and val.allow_set:
783
+ # its a var, skip it if it's already been skipped
784
+ current_skipped = [set(v) for v in skipped.values()]
785
+ if current_skipped and val.key in set.union(
786
+ *current_skipped
787
+ ):
788
+ continue
789
+
790
+ # Perform the check
791
+ if check_atr_f and not check_atr_f(
792
+ pre, scope_name, val_type, check_kw
793
+ ):
794
+ if conf.log_level <= 5:
795
+ conf.msg(f"chk skip {scope_name} {k} {pre} {val}")
796
+ if pre not in skipped:
797
+ skipped[pre] = []
798
+
799
+ if isinstance(val, Ref) and val.allow_set:
800
+ # its a var
801
+ skipped[pre].append(f"{key}{val.key}")
802
+ else:
803
+ # not objective or settable, must be a obj/cond
804
+ skipped[pre].append(scope_name)
805
+ continue
806
+
807
+ # if the value is a dictionary, unpack it with comp key
808
+ if val:
809
+ attr_dict[pre].update({scope_name: val})
810
+ else:
811
+ if attr_dict[pre]:
812
+ continue # keep it!
813
+ else:
814
+ attr_dict[pre] = {} # reset it
815
+
816
+ elif atype not in attr_dict or not attr_dict[atype]:
817
+ # print(f'unpacking {atype} {aval}')
818
+ attr_dict[atype] = {}
819
+
820
+ # Dynamic Variables Add, following the skipped items
821
+ if check_dynamics:
822
+ dyn_refs = self.collect_dynamic_refs(confobj).copy()
823
+ # house keeping to organize special returns
824
+
825
+ # TODO: generalize this with a function to update the attr dict serach result when changing components
826
+ out["dynamic_comps"] = dyn_comp = dyn_refs.pop("dynamic_comps", {})
827
+
828
+ # Check the dynamics for the system
829
+ if check_atr_f or any([v for v in skipped.values()]):
830
+ skipd = set()
831
+ # check each group of dynamics
832
+ for pre, refs in dyn_refs.items():
833
+ if pre not in skipped:
834
+ skipped[pre] = []
835
+
836
+ if pre not in attr_dict:
837
+ attr_dict[pre] = {} # initalize dynamic group
838
+
839
+ # eval each ref for inclusion
840
+ for var, ref in refs.items():
841
+ key_segs = var.split(".")
842
+ key = "" if len(key_segs) == 1 else ".".join(key_segs[:-1])
843
+ scoped_name = f"{var}"
844
+ conf = dyn_comp.get(key)
845
+
846
+ is_ref = isinstance(ref, Ref)
847
+ if not is_ref:
848
+ conf.info(f"not ref {scoped_name}")
849
+ continue
850
+
851
+ if is_ref and not ref.allow_set:
852
+ # adding unsettables (likely rates)
853
+ attr_dict[pre].update(**{var: ref})
854
+ self.debug(f"set escape: {scoped_name} {ref}")
855
+ continue
856
+
857
+ val_type = ref.comp
858
+ if (
859
+ check_atr_f
860
+ and isinstance(var, str)
861
+ and check_atr_f(pre, var, val_type, check_kw)
862
+ ):
863
+ conf.msg(f"dynvar add {pre,var,val_type,skipd}")
864
+ attr_dict[pre].update(**{var: ref})
865
+ else:
866
+ conf.msg(f"dynvar endskip {pre,var,val_type,skipd}")
867
+ if check_atr_f: # then it didn't checkout
868
+ skipped[pre].append(scope_name)
869
+
870
+ else:
871
+ # There's no checks to be done, just add these
872
+ attr_dict.update(**dyn_refs)
873
+ else:
874
+ cpy = dflt_dynamics.copy()
875
+ cpy.pop("dynamic_comps", {})
876
+ attr_dict.update(cpy)
877
+
878
+ return out
879
+
880
+ # Dynamics info refs
881
+ def collect_dynamic_refs(self, conf: "Configuration" = None, **kw) -> dict:
882
+ """collects the dynamics of the systems
883
+ 1. Time.integrate
884
+ 2. Dynamic Instances
885
+ """
886
+ from engforge.dynamics import DynamicsMixin
887
+
888
+ if conf is None:
889
+ conf = self
890
+ dynamics = {k: {} for k in dflt_dynamics}
891
+
892
+ for key, lvl, conf in conf.go_through_configurations(**kw):
893
+ # FIXME: add a check for the dynamics mixin, that isn't hacky
894
+ # BUG: importing dynamicsmixin resolves as different class in different modules, weird
895
+ # if "dynamicsmixin" not in str(conf.__class__.mro()).lower():
896
+ # continue
897
+ if not isinstance(conf, DynamicsMixin) or not conf.is_dynamic:
898
+ continue
899
+
900
+ sval = f"{key}." if key else ""
901
+ scope = lambda d: {f"{sval}{k}": v for k, v in d.items()}
902
+ dynamics["dynamics.state"].update(scope(conf.Xt_ref))
903
+ dynamics["dynamics.input"].update(scope(conf.Ut_ref))
904
+ dynamics["dynamics.output"].update(scope(conf.Yt_ref))
905
+ dynamics["dynamics.rate"].update(scope(conf.dXtdt_ref))
906
+ dynamics["dynamic_comps"][key] = conf
907
+
908
+ return dynamics
909
+
910
+ def get_system_input_refs(
911
+ self,
912
+ strings=False,
913
+ numeric=True,
914
+ misc=False,
915
+ all=False,
916
+ boolean=False,
917
+ **kw,
918
+ ) -> dict:
919
+ """
920
+ Get the references to system input based on the specified criteria.
921
+
922
+ :param strings: Include system properties of string type.
923
+ :param numeric: Include system properties of numeric type (float, int).
924
+ :param misc: Include system properties of miscellaneous type.
925
+ :param all: Include all system properties regardless of type.
926
+ :param boolean: Include system properties of boolean type.
927
+ :param kw: Additional keyword arguments passed to recursive config loop
928
+ :return: A dictionary of system property references.
929
+ :rtype: dict
930
+ """
931
+ from engforge.tabulation import SKIP_REF
932
+
933
+ refs = {}
934
+ for ckey, lvl, comp in self.go_through_configurations(**kw):
935
+ if comp is None:
936
+ continue
937
+ for p, atr in comp.input_fields().items():
938
+ if p in SKIP_REF and not all:
939
+ continue
940
+ if all:
941
+ refs[(f"{ckey}." if ckey else "") + p] = Ref(comp, p, False, True)
942
+ continue
943
+ elif atr.type:
944
+ ty = atr.type
945
+ if issubclass(ty, (bool)):
946
+ if not boolean:
947
+ continue # prevent catch at int type
948
+ refs[(f"{ckey}." if ckey else "") + p] = Ref(
949
+ comp, p, True, False
950
+ )
951
+ elif issubclass(ty, (float, int)) and numeric:
952
+ refs[(f"{ckey}." if ckey else "") + p] = Ref(
953
+ comp, p, False, True
954
+ )
955
+ elif issubclass(ty, (str)) and strings:
956
+ refs[(f"{ckey}." if ckey else "") + p] = Ref(
957
+ comp, p, False, True
958
+ )
959
+ elif misc:
960
+ refs[(f"{ckey}." if ckey else "") + p] = Ref(
961
+ comp, p, False, True
962
+ )
963
+
964
+ return refs