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,2251 @@
1
+ """The ProblemExec provides a uniform set of options for managing the state of the system and its solvables, establishing the selection of combos or de/active attributes to Solvables. Once once created any further entracnces to ProblemExec will return the same instance until finally the last exit is called.
2
+
3
+ The ProblemExec class allows entrance to a its context to the same instance until finally the last exit is called. The first entrance to the context will create the instance, each subsequent entrance will return the same instance. The ProblemExec arguments are set the first time and remove keyword arguments from the input dictionary (passed as a dict ie stateful) to subsequent methods.
4
+ This isn't technically a singleton pattern, but it does provide a similar interface. Instead mutliple problem instances will be clones of the first instance, with the optional difference of input/output/event criteria. The first instance will be returned by each context entry, so for that reason it may always appear to have same instance, however each instance is unique in a recusive setting so it may record its own state and be reverted to its own state as per the options defined.
5
+
6
+ #TODO: allow update of kwargs on re-entrance
7
+
8
+ ## Example:
9
+ .. code-block:: python
10
+
11
+ #Application code (arguments passed in kw)
12
+ with ProblemExec(sys,combos='default',slv_vars'*',**kw) as pe:
13
+ pe._sys_refs #get the references and compiled problem
14
+ for i in range(10):
15
+ pe.solve_min(pe.Xref,pe.Yref,**other_args)
16
+ pe.set_checkpoint() #save the state of the system
17
+ pe.save_data()
18
+
19
+
20
+ #Solver Module (can use without knowledge of the runtime system)
21
+ with ProblemExec(sys,{},Xnew=Xnext,ctx_fail_new=True) as pe:
22
+ #do revertable math on the state of the system without concern for the state of the system
23
+ ...
24
+
25
+ # Combos Selection
26
+ By default no arguments run will select all active items with combo="default". The `combos` argument can be used to select a specific set of combos, a outer select. From this set, the `ign_combos` and `only_combos` arguments can be used to ignore or select specific combos based on exclusion or inclusion respectively.
27
+
28
+ # Parameter Name Selection
29
+ The `slv_vars` argument can be used to select a specific set of solvables. From this set, the `ign_vars` and `only_vars` arguments can be used to ignore or select specific solvables based on exclusion or inclusion respectively. The `add_vars` argument can be used to add a specific set of solvables to the solver.
30
+
31
+ # Active Mode Handiling
32
+ The `only_active` argument can be used to select only active items. The `activate` and `deactivate` arguments can be used to activate or deactivate specific solvables.
33
+
34
+ `add_obj` can be used to add an objective to the solver.
35
+
36
+ # Root Parameter Determination:
37
+ Any session's may access root session parameters defined in the `root_parameters` dictionary like `converged`, or `data` can be accessed by calling `session.<parm>` this eventually calls `root._<parm>` via the __getattr__ method.
38
+
39
+ # Exit Mode Handling
40
+
41
+ The ProblemExec supports the following exit mode handling vars:
42
+
43
+ - `fail_revert`: Whether to raise an error if no solvables are selected. Default is True.
44
+ - `revert_last`: Whether to revert the last change. Default is True.
45
+ - `revert_every`: Whether to revert every change. Default is True.
46
+ - `exit_on_failure`: Whether to exit on first failure. Default is True.
47
+
48
+ These vars control the behavior of the ProblemExec when an error occurs or when no solvables are selected.
49
+
50
+ """
51
+
52
+ # TODO: define the
53
+
54
+ from engforge.logging import LoggingMixin
55
+ from engforge.system_reference import Ref
56
+ from engforge.dataframe import DataframeMixin, pandas
57
+ from engforge.solver_utils import *
58
+ from engforge.env_var import EnvVariable
59
+ import weakref
60
+
61
+ from scipy.integrate import solve_ivp
62
+ from collections import OrderedDict
63
+ import numpy as np
64
+ import pandas as pd
65
+ import datetime
66
+
67
+
68
+ class ProbLog(LoggingMixin):
69
+ pass
70
+
71
+
72
+ log = ProbLog()
73
+
74
+ import uuid
75
+
76
+ # TODO: implement add_vars feature, ie it creates a solver variable, or activates one if it doesn't exist from in system.heirarchy.format
77
+ # TODO: define the dataframe / data storage feature
78
+
79
+ min_opt = {"finite_diff_rel_step": 0.25, "maxiter": 10000}
80
+ min_kw_dflt = {
81
+ "tol": 1e-10,
82
+ "method": "SLSQP",
83
+ "jac": "cs",
84
+ "hess": "cs",
85
+ "options": min_opt,
86
+ }
87
+
88
+
89
+ # The KW Defaults for Solver via kw_dict
90
+ # IMPORTANT:!!! these group parameter names by behavior, they are as important as the following class, add/remove variables with caution
91
+ # these choices affect how solver-items are selected and added to the solver
92
+ slv_dflt_options = dict(
93
+ combos="default",
94
+ ign_combos=None,
95
+ only_combos=None,
96
+ add_obj=True,
97
+ slv_vars="*",
98
+ add_vars=None,
99
+ ign_vars=None,
100
+ only_vars=None,
101
+ only_active=True,
102
+ activate=None,
103
+ deactivate=None,
104
+ dxdt=None,
105
+ weights=None,
106
+ both_match=True,
107
+ obj=None,
108
+ )
109
+ # KW Defaults for the local context (what saves state for contexts / reverts ect)
110
+ dflt_parse_kw = dict(
111
+ fail_revert=True,
112
+ revert_last=True,
113
+ revert_every=True,
114
+ exit_on_failure=True,
115
+ pre_exec=True,
116
+ post_exec=True,
117
+ opt_fail=True,
118
+ level_name="top",
119
+ post_callback=None,
120
+ success_thresh=10,
121
+ copy_system=False,
122
+ run_solver=False,
123
+ min_kw=None,
124
+ save_mode="all",
125
+ x_start=None,
126
+ save_on_exit=False,
127
+ enter_refresh=True,
128
+ )
129
+ # can be found on session._<parm> or session.<parm>
130
+ root_defined = dict(
131
+ last_time=0,
132
+ time=0,
133
+ dt=0,
134
+ update_refs=None,
135
+ post_update_refs=None,
136
+ sys_refs=None,
137
+ slv_kw=None,
138
+ minimizer_kw=None,
139
+ data=None,
140
+ weights=None,
141
+ dxdt=None,
142
+ run_start=None,
143
+ run_end=None,
144
+ run_time=None,
145
+ all_refs=None,
146
+ num_refs=None,
147
+ converged=None,
148
+ comp_changed=False,
149
+ )
150
+ save_modes = ["vars", "nums", "all", "prob"]
151
+ transfer_kw = ["system", "_dxdt"]
152
+
153
+ root_possible = list(root_defined.keys()) + list("_" + k for k in root_defined.keys())
154
+
155
+ # TODO: output options extend_dataframe=True,return_dataframe=True,condensed_dataframe=True,return_system=True,return_problem=True,return_df=True,return_data=True
156
+ # TODO: connect save_data() output to _data table.
157
+ # TODO: move dataframe mixin here, system should return a dataframe, and the problem should be able to save data to the dataframe, call this class Problem(). With default behavior it could seem like a normal dataframe is returned on problem.return(*state,exit,revert...)
158
+
159
+
160
+ # Special exception classes handled in exit
161
+ class IllegalArgument(Exception):
162
+ """an exception to exit the problem context as specified"""
163
+
164
+ pass
165
+
166
+
167
+ class ProblemExit(Exception):
168
+ """an exception to exit the problem context, without error"""
169
+
170
+ revert: bool
171
+ prob: "ProblemExec"
172
+
173
+ def __init__(self, prob: "ProblemExec", revert: bool = None):
174
+ self.revert = revert
175
+ self.prob = prob
176
+
177
+ def __str__(self) -> str:
178
+ return f"ProblemExit[{self.prob}|rvt={self.revert}]"
179
+
180
+
181
+ class ProblemExitAtLevel(ProblemExit):
182
+ """an exception to exit the problem context, without error"""
183
+
184
+ level: str
185
+
186
+ def __init__(self, prob: "ProblemExec", level: str, revert=None):
187
+ assert level is not None, "level must be defined"
188
+ assert isinstance(level, str), "level must be a string"
189
+ self.prob = prob
190
+ self.level = level.lower()
191
+ self.revert = revert
192
+
193
+ def __str__(self) -> str:
194
+ return f"ProblemExit[{self.prob}|lvl={self.level}|rvt={self.revert}]"
195
+
196
+
197
+ # TODO: develop subproblem strategy (multiple root cache problem in class cache)
198
+ # TODO: determine when components are updated, and refresh the system references accordingly.
199
+ # TODO: Map attributes/properties by component key and then autofix refs! (this is a big one), no refresh required. Min work
200
+ # TODO: component graph with pyee anything changed system to make a lazy observer system.
201
+ # TODO: plot levels to manage report output (with pdf graph publishing)
202
+ class ProblemExec:
203
+ """
204
+ Represents the execution context for a problem in the system. The ProblemExec class provides a uniform set of options for managing the state of the system and its solvables, establishing the selection of combos or de/active attributes to Solvables. Once once created any further entracnces to ProblemExec will return the same instance until finally the last exit is called.
205
+
206
+ ## params:
207
+ - _problem_id: uuid for subproblems, or True for top level, None means uninitialized
208
+
209
+ """
210
+
211
+ full_update = True # TODO: cant justify setting this to false for performance gains. accuracy comes first. see event based update
212
+
213
+ # TODO: convert this to a system based cache where there is a unique problem for each system instance. On subprobem copy a system and add o dictionary.
214
+ class_cache = None # ProblemExec is assigned below
215
+
216
+ # this class, wide, dont redefine it pls
217
+ problems_dict = weakref.WeakValueDictionary()
218
+
219
+ system: "System"
220
+ session: "ProblemExec"
221
+ session_id = None
222
+
223
+ # problem state / per level
224
+ problem_id = None
225
+ entered: bool = False
226
+ exited: bool = False
227
+
228
+ # solution control (point to singleton/subproblem via magic getattr)
229
+ _last_time: float = 0
230
+ _time: float = 0
231
+ _dt: float = 0
232
+ _update_refs: dict
233
+ _post_update_refs: dict
234
+ _sys_refs: dict
235
+ _slv_kw: dict
236
+ _minimizer_kw: dict
237
+ _data: list
238
+ _weights: dict
239
+ x_start: dict
240
+ _dxdt: float
241
+ _run_start: float
242
+ _run_end: float
243
+ _run_time: float
244
+ _converged: bool
245
+
246
+ # Interior Context Options
247
+ enter_refresh: bool = (
248
+ True # TODO: allow this off (or lower impact) with event system
249
+ )
250
+ save_on_exit: bool = False
251
+ save_mode: str = "all"
252
+ level_name: str = None # target this context with the level name
253
+ level_number: int = 0 # TODO: keep track of level on the global context
254
+ pre_exec: bool = True
255
+ post_exec: bool = True
256
+ fail_revert: bool = True
257
+ revert_last: bool = True
258
+ revert_every: bool = True
259
+ exit_on_failure: bool = True
260
+ opt_fail: bool = True
261
+ raise_on_unknown: bool = True
262
+ copy_system: bool = False
263
+ success_thresh = (
264
+ 1e6 # if system has `success_thresh` it will be assigned to the context
265
+ )
266
+ post_callback: callable = (
267
+ None # callback that will be called on the system each time it is reverted, it should take args(system,current_problem_exec)
268
+ )
269
+ run_solver: bool = (
270
+ False # for transient #i would love this to be=true, but there's just too much possible variation in application to make it so without some kind of control / continuity strategy. Dynamics are natural responses anyways, so solver use should be an advanced case for now (MPC/Filtering/ect later)
271
+ )
272
+
273
+ def __getattr__(self, name):
274
+ """
275
+ This is a special method that is called when an attribute is not found in the usual places, like when interior contexts (anything not the root (session_id=True)) are created that dont have the top level's attributes. some attributes will look to the parent session
276
+ """
277
+
278
+ # interior context lookup (when in active context, ie session exists)
279
+ if hasattr(self.class_cache, "session") and name in root_possible:
280
+ # revert to the parent session
281
+ if self.session_id != True and name.startswith("_"):
282
+ # self.info(f'get parent private {name}')
283
+ return getattr(self.class_cache.session, name)
284
+
285
+ elif name in root_defined:
286
+ # self.info(f'get parent public {name}')
287
+ return getattr(self.class_cache.session, "_" + name)
288
+
289
+ if name in root_defined: # public interface
290
+ # self.info(f'get root fallback {name}')
291
+ return self.__getattribute__("_" + name)
292
+
293
+ # Default behaviour
294
+ return self.__getattribute__(name)
295
+
296
+ def __init__(self, system, kw_dict=None, Xnew=None, ctx_fail_new=False, **opts):
297
+ """
298
+ Initializes the ProblemExec.
299
+
300
+ #TODO: exit system should abide by update / signals options
301
+
302
+ #TODO: provide data storage options for dataframe / table storage history/ record keeping (ss vs transient data)
303
+
304
+ #TODO: create an option to copy the system and run operations on it, and options for applying the state from the optimized copy to the original system
305
+
306
+ :param system: The system to be executed.
307
+ :param Xnew: The new state of the system to set at the start o the problem context, and will revert after the problem exits, optional
308
+ :param ctx_fail_new: Whether to raise an error if no execution context is available, use in utility methods ect. Default is False.
309
+ :param kw_dict: A keyword argument dictionary to be parsed for solver options, and removed from the outer context. Changes are made to this dictionary, so they are removed automatically from the outer context, and thus no longer passed to interior vars.
310
+ :param dxdt: The dynamics integration method. Default is None meaning that dynamic vars are not considered for minimization unless otherwise specified. Steady State can be specified by dxdt=0 all dynamic vars are considered as solver variables, with the constraint that their rate of change is zero. If a dictionary is passed then the dynamic vars are considered as solver variables, with the constraint that their rate of change is equal to the value in the dictionary, and all other unspecified rates are zero (steady).
311
+
312
+ #### Solver Selection Options
313
+ :param combos: The selection of combos. Default is '*' (select all).
314
+ :param ign_combos: The combos to be ignored.
315
+ :param only_combos: The combos to be selected.
316
+ :param add_obj: Whether to add an objective to the solver. Default is True.
317
+ :param slv_vars: The selection of solvables. Default is '*' (select all).
318
+ :param add_vars: The solvables to be added to the solver.
319
+ :param ign_vars: The solvables to be ignored.
320
+ :param only_vars: The solvables to be selected.
321
+ :param only_active: Whether to select only active items. Default is True.
322
+ :param activate: The solvables to be activated.
323
+ :param deactivate: The solvables to be deactivated.
324
+ :param fail_revert: Whether to raise an error if no solvables are selected. Default is True.
325
+ :param revert_last: Whether to revert the last change. Default is True.
326
+ :param revert_every: Whether to revert every change. Default is True.
327
+ :param exit_on_failure: Whether to exit on failure, or continue on. Default is True.
328
+ """
329
+
330
+ self.dynamics_updated = False # it is known
331
+
332
+ if kw_dict is None:
333
+ # kw_dict is stateful so you can mix system & context args together, and ensure context args are removed. in the case this is unused, we'll create an empty dict to avoid errors
334
+ kw_dict = {}
335
+
336
+ # storage optoins
337
+ if opts.pop("persist", False) or kw_dict.pop("persist", False):
338
+ self.persist_contexts()
339
+
340
+ # temp solver storage #TODO
341
+ # self.solver_hist = expiringdict.ExpiringDict(100, 60)
342
+
343
+ if self.log_level < 5:
344
+ if hasattr(self.class_cache, "session"):
345
+ self.debug(
346
+ f"subctx{self.level_number}| keywords: {kw_dict} and misc: {opts}"
347
+ )
348
+ else:
349
+ self.debug(f"context| keywords: {kw_dict} and misc: {opts}")
350
+
351
+ # special cases for parsing
352
+ # parse the options to change behavior of the context
353
+ level_name = None
354
+ if opts and "level_name" in opts:
355
+ level_name = opts.pop("level_name").lower()
356
+ if kw_dict and "level_name" in kw_dict:
357
+ level_name = kw_dict.pop("level_name").lower()
358
+
359
+ # solver min-args wrt defaults
360
+ min_kw = None
361
+ if opts and "min_kw" in opts:
362
+ min_kw = kw_dict.pop("min_kw")
363
+ if kw_dict and "min_kw" in kw_dict:
364
+ min_kw = kw_dict.pop("min_kw")
365
+
366
+ mkw = min_kw_dflt.copy()
367
+ if min_kw is None:
368
+ min_kw = mkw
369
+ else:
370
+ mkw.update(min_kw)
371
+
372
+ self._minimizer_kw = mkw
373
+
374
+ # Merge kwdict(stateful) and opts (current level)
375
+ # solver vars should be static for a problem and subcontexts, however the default vars can change. Subproblems allow for the solver vars to be changed on its creation.
376
+ opt_in, opt_out = {}, {}
377
+ if opts:
378
+ # these go to the context instance optoins
379
+ opt_in = {k: v for k, v in opts.items() if k in dflt_parse_kw}
380
+ # these go to system establishment
381
+ opt_out = {k: v for k, v in opts.items() if k not in opt_in}
382
+
383
+ if kw_dict is None:
384
+ kw_dict = {}
385
+
386
+ else:
387
+ # these go to the context instance optoins
388
+ kw_in = {k: v for k, v in kw_dict.items() if k in dflt_parse_kw}
389
+ opt_in.update(kw_in)
390
+ kw_out = {k: v for k, v in kw_dict.items() if k not in opt_in}
391
+ # these go to system establishment
392
+ opt_out.update(kw_out)
393
+ # remove problem options from dict (otherwise passed along to system!)
394
+ for k in kw_in:
395
+ kw_dict.pop(k)
396
+
397
+ # Define the handiling of rate integrals
398
+ if "dxdt" in opts and opts["dxdt"] is not None:
399
+ dxdt = opts.pop("dxdt")
400
+ if "dxdt" in kw_dict and kw_dict["dxdt"] is not None:
401
+ dxdt = kw_dict.pop("dxdt")
402
+ else:
403
+ dxdt = None # by default dont consider dynamics
404
+
405
+ if dxdt is not None and dxdt is not False:
406
+ if dxdt == 0:
407
+ pass
408
+ elif dxdt is True:
409
+ pass
410
+ elif isinstance(dxdt, dict): # dxdt is a dictionary
411
+ # provide a set of values or function to have the solver solve for
412
+ pass
413
+ else:
414
+ raise IllegalArgument(f"bad dxdt value {dxdt}")
415
+
416
+ # Check if there is an existing problem session, else create it
417
+ if hasattr(self.class_cache, "session"):
418
+ # mirror the state of session (exactly)
419
+ copy_vals = {
420
+ k: v
421
+ for k, v in self.class_cache.session.__dict__.items()
422
+ if k in dflt_parse_kw or k in transfer_kw
423
+ }
424
+ self.__dict__.update(copy_vals)
425
+ self._problem_id = int(uuid.uuid4())
426
+ self.problems_dict[self._problem_id] = self # carry that weight
427
+ self.session_id = int(uuid.uuid4())
428
+
429
+ self.class_cache.session._prob_levels[self.level_name] = self
430
+ # error if the system is different (it shouldn't be!)
431
+ if self.system is not system:
432
+ # TODO: subproblems allow different systems, but the top level should be the same
433
+ # idea - use (system,pid) as key for problems_dict, (system,True) would be root problem. This breaks checking for `class_cache.session` though one could gather that from the root problem key`
434
+ raise IllegalArgument(
435
+ f"somethings wrong! change of comp! {self.system} -> {system}"
436
+ )
437
+
438
+ # modify things from the input
439
+ if level_name is None:
440
+ # your new id
441
+ self.level_name = "ctx_" + str(int(self._problem_id))[0:15]
442
+ else:
443
+ self.level_name = level_name
444
+
445
+ if opt_in:
446
+ self.__dict__.update(opt_in) # level options ASAP
447
+ self.temp_state = Xnew # input state exception to this
448
+ if log.log_level < 5:
449
+ self.msg(f"setting execution context with {opt_in}| {opt_out}")
450
+
451
+ # each state request to be reverted, then we need to store the state of each execution context overriding the outer context x_start
452
+ self.set_checkpoint()
453
+
454
+ elif ctx_fail_new:
455
+ raise IllegalArgument(f"no execution context available")
456
+
457
+ else:
458
+ # add the prob options to the context and establish system
459
+ self.__dict__.update(opt_in)
460
+ self._problem_id = True # this is the top level
461
+ self.problems_dict[self._problem_id] = self # carry that weight
462
+ self._prob_levels = {}
463
+ self._converged = None
464
+ self._dxdt = dxdt
465
+ self.reset_data()
466
+
467
+ # supply the level name default as top if not set
468
+ if level_name is None:
469
+ self.level_name = "top"
470
+ else:
471
+ self.level_name = level_name
472
+
473
+ self.temp_state = Xnew
474
+ self.establish_system(system, kw_dict=kw_dict, **opt_out)
475
+
476
+ # Finally we record where we started!
477
+ self.set_checkpoint()
478
+
479
+ if log.log_level < 10:
480
+ self.info(f"new execution context for {system}| {opts} | {self._slv_kw}")
481
+
482
+ elif log.log_level <= 3:
483
+ self.msg(f"new execution context for {system}| {self._slv_kw}")
484
+
485
+ def reset_data(self):
486
+ """reset the data storage"""
487
+ # the data storage!!
488
+ # TODO: add buffer type, or disk cache
489
+ self._data = {} # index:row_dict
490
+ self._index = 0 # works for time or index
491
+
492
+ def establish_system(self, system, kw_dict, **kwargs):
493
+ """caches the system references, and parses the system arguments"""
494
+ from engforge.solver import SolverMixin
495
+ from engforge.system import System
496
+
497
+ if self.copy_system:
498
+ system = system.copy_config_at_state()
499
+
500
+ # place me here after system has been modified
501
+ self.system = system
502
+
503
+ # pass args without creating singleton (yet)
504
+ self.session_id = int(uuid.uuid4())
505
+ self._run_start = datetime.datetime.now()
506
+ self.name = system.name + "-" + str(self.session_id)[:8]
507
+
508
+ if log.log_level < 5:
509
+ self.info(f"establish {system}| {kw_dict} {kwargs}")
510
+
511
+ assert isinstance(
512
+ self.system, SolverMixin
513
+ ), "only solveable interfaces are supported for execution context"
514
+ self.system._last_context = self # set the last context to this one
515
+
516
+ if hasattr(self.system, "success_thresh") and isinstance(
517
+ self.system.success_thresh, (int, float)
518
+ ):
519
+ self.success_thresh = self.system.success_thresh
520
+ # Extract solver vars and set them on this object, they will be distributed to any new further execution context's via monkey patch above
521
+ in_kw = self.get_extra_kws(kwargs, slv_dflt_options, use_defaults=False)
522
+ self._slv_kw = self.get_extra_kws(kw_dict, slv_dflt_options, rmv=True)
523
+ self._slv_kw.update(in_kw) # update with input!
524
+
525
+ self.refresh_references()
526
+
527
+ # Get solver weights
528
+ self._weights = self._slv_kw.get("weights", None)
529
+
530
+ # Grab inputs and set to system
531
+ for k, v in dflt_parse_kw.items():
532
+ if k in self._slv_kw:
533
+ setattr(self, k, self._slv_kw[k])
534
+
535
+ if log.log_level < 5:
536
+ self.msg(f"established sys context: {self} {self._slv_kw}")
537
+
538
+ @property
539
+ def sesh(self):
540
+ """caches the property for the session"""
541
+ if hasattr(self, "inst_sesh"):
542
+ return self.inst_sesh
543
+ sesh = self.get_sesh()
544
+ return sesh
545
+
546
+ def get_sesh(self, sesh=None):
547
+ """get the session"""
548
+ out = sesh
549
+ if not sesh:
550
+ if hasattr(self.class_cache, "session"):
551
+ out = self.class_cache.session
552
+ elif self._problem_id == True:
553
+ out = self
554
+ if out:
555
+ self.inst_sesh = out
556
+ return out
557
+
558
+ # @classmethod
559
+ # def cls_get_sesh(cls, sesh=None):
560
+ # """get the session"""
561
+ # out = sesh
562
+ # if not sesh:
563
+ # if hasattr(cls.class_cache, "session"):
564
+ # out = self.class_cache.session
565
+ # elif self._problem_id == True:
566
+ # out = self
567
+ # if out:
568
+ # self.inst_sesh = out
569
+ # return out
570
+
571
+ # @property
572
+ # def index(self):
573
+ # sesh = self.get_sesh()
574
+ # if not sesh._data:
575
+ # return 0
576
+ # else:
577
+ # return max(list(sesh._data.keys()))
578
+ #
579
+ # @property
580
+ # def last_index(self):
581
+ # sesh = self.index
582
+ # if sesh == 0:
583
+ # return None
584
+ # else:
585
+ # return sesh - 1
586
+ #
587
+ # @property
588
+ # def next_index(self):
589
+ # sesh = self.index
590
+ # if sesh == 0:
591
+ # return 1
592
+ # else:
593
+ # return sesh + 1
594
+
595
+ # Update Methods
596
+ def refresh_references(self, sesh=None):
597
+ """refresh the system references"""
598
+
599
+ if sesh is None:
600
+ sesh = self.sesh
601
+
602
+ if self.log_level < 5:
603
+ self.warning(f"refreshing system references")
604
+
605
+ sesh.full_refresh(sesh=sesh)
606
+ sesh.min_refresh(sesh=sesh)
607
+
608
+ def update_methods(self, sesh=None):
609
+ # Get the update method refs
610
+ sesh = sesh if sesh is not None else self.sesh
611
+
612
+ sesh._update_refs = sesh.system.collect_update_refs()
613
+ # TODO: find subsystems that are not-subsolvers and execute them
614
+ sesh._post_update_refs = sesh.system.collect_post_update_refs()
615
+
616
+ sesh.update_dynamics(sesh=sesh)
617
+
618
+ def update_dynamics(self, sesh=None):
619
+ # apply changes to the dynamics models
620
+ sesh = sesh if sesh is not None else self.sesh
621
+ if self.dynamic_comps:
622
+ self.info(f"update dynamics")
623
+ self.system.setup_global_dynamics()
624
+
625
+ def full_refresh(self, sesh=None):
626
+ """a more time consuming but throughout refresh of the system"""
627
+ if self.log_level < 5:
628
+ self.info(f"full refresh")
629
+
630
+ check_dynamics = sesh.check_dynamics
631
+ sesh._num_refs = sesh.system.system_references(
632
+ numeric_only=True,
633
+ none_ok=True,
634
+ only_inst=False,
635
+ ignore_none_comp=False,
636
+ recache=True,
637
+ )
638
+ sesh._sys_refs = sesh.system.solver_vars(
639
+ check_dynamics=check_dynamics,
640
+ addable=sesh._num_refs,
641
+ **sesh._slv_kw,
642
+ )
643
+ sesh.update_methods(sesh=sesh)
644
+
645
+ def min_refresh(self, sesh=None):
646
+ """what things need to be refreshed per execution, this is important whenever items are replaced"""
647
+ # TODO: replace this function with an event based responsiblity model.
648
+ sesh = sesh if sesh is not None else self.sesh
649
+
650
+ if self.log_level < 5:
651
+ self.info(f"min refresh")
652
+
653
+ if sesh.full_update:
654
+ # TODO: dont require this
655
+ sesh.full_refresh(sesh=sesh)
656
+
657
+ # final ref's after update
658
+ # after updates
659
+ sesh._all_refs = sesh.system.system_references(
660
+ recache=True,
661
+ check_config=False,
662
+ ignore_none_comp=False,
663
+ none_ok=True,
664
+ only_inst=False,
665
+ )
666
+ # sesh._attr_sys_key_map = sesh.attribute_sys_key_map
667
+
668
+ # Problem Variable Definitions
669
+ sesh.Xref = sesh.all_problem_vars
670
+ sesh.Yref = sesh.sys_solver_objectives()
671
+
672
+ cons = {} # TODO: parse additional constraints
673
+ sesh.constraints = sesh.sys_solver_constraints(cons)
674
+
675
+ def print_all_info(self, keys: str = None, comps: str = None):
676
+ """
677
+ Print all the information of each component's dictionary.
678
+ Parameters:
679
+ key_sch (str, optional): A pattern to match dictionary keys. Only keys matching this pattern will be included in the output.
680
+ comps (list, optional): A list of component sys names to filter. Only information of these components will be printed.
681
+ Returns: None (except stdout :)
682
+ """
683
+ from pprint import pprint
684
+
685
+ keys = keys.split(",")
686
+ comps = (comps + ",").split(",") # always top level
687
+ print(f"CONTEXT: {self}")
688
+
689
+ mtch = lambda key, ptrns: any(
690
+ [fnmatch.fnmatch(key.lower(), ptn.lower()) for ptn in ptrns]
691
+ )
692
+
693
+ # check your comps
694
+ itrs = self.all_comps.copy()
695
+ itrs[""] = Ref(self.system, "", True, False)
696
+
697
+ # check your comps
698
+ for cn, comp in itrs.items():
699
+ if comps is not None and not mtch(cn, comps):
700
+ continue
701
+
702
+ dct = comp.value().as_dict
703
+ if keys: # filter keys
704
+ dct = {k: v for k, v in dct.items() if mtch(k, keys)}
705
+ if dct:
706
+ print(f'INFO: {cn if cn else "<problem.system>"}')
707
+ pprint(dct)
708
+ print("-" * 80)
709
+
710
+ @property
711
+ def check_dynamics(self):
712
+ sesh = self.sesh
713
+ return sesh._dxdt is not None and sesh._dxdt is not False
714
+
715
+ # Context Manager Interface
716
+ def __enter__(self):
717
+ # Set the new state
718
+
719
+ if self.entered:
720
+ # TODO: enable env-var STRICT MODE to fail on things like this
721
+ self.warning(f"context already entered!")
722
+ elif self.log_level < 10:
723
+ self.debug(
724
+ f"enter context: {self.level_name} {self._dxdt} {self.dynamics_updated}"
725
+ )
726
+
727
+ # Important managed updates / refs from Xnew input
728
+ self.activate_temp_state()
729
+ self.entered = True
730
+
731
+ # signals / updates
732
+ if self.pre_exec:
733
+ self.pre_execute()
734
+
735
+ # TODO: create a component-slot ref-update graph, and update the system references accordingly.
736
+ # TODO: map the signals to the system references, and update the system references accordingly.
737
+ # TODO:
738
+ # transients wont update components/ methods dynamically (or shouldn't) so we can just update the system references once and be done with it for other cases, but that is not necessary unless a component changes or a component has in general a unique reference update system (economics / component-iterators)
739
+ sesh = self.sesh
740
+ if not sesh._dxdt is True and self.enter_refresh:
741
+ sesh.min_refresh(sesh=sesh)
742
+
743
+ elif sesh.dynamics_updated:
744
+ sesh.update_dynamics(sesh=sesh)
745
+
746
+ # Check for existing session
747
+ if sesh not in [None, self]:
748
+ self.msg(f"entering existing execution context")
749
+ if not isinstance(self, self.class_cache):
750
+ self.warning(f"change of execution class!")
751
+ # global level number
752
+
753
+ self.class_cache.level_number += 1
754
+ self.class_cache.session._prob_levels[self.level_name] = self
755
+
756
+ return self.class_cache.session # appear as top
757
+
758
+ # return New
759
+ self.class_cache.session = self
760
+ self.class_cache.level_number = 0
761
+
762
+ if self.log_level < 10:
763
+ refs = {k: v for k, v in self.sesh._sys_refs.get("attrs", {}).items() if v}
764
+ self.debug(
765
+ f"creating execution context for {self.system}| {self._slv_kw}| {refs}"
766
+ )
767
+
768
+ return self # return the local problem context, use self.sesh to get top values
769
+
770
+ def __exit__(self, exc_type, exc_value, traceback):
771
+ # define exit action, to handle the error here return True. Otherwise error propigates up to top level
772
+ self.exited = True
773
+ if self.log_level < 10:
774
+ self.debug(f"exit action {exc_type} {exc_value}")
775
+
776
+ # Last opprotunity to update the system at tsate
777
+ if self.post_exec:
778
+ # a component cutsom callback + signals
779
+ self.post_execute()
780
+
781
+ if self.post_callback:
782
+ # a context custom callback
783
+ self.post_callback()
784
+
785
+ # save state to dataframe
786
+ if self.save_on_exit:
787
+ self.save_data()
788
+
789
+ # sesh = self.sesh #this should be here
790
+ # if self.level_name in sesh._prob_levels:
791
+ # sesh._prob_levels.pop(self.level_name)
792
+
793
+ # Exit Scenerio (boolean return important for context manager exit handling in heirarchy)
794
+ if isinstance(exc_value, ProblemExit):
795
+ if self.log_level < 7:
796
+ self.debug(f"exit action {exc_type}| {exc_value.__dict__}")
797
+
798
+ # first things first
799
+ if exc_value.revert:
800
+ self.revert_to_start()
801
+ if self.pre_exec:
802
+ self.pre_execute()
803
+
804
+ lvl_match = False
805
+ # Decide our exit conditon (if we should exit)
806
+ if isinstance(exc_value, ProblemExitAtLevel):
807
+ # should we stop?
808
+ lvl_match = exc_value.level == self.level_name
809
+ if lvl_match:
810
+ if self.log_level <= 11:
811
+ self.debug(f"exit at level {exc_value}")
812
+ ext = True
813
+ else:
814
+ if self.log_level <= 5:
815
+ self.msg(f"exit not at level {exc_value}")
816
+ ext = False
817
+
818
+ # Check if we missed a level name and its the top level, if so then we raise a real error!
819
+ # always exit with level_name='top' at outer context
820
+ if (
821
+ not ext
822
+ and self.class_cache.session is self
823
+ and exc_value.level == "top"
824
+ ):
825
+ if self.log_level <= 11:
826
+ self.debug(f"exit at top")
827
+
828
+ ext = True # top override
829
+
830
+ elif self.class_cache.session is self and not ext:
831
+ # never ever leave the top level without deleting the session
832
+ self.class_cache.level_number = 0
833
+ if type(self.problems_dict) is not dict:
834
+ self.problems_dict.pop(self._problem_id, None)
835
+ del self.class_cache.session
836
+ raise KeyError(f"cant exit to level! {exc_value.level} not found!!")
837
+
838
+ else:
839
+ if self.log_level <= 18:
840
+ self.info(f"problem exit revert={exc_value.revert}")
841
+
842
+ ext = True # basic exit is one level up
843
+
844
+ self.clean_context()
845
+ if type(self.problems_dict) is not dict:
846
+ self.problems_dict.pop(self._problem_id, None)
847
+ return ext
848
+
849
+ # default exit scenerios
850
+ elif exc_type is not None:
851
+ ext = self.error_action(exc_value)
852
+ else:
853
+ ext = self.exit_action()
854
+
855
+ self.clean_context()
856
+ if type(self.problems_dict) is not dict:
857
+ self.problems_dict.pop(self._problem_id, None)
858
+ return ext
859
+
860
+ def debug_levels(self):
861
+ """debug the levels of the context"""
862
+ if hasattr(self.class_cache, "session"):
863
+ for k, v in self.class_cache.session._prob_levels.items():
864
+ self.info(f"level: {k} | {v} | {v.x_start}")
865
+
866
+ else:
867
+ raise IllegalArgument(f"no session available")
868
+
869
+ # Multi Context Exiting:
870
+ # TODO: rethink this
871
+ def persist_contexts(self):
872
+ """convert all contexts to a new storage format"""
873
+ self.info(f"persisting contexts!")
874
+ current_problems = self.problems_dict
875
+ ProblemExec.problems_dict = {}
876
+ for k, v in current_problems.items():
877
+ self.problems_dict[k] = v # you will go on!
878
+
879
+ def discard_contexts(self):
880
+ """discard all contexts"""
881
+ current_problems = self.problems_dict
882
+ ProblemExec.problems_dict = weakref.WeakValueDictionary()
883
+ for k, v in current_problems.items():
884
+ ProblemExec.problems_dict[k] = v # you will go on!
885
+
886
+ def reset_contexts(self, fail_if_discardmode=True):
887
+ """reset all contexts to a new storage format"""
888
+ if isinstance(self.problems_dict, dict):
889
+ ProblemExec.problems_dict = {}
890
+ elif fail_if_discardmode:
891
+ raise IllegalArgument(
892
+ f"cant reset contexts! {self.problems_dict} while not in persistance mode"
893
+ )
894
+
895
+ def exit_with_state(self):
896
+ raise ProblemExit(self, revert=False)
897
+
898
+ def exit_and_revert(self):
899
+ raise ProblemExit(self, revert=True)
900
+
901
+ def exit_to_level(self, level: str, revert=False):
902
+ raise ProblemExitAtLevel(self, level=level, revert=revert)
903
+
904
+ def exit_action(self):
905
+ """handles the exit action wrt system"""
906
+ EOL = self.class_cache.session is self or self.level_name == "top"
907
+ if self.revert_last and EOL:
908
+ if self.log_level <= 8:
909
+ self.debug(f"revert last!")
910
+ self.debug(f"revert to{self.x_start}")
911
+ self.revert_to_start()
912
+
913
+ # run execute
914
+ if self.pre_exec:
915
+ self.pre_execute()
916
+
917
+ elif self.revert_every:
918
+ if self.log_level <= 8:
919
+ self.debug(f"revert to{self.x_start}")
920
+ self.revert_to_start()
921
+
922
+ # run execute
923
+ if self.pre_exec:
924
+ self.pre_execute()
925
+
926
+ # TODO: add exit on success option
927
+ return True # continue as normal
928
+
929
+ def error_action(self, error):
930
+ """handles the error action wrt to the problem"""
931
+ if self.log_level <= 11:
932
+ self.debug(f" with input: {self.kwargs}")
933
+
934
+ if self.fail_revert:
935
+ self.revert_to_start()
936
+
937
+ if self.exit_on_failure:
938
+ self.error(error, f"error in execution context")
939
+ return False # send me up
940
+ else:
941
+ self.warning(f"error in execution context: {error}")
942
+
943
+ return True # our problem will go on
944
+
945
+ def save_data(self, index=None, force=False, **add_data):
946
+ """save data to the context"""
947
+
948
+ sesh = self.sesh
949
+ if not self.exited and self.post_exec:
950
+ # a context custom callback
951
+ sesh.post_execute()
952
+
953
+ if force or not sesh.data or sesh.system.anything_changed:
954
+ out = sesh.output_state
955
+ if index is None and sesh._dxdt == True: # integration
956
+ index = sesh._time
957
+ elif index is None:
958
+ index = sesh._index
959
+
960
+ if add_data:
961
+ out.update(add_data)
962
+ if sesh._dxdt == True:
963
+ out["time"] = sesh._time
964
+ out["index"] = index
965
+ sesh._data[index] = out
966
+ # if we are integrating, then we dont increment the index
967
+ if sesh._dxdt != True:
968
+ sesh._index += 1
969
+
970
+ # reset the data for changed items
971
+ sesh.system._anything_changed = False
972
+ self.debug(f"data saved = {index}")
973
+
974
+ elif self.log_level < 15:
975
+ self.warning(f"no data saved, nothing changed")
976
+
977
+ def clean_context(self):
978
+ if hasattr(self.class_cache, "session") and self.class_cache.session is self:
979
+ if self.log_level <= 8:
980
+ self.debug(f"closing execution session")
981
+ self.class_cache.level_number = 0
982
+ del self.class_cache.session
983
+ elif hasattr(self.class_cache, "session"):
984
+ # global level number
985
+ self.class_cache.level_number -= 1
986
+
987
+ # if we are the top level, then we mark the session runtime/messages
988
+ if self.session_id == True:
989
+ self._run_end = datetime.datetime.now()
990
+ self._run_time = self._run_end - self._run_start
991
+ if self.log_level <= 10:
992
+ self.debug(
993
+ f"EXIT[{self.system.identity}] run time: {self._run_time}",
994
+ lvl=5,
995
+ )
996
+
997
+ # time context
998
+ def set_time(self, time, dt):
999
+ self._last_time = lt = self._time
1000
+ self._time = time
1001
+ dt_calc = time - lt
1002
+ self._dt = dt if dt_calc <= 0 else dt_calc
1003
+ # self.system.set_time(time) #system times / subcomponents too
1004
+
1005
+ def integrate(self, endtime, dt=0.001, max_step_dt=0.01, X0=None, **kw):
1006
+ # Unpack Transient Problem
1007
+ sesh = self.sesh
1008
+ intl_refs = sesh.integrator_var_refs # order forms problem basis
1009
+ sesh.prv_ingtegral_refs = intl_refs # for rate function
1010
+ refs = sesh._sys_refs
1011
+ system = sesh.system
1012
+
1013
+ min_kw = sesh._minimizer_kw
1014
+ if min_kw is None:
1015
+ min_kw = {}
1016
+
1017
+ if dt > max_step_dt:
1018
+ self.warning(f"dt {dt} > max_step_dt {max_step_dt}!")
1019
+ dt = max_step_dt
1020
+
1021
+ if self.log_level < 15:
1022
+ self.info(f"simulating {system},{sesh}| int:{intl_refs} | refs: {refs}")
1023
+
1024
+ if not intl_refs:
1025
+ raise Exception(f"no transient parameters found")
1026
+
1027
+ x_cur = {k: v.value(v.comp, sesh) for k, v in intl_refs.items()}
1028
+
1029
+ if self.log_level < 10:
1030
+ self.debug(f"initial state {X0} {intl_refs}| {refs}")
1031
+
1032
+ if X0 is None:
1033
+ # get current
1034
+ X0 = x_cur
1035
+ # add any missing solver vars existing in the system
1036
+ if set(X0) != set(x_cur):
1037
+ X0 = x_cur.update(X0)
1038
+
1039
+ # this will fail if X0 doesn't have solver vars!
1040
+ X0 = np.array([X0[p] for p in intl_refs])
1041
+ Time = np.arange(sesh.system.time, endtime + dt, dt)
1042
+
1043
+ rate_kw = {"min_kw": min_kw, "dt": dt}
1044
+
1045
+ # get the probelem variables
1046
+ Xss = sesh.problem_opt_vars
1047
+ Yobj = sesh.final_objectives
1048
+
1049
+ # run the simulation from the current state to the endtime
1050
+ ans = solve_ivp(
1051
+ sesh.integral_rate,
1052
+ [sesh.system.time, endtime],
1053
+ X0,
1054
+ method="RK45",
1055
+ t_eval=Time,
1056
+ max_step=max_step_dt,
1057
+ args=(dt, Xss, Yobj),
1058
+ **kw,
1059
+ )
1060
+
1061
+ print(ans)
1062
+
1063
+ return ans
1064
+
1065
+ def integral_rate(self, t, x, dt, Xss=None, Yobj=None, **kw):
1066
+ """provides the dynamic rate of the system at time t, and state x"""
1067
+ sesh = self.sesh
1068
+ intl_refs = sesh.prv_ingtegral_refs # cached in self.integral()
1069
+ refs = sesh._sys_refs
1070
+ system = sesh.system
1071
+
1072
+ out = {p: np.nan for p in intl_refs}
1073
+ Xin = {p: x[i] for i, p in enumerate(intl_refs)}
1074
+
1075
+ if self.log_level < 10:
1076
+ self.info(f"sim_iter {t} {x} {Xin}")
1077
+
1078
+ with ProblemExec(
1079
+ system,
1080
+ level_name="tr_slvr",
1081
+ Xnew=Xin,
1082
+ revert_last=False,
1083
+ revert_every=False,
1084
+ dxdt=True,
1085
+ ) as pbx:
1086
+ # test for record time
1087
+
1088
+ self.set_time(t, dt)
1089
+
1090
+ # save data at the start
1091
+ pbx.save_data() # TODO: check_enable/ rate_check
1092
+
1093
+ # ad hoc time integration
1094
+ for name, trdct in pbx.integrators.items():
1095
+ if self.log_level <= 10:
1096
+ self.info(
1097
+ f"updating {trdct.var}|{trdct.var_ref.value(self.system,self)}<-{trdct.rate}|{trdct.current_rate}|{trdct.rate_ref.value(trdct.comp,system.last_context)}"
1098
+ )
1099
+ print(getattr(self.system, trdct.var, None))
1100
+ print(getattr(self.system, trdct.rate, None))
1101
+
1102
+ out[trdct.var] = trdct.current_rate
1103
+
1104
+ # dynamics
1105
+ for compnm, compdict in pbx.dynamic_comps.items():
1106
+ comp = compdict # ["comp"]
1107
+ if not comp.dynamic_state_vars and not comp.dynamic_input_vars:
1108
+ continue # nothing to do...
1109
+
1110
+ Xds = np.array([r.value() for r in comp.Xt_ref.values()])
1111
+ Uds = np.array([r.value() for r in comp.Ut_ref.values()])
1112
+ # time updated in step
1113
+ # system.info(f'comp {comp} {compnm} {Xds} {Uds}')
1114
+ dxdt = comp.step(t, dt, Xds, Uds, True)
1115
+
1116
+ for i, (p, ref) in enumerate(comp.Xt_ref.items()):
1117
+ out[(f"{compnm}." if compnm else "") + p] = dxdt[i]
1118
+
1119
+ # solvers
1120
+ if self.run_solver and Xss and Yobj and self.solveable:
1121
+ # TODO: add in any transient
1122
+ with ProblemExec(
1123
+ system,
1124
+ level_name="ss_slvr",
1125
+ revert_last=False,
1126
+ revert_every=False,
1127
+ dxdt=True,
1128
+ ) as pbx:
1129
+ ss_out = pbx.solve_min(Xss, Yobj, **self._minimizer_kw)
1130
+ if ss_out["ans"].success:
1131
+ if self.log_level <= 9:
1132
+ self.info(
1133
+ f'exiting solver {t} {ss_out["Xans"]} {ss_out["Xstart"]}'
1134
+ )
1135
+ pbx.set_ref_values(ss_out["Xans"], scope="intgrl")
1136
+ pbx.exit_to_level("ss_slvr", False)
1137
+ else:
1138
+ self.warning(
1139
+ f'solver failed to converge {ss_out["ans"].message} {ss_out["Xans"]} {ss_out["X0"]}'
1140
+ )
1141
+ if pbx.opt_fail:
1142
+ pbx.exit_to_level("sim", pbx.fail_revert)
1143
+ else:
1144
+ pbx.exit_to_level("ss_slvr", pbx.fail_revert)
1145
+
1146
+ V_dxdt = np.array([out[p] for p in intl_refs])
1147
+ if self.log_level <= 10:
1148
+ self.info(f"exiting transient {t} {V_dxdt} {Xin}")
1149
+ pbx.exit_to_level("tr_slvr", False)
1150
+
1151
+ if any(np.isnan(V_dxdt)):
1152
+ self.warning(f"solver got infeasible: {V_dxdt}|{Xin}")
1153
+ pbx.exit_and_revert()
1154
+ # TODO: handle this better, seems to cause a warning
1155
+ raise ValueError(f"infeasible! nan result {V_dxdt} {out} {Xin}")
1156
+
1157
+ elif self.log_level <= 5:
1158
+ self.debug(f"rate {self._dt} {t:5.3f}| {x}<-{V_dxdt} {Xin}")
1159
+
1160
+ return V_dxdt
1161
+
1162
+ def solve_min(self, Xref=None, Yref=None, output=None, **kw):
1163
+ """
1164
+ Solve the minimization problem using the given vars and constraints. And sets the system state to the solution depending on input of the following:
1165
+
1166
+ Solve the root problem using the given vars.
1167
+ :param Xref: The reference input values.
1168
+ :param Yref: The reference objective values to minimize.
1169
+ :param output: The output dictionary to store the results. (default: None)
1170
+ :param fail: Flag indicating whether to raise an exception if the solver doesn't converge. (default: True)
1171
+ :param kw: Additional keyword arguments.
1172
+ :return: The output dictionary containing the results.
1173
+ """
1174
+ sesh = self.sesh
1175
+ if Xref is None:
1176
+ Xref = sesh.Xref
1177
+
1178
+ if Yref is None:
1179
+ Yref = sesh.final_objectives
1180
+
1181
+ thresh = kw.pop("thresh", sesh.success_thresh)
1182
+
1183
+ # TODO: options for solver detail in response
1184
+ dflt = {
1185
+ "Xstart": Ref.refset_get(Xref, prob=sesh),
1186
+ "Ystart": Ref.refset_get(Yref, prob=sesh),
1187
+ "Xans": None,
1188
+ "success": None,
1189
+ "Xans": None,
1190
+ "Yobj": None,
1191
+ "Ycon": None,
1192
+ "ans": None,
1193
+ "weights": sesh._weights,
1194
+ "constraints": sesh.constraints,
1195
+ }
1196
+
1197
+ if output:
1198
+ dflt.update(output)
1199
+ output = dflt
1200
+ else:
1201
+ output = dflt
1202
+
1203
+ if len(Xref) == 0:
1204
+ self.debug(f"no variables found for solver: {kw}")
1205
+ # None for `ans` will not trigger optimization failure
1206
+ return output
1207
+
1208
+ # override constraints input
1209
+ kw.update(sesh.constraints)
1210
+
1211
+ if len(kw["bounds"]) != len(Xref):
1212
+ raise ValueError(
1213
+ f"bounds {len(sesh.constraints['bounds'])} != Xref {len(Xref)}"
1214
+ )
1215
+
1216
+ if self.log_level < 10:
1217
+ self.debug(f"minimize {Xref} {Yref} {kw}")
1218
+
1219
+ if sesh._weights is not None:
1220
+ kw["weights"] = sesh._weights
1221
+
1222
+ sesh._ans = refmin_solve(sesh.system, self, Xref, Yref, **kw)
1223
+ output["ans"] = sesh._ans
1224
+
1225
+ sesh.handle_solution(sesh._ans, Xref, Yref, output)
1226
+
1227
+ return output
1228
+
1229
+ def handle_solution(self, answer, Xref, Yref, output):
1230
+ # TODO: move exit condition handiling somewhere else, reduce cross over from process_ans
1231
+ sesh = self.sesh
1232
+ if self.log_level < 10:
1233
+ self.info(f"handiling solution: {answer}")
1234
+ thresh = sesh.success_thresh
1235
+ vars = list(Xref)
1236
+
1237
+ # Output Results
1238
+ Xa = {p: answer.x[i] for i, p in enumerate(vars)}
1239
+ output["Xans"] = Xa
1240
+ Ref.refset_input(Xref, Xa, scope="solvd")
1241
+
1242
+ Yout = {p: yit.value(yit.comp, self) for p, yit in Yref.items()}
1243
+ output["Yobj"] = Yout
1244
+
1245
+ Ycon = {}
1246
+ if sesh.constraints["constraints"]:
1247
+ x_in = answer.x
1248
+ for c, k in zip(sesh.constraints["constraints"], sesh.constraints["info"]):
1249
+ cv = c["fun"](x_in, self, {})
1250
+ Ycon[k] = cv
1251
+ output["Ycon"] = Ycon
1252
+
1253
+ de = answer.fun
1254
+ if answer.success and de < thresh if thresh else True:
1255
+ sesh.system._converged = True # TODO: put in context
1256
+ sesh._converged = True
1257
+ output["success"] = True
1258
+
1259
+ elif answer.success:
1260
+ # out of threshold condition
1261
+ self.warning(
1262
+ f"solver didnt meet threshold: {de} <? {thresh} ! {answer.x} -> residual: {answer.fun}"
1263
+ )
1264
+ sesh.system._converged = False
1265
+ sesh._converged = False
1266
+ output["success"] = False # only false with threshold
1267
+
1268
+ else:
1269
+ sesh.system._converged = False
1270
+ sesh._converged = False
1271
+ if self.opt_fail:
1272
+ raise Exception(f"solver didnt converge: {answer}")
1273
+ else:
1274
+ self.warning(f"solver didnt converge: {answer}")
1275
+ output["success"] = False
1276
+
1277
+ return output
1278
+
1279
+ # Solver Parsing Methods
1280
+
1281
+ def sys_solver_objectives(self, **kw):
1282
+ """gathers variables from solver vars, and attempts to locate any input_vars to add as well. use exclude_vars to eliminate a variable from the solver"""
1283
+ sys_refs = self.sesh._sys_refs
1284
+
1285
+ # Convert result per kind of objective (min/max ect)
1286
+ objs = sys_refs.get("attrs", {}).get("solver.obj", {})
1287
+ return {k: v for k, v in objs.items()}
1288
+
1289
+ def pos_obj(self, ref):
1290
+ """converts an objective to a positive value"""
1291
+
1292
+ def f(sys, prob):
1293
+ return 1 + ref.value(sys, prob) ** 2
1294
+
1295
+ return ref.copy(key=f)
1296
+
1297
+ @property
1298
+ def final_objectives(self) -> dict:
1299
+ """returns the final objective of the system, depending on mixed objective, equalities, and constraints"""
1300
+ sesh = self.sesh
1301
+ Yobj = sesh.problem_objs
1302
+ Yeq = sesh.problem_eq
1303
+ Xss = sesh.problem_opt_vars
1304
+ if Yobj:
1305
+ return Yobj # here is your application objective, sir
1306
+
1307
+ # now make up an objective
1308
+ elif not Yobj and Yeq:
1309
+ # TODO: handle case of Yineq == None with root solver
1310
+ self.info(f"making Yobj from Yeq: {Yeq}")
1311
+ Yobj = {k: self.pos_obj(v) for k, v in Yeq.items()}
1312
+
1313
+ elif not Yobj:
1314
+ # minimize the product of all vars, so the smallest value is the best that satisfies all constraints
1315
+ if self.session_id == True:
1316
+ self.info(f"making Yobj from X: {Xss}")
1317
+
1318
+ def dflt(sys, prob) -> float:
1319
+ out = 1
1320
+ for k, v in prob.problem_opt_vars.items():
1321
+ val = v.value(sys, prob)
1322
+ # The code snippet is calculating the linear norm of positive
1323
+ # values greater than 1 by adding the square root of the sum
1324
+ # of 1 and the square of the value to the variable `out`. This
1325
+ # operation is intended to apply a large penalty to positive
1326
+ # values greater than 1.
1327
+ out = (
1328
+ out + (1 + val**2) ** 0.5
1329
+ ) # linear norm of positive values > 1 should be very large penalty
1330
+ return 1
1331
+
1332
+ Yobj = {"smallness": Ref(sesh.system, dflt)}
1333
+ return Yobj # our residual based objective
1334
+
1335
+ def sys_solver_constraints(self, add_con=None, combo_filter=True, **kw):
1336
+ """formatted as arguments for the solver"""
1337
+ from engforge.solver_utils import create_constraint
1338
+
1339
+ sesh = self.sesh
1340
+ Xrefs = sesh.Xref
1341
+
1342
+ system = sesh.system
1343
+ sys_refs = sesh._sys_refs
1344
+ all_refz = sesh.ref_attrs
1345
+
1346
+ extra_kw = self.kwargs
1347
+
1348
+ # TODO: move to kwarg parsing on setup
1349
+ deactivated = (
1350
+ ext_str_list(extra_kw, "deactivate", [])
1351
+ if "deactivate" in extra_kw and extra_kw["deactivate"]
1352
+ else []
1353
+ )
1354
+ activated = (
1355
+ ext_str_list(extra_kw, "activate", [])
1356
+ if "activate" in extra_kw and extra_kw["activate"]
1357
+ else []
1358
+ )
1359
+
1360
+ slv_inst = sys_refs.get("type", {}).get("solver", {})
1361
+ trv_inst = {v.var: v for v in sys_refs.get("type", {}).get("time", {}).values()}
1362
+ sys_refs = sys_refs.get("attrs", {})
1363
+
1364
+ if add_con is None:
1365
+ add_con = {}
1366
+
1367
+ # The official definition of X var order
1368
+ Nstates = len(Xrefs)
1369
+ Xvars = list(Xrefs) # get names of solvers + dynamics
1370
+
1371
+ # constraints lookup
1372
+ bnd_list = [[None, None]] * Nstates
1373
+ con_list = []
1374
+ con_info = [] # names of constraints
1375
+ constraints = {
1376
+ "constraints": con_list,
1377
+ "bounds": bnd_list,
1378
+ "info": con_info,
1379
+ }
1380
+
1381
+ if isinstance(add_con, dict):
1382
+ # Remove None Values
1383
+ nones = {k for k, v in add_con.items() if v is None}
1384
+ for ki in nones:
1385
+ constraints.pop(ki, None)
1386
+ assert all(
1387
+ [callable(v) for k, v in add_con.items()]
1388
+ ), f"all custom input for constraints must be callable with X as argument"
1389
+ constraints["constraints"].extend(
1390
+ [v for k, v in add_con.items() if v is not None]
1391
+ )
1392
+
1393
+ if add_con is False:
1394
+ constraints = {} # youre free!
1395
+ return constraints
1396
+
1397
+ # Add Constraints
1398
+ ex_arg = {"con_args": (), **kw}
1399
+
1400
+ # TODO: dynamic limits
1401
+ # Establish Anonymous Problem Constraint Refs
1402
+ for slvr, ref in sesh.problem_opt_vars.items():
1403
+ assert not all(
1404
+ (slvr in slv_inst, slvr in trv_inst)
1405
+ ), f"solver and integrator share parameter {slvr} "
1406
+ if slvr in slv_inst:
1407
+ slv = slv_inst[slvr]
1408
+ slv_var = True # mark a static varible
1409
+ elif slvr in trv_inst:
1410
+ slv = trv_inst[slvr]
1411
+ slv_var = False # a dynamic variable
1412
+ else:
1413
+ self.warning(f"no solver instance for {slvr} ")
1414
+ continue
1415
+
1416
+ slv_constraints = slv.constraints
1417
+ if log.log_level < 7:
1418
+ self.debug(f"constraints {slvr} {slv_constraints}")
1419
+
1420
+ # combine the independent variables into one, but allow multiple dependent objectives for constraints/objectives
1421
+ for ctype in slv_constraints:
1422
+ cval = ctype["value"]
1423
+ kind = ctype["type"]
1424
+ var = ctype["var"]
1425
+ if log.log_level < 3:
1426
+ self.msg(f"const: {slvr} {ctype}")
1427
+
1428
+ if cval is not None and slvr in Xvars:
1429
+ # Check for combos & activation
1430
+ combos = None
1431
+ if "combos" in ctype:
1432
+ combos = ctype["combos"]
1433
+ combo_var = ctype["combo_var"]
1434
+ active = ctype.get("active", True)
1435
+ in_activate = (
1436
+ any([arg_var_compare(combo_var, v) for v in activated])
1437
+ if activated
1438
+ else False
1439
+ )
1440
+ in_deactivate = (
1441
+ any([arg_var_compare(combo_var, v) for v in deactivated])
1442
+ if deactivated
1443
+ else False
1444
+ )
1445
+
1446
+ if log.log_level <= 5:
1447
+ self.debug(f"filter combo: {ctype}=?{extra_kw}")
1448
+
1449
+ # Check active or activated
1450
+ if not active and not activated:
1451
+ if log.log_level < 3:
1452
+ self.msg(f"skip con: inactive {var} {slvr} {ctype}")
1453
+ continue
1454
+
1455
+ elif not active and not in_activate:
1456
+ if log.log_level < 3:
1457
+ self.msg(f"skip con: inactive {var} {slvr} {ctype}")
1458
+ continue
1459
+
1460
+ elif active and in_deactivate:
1461
+ if log.log_level < 3:
1462
+ self.msg(f"skip con: deactivated {var} {slvr} ")
1463
+ continue
1464
+
1465
+ if combos and combo_filter:
1466
+ filt = filter_combos(combo_var, slv, extra_kw, combos)
1467
+ if not filt:
1468
+ if log.log_level < 5:
1469
+ self.debug(
1470
+ f"filtering constraint={filt} {var} |{combos}"
1471
+ )
1472
+ continue
1473
+
1474
+ if log.log_level < 10:
1475
+ self.debug(f"adding var constraint {var,slvr,ctype,combos}")
1476
+
1477
+ # get the index of the variable
1478
+ x_inx = Xvars.index(slvr)
1479
+
1480
+ # lookup rates for overlapping dynamic variables
1481
+ rate_val = None
1482
+ if sesh._dxdt is not None:
1483
+ if isinstance(sesh._dxdt, dict) and not slv_var:
1484
+ rate_val = sesh._dxdt.get(slvr, 0)
1485
+ elif not slv_var:
1486
+ rate_val = 0
1487
+
1488
+ # add the dynamic parameters when configured
1489
+ if not slv_var and rate_val is not None:
1490
+ # print(f'adding dynamic constraint {slvr} {rate_val}')
1491
+ # if kind in ('min','max') and slvr in Xvars:
1492
+ varref = Xrefs[slvr]
1493
+ # varref = slv.rate_ref
1494
+ # Ref Case
1495
+ ccst = ref_to_val_constraint(
1496
+ system,
1497
+ varref.comp,
1498
+ system.last_context,
1499
+ Xrefs,
1500
+ varref,
1501
+ kind,
1502
+ rate_val,
1503
+ **kw,
1504
+ )
1505
+ # con_list.append(ccst)
1506
+ con_info.append(
1507
+ f"dxdt_{varref.comp.classname}.{slvr}_{kind}_{cval}"
1508
+ )
1509
+ con_list.append(ccst)
1510
+
1511
+ # elif slv_var:
1512
+ elif slv_var:
1513
+ # establish simple bounds w/ solver
1514
+ if (
1515
+ kind in ("min", "max")
1516
+ and slvr in Xvars
1517
+ and isinstance(cval, (int, float))
1518
+ ):
1519
+ minv, maxv = bnd_list[x_inx]
1520
+ bnd_list[x_inx] = [
1521
+ cval if kind == "min" else minv,
1522
+ cval if kind == "max" else maxv,
1523
+ ]
1524
+
1525
+ # add the bias of cval to the objective function
1526
+ elif kind in ("min", "max") and slvr in Xvars:
1527
+ varref = Xrefs[slvr]
1528
+ # Ref Case
1529
+ ccst = ref_to_val_constraint(
1530
+ system,
1531
+ varref.comp,
1532
+ system.last_context,
1533
+ Xrefs,
1534
+ varref,
1535
+ kind,
1536
+ cval,
1537
+ **kw,
1538
+ )
1539
+ con_info.append(f"val_{ref.comp.classname}_{kind}_{slvr}")
1540
+ con_list.append(ccst)
1541
+
1542
+ else:
1543
+ self.warning(
1544
+ f"bad constraint: {cval} {kind} {slv_var}|{slvr}"
1545
+ )
1546
+
1547
+ # Add Constraints
1548
+ for slvr, ref in self.problem_ineq.items():
1549
+ slv = slv_inst[slvr]
1550
+ slv_constraints = slv.constraints
1551
+ parent = self.get_parent_key(slvr, look_back_num=2) # get the parent comp
1552
+ for ctype in slv_constraints:
1553
+ cval = ctype["value"]
1554
+ kind = ctype["type"]
1555
+ if cval is not None:
1556
+ name = f"ineq_{parent}{ref.comp.classname}.{slvr}_{kind}_{cval}"
1557
+ if log.log_level < 5:
1558
+ self.debug(f"filtering constraint {slvr} |{name}")
1559
+ con_info.append(name)
1560
+ con_list.append(
1561
+ create_constraint(
1562
+ system,
1563
+ ref.comp,
1564
+ Xrefs,
1565
+ "ineq",
1566
+ cval,
1567
+ system.last_context,
1568
+ **kw,
1569
+ )
1570
+ )
1571
+
1572
+ for slvr, ref in self.problem_eq.items():
1573
+ parent = self.get_parent_key(slvr, look_back_num=2) # get the parent comp
1574
+ if slvr in slv_inst and slvr in all_refz.get("solver.eq", {}):
1575
+ slv = slv_inst[slvr]
1576
+ slv_constraints = slv.constraints
1577
+
1578
+ for ctype in slv_constraints:
1579
+ cval = ctype["value"]
1580
+ kind = ctype["type"]
1581
+ if cval is not None:
1582
+ name = f"eq_{parent}{ref.comp.classname}.{slvr}_{kind}_{cval}"
1583
+ if log.log_level < 5:
1584
+ self.debug(f"filtering constraint {slvr} |{name}")
1585
+ con_info.append(name)
1586
+ con_info.append(
1587
+ f"eq_{parent}{ref.comp.classname}.{slvr}_{kind}_{cval}"
1588
+ )
1589
+ con_list.append(
1590
+ create_constraint(
1591
+ system,
1592
+ ref.comp,
1593
+ Xrefs,
1594
+ "eq",
1595
+ cval,
1596
+ system.last_context,
1597
+ **kw,
1598
+ )
1599
+ )
1600
+ else:
1601
+ # This must be a dynamic rate
1602
+ self.debug(f"dynamic rate eq {slvr} ")
1603
+ con_info.append(f"eq_{parent}{ref.comp.classname}.{slvr}_rate")
1604
+ con_list.append(
1605
+ create_constraint(
1606
+ system,
1607
+ ref.comp,
1608
+ Xrefs,
1609
+ "eq",
1610
+ ref,
1611
+ system.last_context,
1612
+ **kw,
1613
+ )
1614
+ )
1615
+
1616
+ return constraints
1617
+
1618
+ # General method to distribute input to internal components
1619
+ @classmethod
1620
+ def parse_default(self, key, defaults, input_dict, rmv=False, empty_str=True):
1621
+ """splits strings or lists and returns a list of options for the key, if nothing found returns None if fail set to True raises an exception, otherwise returns the default value"""
1622
+ if key in input_dict:
1623
+ # kwargs will no longer have key!
1624
+ if not rmv:
1625
+ option = input_dict.get(key)
1626
+ else:
1627
+ option = input_dict.pop(key)
1628
+ # print(f'removing option {key} {option}')
1629
+ if option is None:
1630
+ return option, False
1631
+
1632
+ elif isinstance(option, (int, float, bool)):
1633
+ return option, False
1634
+
1635
+ elif isinstance(option, str):
1636
+ if not empty_str and not option:
1637
+ return None, False
1638
+
1639
+ option = option.split(",")
1640
+
1641
+ return option, False
1642
+
1643
+ elif key in defaults:
1644
+ return defaults[key], True
1645
+
1646
+ return None, None
1647
+
1648
+ @classmethod
1649
+ def get_extra_kws(
1650
+ cls,
1651
+ kwargs,
1652
+ _check_keys: dict = slv_dflt_options,
1653
+ rmv=False,
1654
+ use_defaults=True,
1655
+ ):
1656
+ """extracts the combo input from the kwargs"""
1657
+ # extract combo input
1658
+ if not _check_keys:
1659
+ return {}
1660
+ _check_keys = _check_keys.copy()
1661
+ # TODO: allow extended check_keys / defaults to be passed in, now every value in check_keys has a default
1662
+ cur_in = kwargs
1663
+ output = {}
1664
+ for p, dflt in _check_keys.items():
1665
+ val, is_dflt = cls.parse_default(p, _check_keys, cur_in, rmv=rmv)
1666
+ if not is_dflt:
1667
+ output[p] = val
1668
+ elif use_defaults:
1669
+ output[p] = val
1670
+ if rmv:
1671
+ cur_in.pop(p, None)
1672
+
1673
+ # copy from data
1674
+ filtr = dict(
1675
+ list(
1676
+ filter(
1677
+ lambda kv: kv[1] is not None or kv[0] in _check_keys,
1678
+ output.items(),
1679
+ )
1680
+ )
1681
+ )
1682
+ # print(f'got {combos} -> {comboos} from {kwargs} with {_check_keys}')
1683
+ return filtr
1684
+
1685
+ # State Interfaces
1686
+ @property
1687
+ def record_state(self) -> dict:
1688
+ """records the state of the system using session"""
1689
+ # refs = self.all_variable_refs
1690
+ sesh = self.sesh
1691
+ # only get used refs modified no need for properties
1692
+ # TODO: more elegant solution
1693
+ chk = self.temp_state if self.temp_state else {}
1694
+ # FIXME: only record state as it changes
1695
+ # refs = {k:v for k,v in sesh.all_comps_and_vars.items()} # if k in chk
1696
+ refs = {k: v for k, v in sesh.all_comps_and_vars.items()}
1697
+ return Ref.refset_get(refs, sys=sesh.system, prob=self)
1698
+
1699
+ @property
1700
+ def output_state(self) -> dict:
1701
+ """records the state of the system"""
1702
+ sesh = self.sesh
1703
+ # TODO: add system_properties to num_refs / all_system_refs ect.
1704
+ if "nums" == sesh.save_mode:
1705
+ refs = sesh.num_refs
1706
+ elif "all" == sesh.save_mode:
1707
+ refs = sesh.all_system_references
1708
+ elif "vars" == sesh.save_mode:
1709
+ refs = self.all_variable_refs
1710
+ elif "prob" == sesh.save_mode:
1711
+ raise NotImplementedError(f"problem save mode not implemented")
1712
+ else:
1713
+ raise KeyError(f"unknown save mode {sesh.save_mode}, not in {save_modes}")
1714
+
1715
+ out = Ref.refset_get(refs, sys=sesh.system, prob=self)
1716
+ # Integration
1717
+ if sesh._dxdt == True:
1718
+ out["time"] = sesh._time
1719
+
1720
+ return out
1721
+
1722
+ def get_ref_values(self, refs=None):
1723
+ """returns the values of the refs"""
1724
+ sesh = self.sesh
1725
+ if refs is None:
1726
+ refs = sesh.all_system_references
1727
+ return Ref.refset_get(refs, sys=self.system, prob=self)
1728
+
1729
+ def set_ref_values(self, values, refs=None, scope="sref"):
1730
+ """returns the values of the refs"""
1731
+ # TODO: add checks for the refs
1732
+ if refs is None:
1733
+ sesh = self.sesh
1734
+ refs = sesh.all_comps_and_vars
1735
+ return Ref.refset_input(refs, values, scope=scope)
1736
+
1737
+ def change_sys_var(self, key, value, refs=None, doset=True, attr_key_map=None):
1738
+ """use this function to change the value of a system var and update the start state, multiple uses in the same context will not change the record preserving the start value
1739
+
1740
+ :param key: a string corresponding to a ref, or an `attrs.Attribute` of one of the system or its component's.
1741
+ """
1742
+ if self.log_level < 5:
1743
+ self.msg(f"setting var: {key} <= {value}")
1744
+
1745
+ # if isinstance(key,attrs.Attribute):
1746
+ # if attr_key_map is None:
1747
+ # attr_key_map = self.attribute_sys_key_map
1748
+ # key = attr_key_map[key] #change to system key format
1749
+
1750
+ if refs is None:
1751
+ refs = self.sesh.all_comps_and_vars
1752
+ if key in refs:
1753
+ ref = refs[key]
1754
+ if key not in self.x_start:
1755
+ cur_value = ref.value()
1756
+ self.x_start[key] = cur_value
1757
+ if self.log_level < 5:
1758
+ self.msg(f"setting var: {key} <= {value} from {cur_value}")
1759
+ if doset:
1760
+ ref.set_value(value)
1761
+ elif isinstance(key, Ref):
1762
+ ref = key
1763
+ self.x_start[key] = key.value()
1764
+ if doset:
1765
+ ref.set_value(value)
1766
+
1767
+ def set_checkpoint(self):
1768
+ """sets the checkpoint"""
1769
+ self.x_start = self.record_state
1770
+ if log.log_level <= 7:
1771
+ self.debug(f"set checkpoint: {list(self.x_start.values())}")
1772
+
1773
+ def revert_to_start(self):
1774
+ sesh = self.sesh
1775
+ if log.log_level < 5:
1776
+ xs = list(self.x_start.values())
1777
+ rs = list(self.record_state.values())
1778
+ self.debug(f"reverting to start: {xs} -> {rs}")
1779
+ # TODO: STRICT MODE Fail for refset_input
1780
+ Ref.refset_input(
1781
+ sesh.all_comps_and_vars, self.x_start, fail=False, scope="rvtst"
1782
+ )
1783
+
1784
+ def activate_temp_state(self, new_state=None):
1785
+ # TODO: determine when components change, and update refs accordingly!
1786
+ sesh = self.sesh
1787
+ # TODO: STRICT MODE Fail for refset_input
1788
+ if new_state:
1789
+ if self.log_level < 3:
1790
+ self.debug(f"new-state: {self.temp_state}")
1791
+ Ref.refset_input(
1792
+ sesh.all_comps_and_vars, new_state, fail=False, scope="ntemp"
1793
+ )
1794
+ elif self.temp_state:
1795
+ if self.log_level < 3:
1796
+ self.debug(f"act-state: {self.temp_state}")
1797
+ Ref.refset_input(
1798
+ sesh.all_comps_and_vars,
1799
+ self.temp_state,
1800
+ fail=False,
1801
+ scope="atemp",
1802
+ )
1803
+ elif self.log_level < 3:
1804
+ self.debug(f"no-state: {new_state}")
1805
+
1806
+ # initial establishment costs / ect
1807
+ if sesh.pre_exec:
1808
+ sesh.pre_execute()
1809
+
1810
+ # System Events
1811
+ def apply_pre_signals(self):
1812
+ """applies all pre signals"""
1813
+ msg_lvl = self.log_level <= 2
1814
+ if self.log_level < 5:
1815
+ self.msg(f"applying pre signals", lvl=6)
1816
+ for signame, sig in self.sesh.signals.items():
1817
+ if sig.mode == "pre" or sig.mode == "both":
1818
+ if msg_lvl:
1819
+ self.msg(f"applying post signals: {signame}", lvl=3)
1820
+ sig.apply()
1821
+
1822
+ def apply_post_signals(self):
1823
+ """applies all post signals"""
1824
+ msg_lvl = self.log_level <= 2
1825
+ if self.log_level < 5:
1826
+ self.msg(f"applying post signals", lvl=6)
1827
+ for signame, sig in self.sesh.signals.items():
1828
+ if sig.mode == "post" or sig.mode == "both":
1829
+ if msg_lvl:
1830
+ self.msg(f"applying post signals: {signame}", lvl=3)
1831
+ sig.apply()
1832
+
1833
+ def update_system(self, *args, **kwargs):
1834
+ """updates the system"""
1835
+ for ukey, uref in self.sesh._update_refs.items():
1836
+ self.debug(f"context updating {ukey}")
1837
+ uref.value(*args, **kwargs)
1838
+
1839
+ def post_update_system(self, *args, **kwargs):
1840
+ """updates the system"""
1841
+ for ukey, uref in self.sesh._post_update_refs.items():
1842
+ self.debug(f"context post updating {ukey}")
1843
+ uref.value(*args, **kwargs)
1844
+
1845
+ def pre_execute(self, *args, **kwargs):
1846
+ """Updates the pre/both signals after the solver has been executed. This is useful for updating the system state after the solver has been executed."""
1847
+ if log.log_level < 5:
1848
+ self.msg(f"pre execute")
1849
+
1850
+ sesh = self.sesh
1851
+ sesh.apply_pre_signals()
1852
+ sesh.update_system(*args, **kwargs)
1853
+
1854
+ def post_execute(self, *args, **kwargs):
1855
+ """Updates the post/both signals after the solver has been executed. This is useful for updating the system state after the solver has been executed."""
1856
+ if log.log_level < 5:
1857
+ self.msg(f"post execute")
1858
+
1859
+ sesh = self.sesh
1860
+ sesh.apply_post_signals()
1861
+ sesh.post_update_system(*args, **kwargs)
1862
+
1863
+ # Logging to class logger
1864
+ @property
1865
+ def identity(self):
1866
+ return f"PROB|{str(self.session_id)[0:5]}"
1867
+
1868
+ @property
1869
+ def log_level(self):
1870
+ return log.log_level
1871
+
1872
+ def msg(self, msg, *a, **kw):
1873
+ if log.log_level < 5:
1874
+ log.msg(
1875
+ f"{self.identity}|[{self.level_number}-{self.level_name}] {msg}",
1876
+ *a,
1877
+ **kw,
1878
+ )
1879
+
1880
+ def debug(self, msg, *a, **kw):
1881
+ if log.log_level <= 15:
1882
+ log.debug(
1883
+ f"{self.identity}|[{self.level_number}-{self.level_name}] {msg}",
1884
+ *a,
1885
+ **kw,
1886
+ )
1887
+
1888
+ def warning(self, msg, *a, **kw):
1889
+ log.warning(
1890
+ f"{self.identity}|[{self.level_number}-{self.level_name}] {msg}",
1891
+ *a,
1892
+ **kw,
1893
+ )
1894
+
1895
+ def info(self, msg, *a, **kw):
1896
+ log.info(
1897
+ f"{self.identity}|[{self.level_number}-{self.level_name}] {msg}",
1898
+ *a,
1899
+ **kw,
1900
+ )
1901
+
1902
+ def error(self, error, msg, *a, **kw):
1903
+ log.error(
1904
+ error,
1905
+ f"{self.identity}|[{self.level_number}-{self.level_name}] {msg}",
1906
+ *a,
1907
+ **kw,
1908
+ )
1909
+
1910
+ def critical(self, msg, *a, **kw):
1911
+ log.critical(
1912
+ f"{self.identity}|[{self.level_number}-{self.level_name}] {msg}",
1913
+ *a,
1914
+ **kw,
1915
+ )
1916
+
1917
+ # Safe Access Methods
1918
+ @property
1919
+ def ref_attrs(self):
1920
+ return self.sesh._sys_refs.get("attrs", {}).copy()
1921
+
1922
+ @property
1923
+ def attr_inst(self):
1924
+ return self.sesh._sys_refs.get("type", {}).copy()
1925
+
1926
+ @property
1927
+ def dynamic_comps(self):
1928
+ return self.sesh._sys_refs.get("dynamic_comps", {}).copy()
1929
+
1930
+ # Instances
1931
+ @property
1932
+ def integrators(self):
1933
+ return self.attr_inst.get("time", {}).copy()
1934
+
1935
+ @property
1936
+ def signal_inst(self):
1937
+ return self.attr_inst.get("signal", {}).copy()
1938
+
1939
+ @property
1940
+ def solver_inst(self):
1941
+ return self.attr_inst.get("solver", {}).copy()
1942
+
1943
+ @property
1944
+ def kwargs(self):
1945
+ """copy of slv_kw args"""
1946
+ return self.sesh._slv_kw.copy()
1947
+
1948
+ @property
1949
+ def dynamic_state(self):
1950
+ return self.ref_attrs.get("dynamics.state", {}).copy()
1951
+
1952
+ @property
1953
+ def dynamic_rate(self):
1954
+ return self.ref_attrs.get("dynamics.rate", {}).copy()
1955
+
1956
+ @property
1957
+ def problem_input(self):
1958
+ return self.ref_attrs.get("dynamics.input", {}).copy()
1959
+
1960
+ @property
1961
+ def integrator_vars(self):
1962
+ return self.ref_attrs.get("time.var", {}).copy()
1963
+
1964
+ @property
1965
+ def integrator_rates(self):
1966
+ return self.ref_attrs.get("time.rate", {}).copy()
1967
+
1968
+ # Y solver variables
1969
+ @property
1970
+ def problem_objs(self):
1971
+ return self.ref_attrs.get("solver.obj", {}).copy()
1972
+
1973
+ @property
1974
+ def problem_eq(self):
1975
+ base_eq = self.ref_attrs.get("solver.eq", {}).copy()
1976
+ if self._dxdt in [True, None] or self._dxdt is False:
1977
+ return base_eq # wysiwyg
1978
+ base_eq.update(self.dynamic_rate_eq)
1979
+
1980
+ if len(base_eq) > len(self.Xref):
1981
+ self.warning(
1982
+ f"problem_eq has more items than variables {base_eq} {self.Xref}"
1983
+ )
1984
+ # TODO: in this case combine the rate_eq to prevent this error
1985
+
1986
+ return base_eq
1987
+
1988
+ @property
1989
+ def dynamic_rate_eq(self):
1990
+ # Add Dynamics Rate Equalities
1991
+ base_eq = {}
1992
+ if self._dxdt in [True, None] or self._dxdt is False: # zero case
1993
+ return base_eq
1994
+
1995
+ Xrefs = self.Xref
1996
+ system = self.system
1997
+ # henceforth, the rate shall be equal to somethign!
1998
+ base_eq.update(self.dynamic_rate)
1999
+ base_eq.update(self.filter_vars(self.integrator_rates))
2000
+ out = {}
2001
+ # TODO: handle callable case
2002
+ if self._dxdt is not None and (
2003
+ isinstance(self._dxdt, dict) or abs(self._dxdt) > 0
2004
+ ):
2005
+ # add in the dynamic rate constraints
2006
+ for var, varref in base_eq.items():
2007
+ if isinstance(self._dxdt, dict) and var in self._dxdt:
2008
+ rate_val = self._dxdt.get(var, 0) # zero default rate
2009
+ elif isinstance(self._dxdt, (int, float)):
2010
+ rate_val = self._dxdt
2011
+ con_ref = ref_to_val_constraint(
2012
+ system,
2013
+ varref.comp,
2014
+ system.last_context,
2015
+ Xrefs,
2016
+ varref,
2017
+ "min",
2018
+ rate_val,
2019
+ return_ref=True,
2020
+ )
2021
+ out[var] = con_ref
2022
+ else:
2023
+ out[var] = varref # zip rate
2024
+ else:
2025
+ # thou shalt be zero (no ref modifications)
2026
+ out = base_eq
2027
+
2028
+ return out
2029
+
2030
+ @property
2031
+ def problem_ineq(self):
2032
+ return self.ref_attrs.get("solver.ineq", {}).copy()
2033
+
2034
+ @property
2035
+ def signals_source(self):
2036
+ return self.ref_attrs.get("signal.source", {}).copy()
2037
+
2038
+ @property
2039
+ def signals_target(self):
2040
+ return self.ref_attrs.get("signal.target", {}).copy()
2041
+
2042
+ @property
2043
+ def signals(self):
2044
+ return self.ref_attrs.get("signal.signal", {}).copy()
2045
+
2046
+ # formatted output
2047
+ @property
2048
+ def is_active(self):
2049
+ """checks if the context has been entered and not exited"""
2050
+ return self.entered and not self.exited
2051
+
2052
+ @classmethod
2053
+ def cls_is_active(cls):
2054
+ """checks if the cache has a session"""
2055
+ if cls.class_cache and hasattr(cls.class_cache, "session"):
2056
+ return True
2057
+ return False
2058
+
2059
+ @property
2060
+ def solveable(self):
2061
+ """checks the system's references to determine if its solveabl"""
2062
+ if self.sesh.problem_opt_vars:
2063
+ # TODO: expand this
2064
+ return True
2065
+ return False
2066
+
2067
+ @property
2068
+ def integrator_rate_refs(self):
2069
+ """combine the dynamic state and the integrator rates to get the transient state of the system, but convert their keys to the target var names"""
2070
+ dc = self.dynamic_state.copy()
2071
+ for int_name, intinst in self.integrators.items():
2072
+ if intinst.var in dc:
2073
+ raise KeyError(
2074
+ f"conflict with integrator name {intinst.var} and dynamic state"
2075
+ )
2076
+ dc.update({intinst.var: intinst.rate_ref})
2077
+ return dc
2078
+
2079
+ @property
2080
+ def integrator_var_refs(self):
2081
+ """combine the dynamic state and the integrator rates to get the transient state of the system, but convert their keys to the target var names"""
2082
+ dc = self.dynamic_state.copy()
2083
+ for int_name, intinst in self.integrators.items():
2084
+ if intinst.var_ref in dc:
2085
+ raise KeyError(
2086
+ f"conflict with integrator name {intinst.var_ref} and dynamic state"
2087
+ )
2088
+ dc.update({intinst.var: intinst.var_ref})
2089
+ return dc
2090
+
2091
+ # Dataframe support
2092
+
2093
+ @property
2094
+ def numeric_data(self):
2095
+ """return a list of sorted data rows by item and filter each row to remove invalid data"""
2096
+ sesh = self.sesh
2097
+ filter_non_numeric = lambda kv: (
2098
+ False if isinstance(kv[1], (list, dict, tuple)) else True
2099
+ )
2100
+ f_numrow = lambda in_dict: dict(filter(filter_non_numeric, in_dict.items()))
2101
+ return [
2102
+ f_numrow(kv[-1]) for kv in sorted(sesh.data.items(), key=lambda kv: kv[0])
2103
+ ]
2104
+
2105
+ @property
2106
+ def dataframe(self) -> pd.DataFrame:
2107
+ """returns the dataframe of the system"""
2108
+ res = pd.DataFrame(self.numeric_data)
2109
+ self.system.format_columns(res)
2110
+ return res
2111
+
2112
+ # TODO: expose optoin for saving all or part of the system information, for now lets default to all (saftey first, then performance :)
2113
+ def get_parent_key(self, key, look_back_num=1):
2114
+ """returns the parent key of the key"""
2115
+ if not key:
2116
+ return ""
2117
+ elif key.count(".") >= look_back_num:
2118
+ return ".".join(key.split(".")[:-look_back_num]) + "."
2119
+ return ""
2120
+
2121
+ # Dynamics Interface
2122
+ def filter_vars(self, refs: list):
2123
+ """selects only settable refs"""
2124
+ return {
2125
+ f"{self.get_parent_key(k)}{v.key}": v
2126
+ for k, v in refs.items()
2127
+ if v.allow_set
2128
+ }
2129
+
2130
+ # X solver variable refs
2131
+ @property
2132
+ def problem_opt_vars(self) -> dict:
2133
+ """solver variables"""
2134
+ return self.ref_attrs.get("solver.var", {}).copy()
2135
+
2136
+ @property
2137
+ def all_problem_vars(self) -> dict:
2138
+ """solver variables + dynamics states when dynamic_solve is True"""
2139
+ varx = self.ref_attrs.get("solver.var", {}).copy()
2140
+ # Add the dynamic states to be optimized (ignore if integrating)
2141
+ sesh = self.sesh
2142
+ if sesh.dynamic_solve and not sesh._dxdt is True:
2143
+ varx.update(sesh.dynamic_state)
2144
+ varx.update(self.filter_vars(sesh.integrator_vars))
2145
+
2146
+ return varx
2147
+
2148
+ @property
2149
+ def dynamic_solve(self) -> bool:
2150
+ """indicates if the system is dynamic"""
2151
+ sesh = self.sesh
2152
+ dxdt = sesh._dxdt
2153
+
2154
+ if dxdt is None or dxdt is False:
2155
+ return False
2156
+
2157
+ if dxdt is True:
2158
+ return True
2159
+
2160
+ in_type = isinstance(dxdt, (dict, float, int))
2161
+ bool_type = isinstance(dxdt, bool) and dxdt == True
2162
+ if in_type or bool_type:
2163
+ return True
2164
+
2165
+ return False
2166
+
2167
+ @property
2168
+ def all_variable_refs(self) -> dict:
2169
+ sesh = self.sesh
2170
+ ing = self.integrator_vars
2171
+ stt = self.dynamic_state
2172
+ vars = self.problem_opt_vars
2173
+ return {**ing, **stt, **vars}
2174
+
2175
+ @property
2176
+ def all_variables(self) -> dict:
2177
+ """returns all variables in the system"""
2178
+ return self.all_refs["attributes"]
2179
+
2180
+ @property
2181
+ def all_comps(self) -> dict:
2182
+ """returns all variables in the system"""
2183
+ return self.all_refs["components"]
2184
+
2185
+ @property
2186
+ def all_components(self) -> dict:
2187
+ """returns all variables in the system"""
2188
+ return self.all_refs["components"]
2189
+
2190
+ @property
2191
+ def all_comps_and_vars(self) -> dict:
2192
+ # TODO: ensure system refes are fresh per system runtime events
2193
+ sesh = self.sesh
2194
+ refs = sesh.all_refs
2195
+ attrs = refs["attributes"].copy()
2196
+ comps = refs["components"].copy()
2197
+ attrs.update(comps)
2198
+ return attrs
2199
+
2200
+ @property
2201
+ def attribute_sys_key_map(self) -> dict:
2202
+ """returns an attribute:key mapping to lookup the key from the attribute"""
2203
+ sesh = self.sesh
2204
+ attrvars = sesh.all_refs["attributes"]
2205
+ comps = {c: k for k, c in self.all_components.items()}
2206
+ return {
2207
+ refget_attr(v): refget_key(v, sesh.system, comps)
2208
+ for k, v in attrvars.items()
2209
+ }
2210
+
2211
+ @property
2212
+ def all_system_references(self) -> dict:
2213
+ sesh = self.sesh
2214
+ refs = sesh.all_refs
2215
+ out = {}
2216
+ out.update(refs["attributes"])
2217
+ out.update(refs["properties"])
2218
+ return out
2219
+
2220
+ def __str__(self):
2221
+ # TODO: expand this
2222
+ return f"ProblemContext[{self.level_name:^12}][{str(self.session_id)[0:8]}-{str(self._problem_id)[0:8]}][{self.system.identity}]"
2223
+
2224
+
2225
+ refget_attr = lambda ref: getattr(ref.comp.__class__.__attrs_attrs__, ref.key)
2226
+ ###FIXME: ref.comp needs to be reliablly in comps
2227
+ refget_key = (
2228
+ lambda ref, slf, comps: f'{comps[ref.comp]+"." if slf != ref.comp else ""}{ref.key}'
2229
+ )
2230
+
2231
+ # TODO: move all system_reference concept inside problem context, remove from system/tabulation ect.
2232
+ # TODO: use prob.register/change(comp,key='') to add components to the problem context, mapping subcomponents to the problem context
2233
+ # TODO: make a graph of all problem dependencies and use that to determine the order of operations, and subsequent updates.
2234
+
2235
+
2236
+ # subclass before altering please!
2237
+ ProblemExec.class_cache = ProblemExec
2238
+
2239
+
2240
+ class Problem(ProblemExec, DataframeMixin):
2241
+ # TODO: implement checks to ensure that problem is defined as the top level context to be returned to
2242
+ # TODO: also define return options for data/system/dataframe and indexing
2243
+ pass
2244
+
2245
+ @property
2246
+ def level_name(self):
2247
+ return "top" # fixed top output, garuntees exit to here.
2248
+
2249
+ @level_name.setter
2250
+ def level_name(self, value):
2251
+ raise AttributeError(f"cannot set level_name of top level problem context")