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.
- engforge/__init__.py +26 -0
- engforge/_testing_components.py +864 -0
- engforge/analysis.py +257 -0
- engforge/attr_dynamics.py +222 -0
- engforge/attr_plotting.py +811 -0
- engforge/attr_signals.py +116 -0
- engforge/attr_slots.py +211 -0
- engforge/attr_solver.py +443 -0
- engforge/attributes.py +326 -0
- engforge/common.py +76 -0
- engforge/component_collections.py +276 -0
- engforge/components.py +55 -0
- engforge/configuration.py +673 -0
- engforge/dataframe.py +185 -0
- engforge/datastores/__init__.py +116 -0
- engforge/datastores/data.py +519 -0
- engforge/datastores/gdocs.py +2650 -0
- engforge/datastores/reporting.py +935 -0
- engforge/datastores/secrets.py +549 -0
- engforge/dynamics.py +662 -0
- engforge/eng/__init__.py +0 -0
- engforge/eng/costs.py +1459 -0
- engforge/eng/fluid_material.py +279 -0
- engforge/eng/geometry.py +1060 -0
- engforge/eng/pipes.py +1211 -0
- engforge/eng/prediction.py +366 -0
- engforge/eng/solid_materials.py +445 -0
- engforge/eng/structure.py +2228 -0
- engforge/eng/structure_beams.py +821 -0
- engforge/eng/thermodynamics.py +271 -0
- engforge/engforge_attributes.py +380 -0
- engforge/env_var.py +214 -0
- engforge/locations.py +18 -0
- engforge/logging.py +290 -0
- engforge/patterns.py +449 -0
- engforge/problem_context.py +2251 -0
- engforge/properties.py +501 -0
- engforge/reporting.py +595 -0
- engforge/solveable.py +964 -0
- engforge/solver.py +316 -0
- engforge/solver_utils.py +726 -0
- engforge/system.py +143 -0
- engforge/system_reference.py +299 -0
- engforge/tabulation.py +288 -0
- engforge/test/__init__.py +2 -0
- engforge/test/_pre_test_structures.py +221 -0
- engforge/test/report_testing.py +158 -0
- engforge/test/test_airfilter.py +91 -0
- engforge/test/test_analysis.py +3 -0
- engforge/test/test_comp_iter.py +196 -0
- engforge/test/test_composition.py +139 -0
- engforge/test/test_costs.py +338 -0
- engforge/test/test_dynamics.py +109 -0
- engforge/test/test_dynamics_spaces.py +39 -0
- engforge/test/test_four_bar.py +97 -0
- engforge/test/test_modules.py +72 -0
- engforge/test/test_performance.py +93 -0
- engforge/test/test_pipes.py +49 -0
- engforge/test/test_problem.py +411 -0
- engforge/test/test_problem_deepscoping.py +68 -0
- engforge/test/test_slider_crank.py +51 -0
- engforge/test/test_solver.py +343 -0
- engforge/test/test_tabulation.py +233 -0
- engforge/typing.py +92 -0
- engforge-0.3.0.dist-info/METADATA +392 -0
- engforge-0.3.0.dist-info/RECORD +69 -0
- engforge-0.3.0.dist-info/WHEEL +5 -0
- engforge-0.3.0.dist-info/licenses/LICENSE +7 -0
- 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")
|