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
engforge/analysis.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import attr
|
|
2
|
+
from engforge.configuration import forge, Configuration
|
|
3
|
+
from engforge.components import Component
|
|
4
|
+
from engforge.tabulation import TabulationMixin, DataframeMixin
|
|
5
|
+
from engforge.system import System
|
|
6
|
+
from engforge.typing import *
|
|
7
|
+
from engforge.reporting import *
|
|
8
|
+
from engforge.attr_plotting import PlottingMixin
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# import datetime
|
|
12
|
+
import os
|
|
13
|
+
from uuid import uuid4
|
|
14
|
+
|
|
15
|
+
import random
|
|
16
|
+
import attrs
|
|
17
|
+
|
|
18
|
+
from contextlib import contextmanager
|
|
19
|
+
import inspect
|
|
20
|
+
|
|
21
|
+
import matplotlib.pylab as pylab
|
|
22
|
+
|
|
23
|
+
list_check = attrs.validators.instance_of(list)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def make_reporter_check(type_to_check):
|
|
27
|
+
def reporter_type_check(inst, attr, value):
|
|
28
|
+
if not isinstance(value, list):
|
|
29
|
+
raise ValueError("must be a list!")
|
|
30
|
+
|
|
31
|
+
if not all([isinstance(v, type_to_check) for v in value]):
|
|
32
|
+
raise ValueError("must be a list of type {value.__name__}!")
|
|
33
|
+
|
|
34
|
+
return reporter_type_check
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@forge
|
|
38
|
+
class Analysis(Configuration, TabulationMixin, PlottingMixin, DataframeMixin):
|
|
39
|
+
"""Analysis takes a system and many reporters, runs the system, adds its own system properties to the dataframe and post processes the results
|
|
40
|
+
|
|
41
|
+
make_plots() makes plots from the analysis, and stores figure
|
|
42
|
+
post_process() but can be overriden
|
|
43
|
+
report_results() writes to reporters
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
# TODO: generate pdf with tables ect.
|
|
47
|
+
|
|
48
|
+
system: System = attrs.field()
|
|
49
|
+
table_reporters: list = attrs.field(
|
|
50
|
+
factory=list, validator=make_reporter_check(TableReporter)
|
|
51
|
+
)
|
|
52
|
+
plot_reporters: list = attrs.field(
|
|
53
|
+
factory=list, validator=make_reporter_check(PlotReporter)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
_stored_plots: dict = attrs.field(factory=dict)
|
|
57
|
+
_uploaded = False
|
|
58
|
+
|
|
59
|
+
show_plots: bool = attrs.field(default=True)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def uploaded(self):
|
|
63
|
+
return self._uploaded
|
|
64
|
+
|
|
65
|
+
def run(self, *args, **kwargs):
|
|
66
|
+
"""Analysis.run() passes inputs to the assigned system and saves data via the system.run(cb=callback), once complete `Analysis.post_process()` is run also being passed input arguments, then plots & reports are made"""
|
|
67
|
+
self.info(f"running analysis {self.identity} with input {args} {kwargs}")
|
|
68
|
+
cb = lambda *args, **kw: self.system.last_context.save_data(force=True)
|
|
69
|
+
out = self.system.run(*args, **kwargs, cb=cb)
|
|
70
|
+
self.post_process(*args, **kwargs)
|
|
71
|
+
|
|
72
|
+
self._stored_plots = {}
|
|
73
|
+
self.make_plots(analysis=self, store_figures=self._stored_plots)
|
|
74
|
+
self.report_results()
|
|
75
|
+
|
|
76
|
+
def post_process(self, *args, **kwargs):
|
|
77
|
+
"""A user customizeable function"""
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
def report_results(self):
|
|
81
|
+
self.info(f"report results")
|
|
82
|
+
|
|
83
|
+
for tbl_reporter in self.table_reporters:
|
|
84
|
+
try:
|
|
85
|
+
tbl_reporter.upload(self)
|
|
86
|
+
except Exception as e:
|
|
87
|
+
self.error(e, "issue in {tbl_reporter}")
|
|
88
|
+
|
|
89
|
+
for plt_reporter in self.plot_reporters:
|
|
90
|
+
try:
|
|
91
|
+
plt_reporter.upload(self)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
self.error(e, "issue in {plt_reporter}")
|
|
94
|
+
|
|
95
|
+
if self.show_plots:
|
|
96
|
+
self.info(f"showing plots {len(self.stored_plots)}")
|
|
97
|
+
for figkey, fig in self.stored_plots.items():
|
|
98
|
+
self.info(f"showing {figkey}")
|
|
99
|
+
try:
|
|
100
|
+
fig.show()
|
|
101
|
+
except Exception as e:
|
|
102
|
+
self.error(e, f"issue showing {figkey}")
|
|
103
|
+
|
|
104
|
+
self._uploaded = True
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def dataframe(self):
|
|
108
|
+
# TODO: join with analysis dataframe
|
|
109
|
+
return self.system.dataframe
|
|
110
|
+
|
|
111
|
+
# Plotting & Report Methods:
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# @property
|
|
115
|
+
# def _report_path(self):
|
|
116
|
+
# """Add some name options that work into ClientInfoMixin"""
|
|
117
|
+
#
|
|
118
|
+
# if (
|
|
119
|
+
# self.namepath_mode == "both"
|
|
120
|
+
# and self.mode == "iterator"
|
|
121
|
+
# and isinstance(self.component_iterator, Component)
|
|
122
|
+
# ):
|
|
123
|
+
# return os.path.join(
|
|
124
|
+
# self.namepath_root,
|
|
125
|
+
# f"{self.name}",
|
|
126
|
+
# f"{self.component_iterator.name}",
|
|
127
|
+
# )
|
|
128
|
+
# elif (
|
|
129
|
+
# self.namepath_mode == "iterator"
|
|
130
|
+
# and self.mode == "iterator"
|
|
131
|
+
# and isinstance(self.component_iterator, Component)
|
|
132
|
+
# ):
|
|
133
|
+
# return os.path.join(
|
|
134
|
+
# self.namepath_root, f"{self.component_iterator.name}"
|
|
135
|
+
# )
|
|
136
|
+
# else:
|
|
137
|
+
# if self.name != "default":
|
|
138
|
+
# return os.path.join(
|
|
139
|
+
# self.namepath_root, f"{self.classname.lower()}_{self.name}"
|
|
140
|
+
# )
|
|
141
|
+
# return os.path.join(self.namepath_root, f"{self.classname.lower()}")
|
|
142
|
+
|
|
143
|
+
# @property
|
|
144
|
+
# def component_iterator(self) -> ComponentIterator:
|
|
145
|
+
# """Override me!"""
|
|
146
|
+
# return self.iterator
|
|
147
|
+
|
|
148
|
+
# def post_process(self):
|
|
149
|
+
# """override me!"""
|
|
150
|
+
# pass
|
|
151
|
+
#
|
|
152
|
+
# def reset_analysis(self):
|
|
153
|
+
# self.reset_data()
|
|
154
|
+
# self._solved = False
|
|
155
|
+
# self.run_id = None
|
|
156
|
+
|
|
157
|
+
# def gsync_results(self, filename="Analysis", meta_tags=None):
|
|
158
|
+
# """Syncs All Variable Tables To The Cloud"""
|
|
159
|
+
# with self.drive.context(
|
|
160
|
+
# filepath_root=self.local_sync_path, sync_root=self.cloud_sync_path
|
|
161
|
+
# ) as gdrive:
|
|
162
|
+
# with self.drive.rate_limit_manager(
|
|
163
|
+
# self.gsync_results, 6, filename=filename, meta_tags=meta_tags
|
|
164
|
+
# ):
|
|
165
|
+
# old_sleep = gdrive._sleep_time
|
|
166
|
+
# gdrive.reset_sleep_time(max(old_sleep, 1.0))
|
|
167
|
+
#
|
|
168
|
+
# gpath = gdrive.sync_path(self.local_sync_path)
|
|
169
|
+
#
|
|
170
|
+
# self.debug(f"saving as gsheets {gpath}")
|
|
171
|
+
# parent_id = gdrive.get_gpath_id(gpath)
|
|
172
|
+
# # TODO: delete old file if exists
|
|
173
|
+
#
|
|
174
|
+
# gdrive.sleep(12 * random.random())
|
|
175
|
+
#
|
|
176
|
+
# gdrive.cache_directory(parent_id)
|
|
177
|
+
# gdrive.sleep()
|
|
178
|
+
#
|
|
179
|
+
# # Remove items with same name in parent dir
|
|
180
|
+
# parent = gdrive.item_nodes[parent_id]
|
|
181
|
+
# parent.remove_contents_with_title(filename)
|
|
182
|
+
#
|
|
183
|
+
# df = self.joined_dataframe
|
|
184
|
+
#
|
|
185
|
+
# # Make the new sheet
|
|
186
|
+
# sht = gdrive.gsheets.create(filename, folder=parent_id)
|
|
187
|
+
# gdrive.sleep(2 * (1 + gdrive.time_fuzz * random.random()))
|
|
188
|
+
#
|
|
189
|
+
# wk = sht.add_worksheet(filename)
|
|
190
|
+
# gdrive.sleep(2 * (1 + gdrive.time_fuzz * random.random()))
|
|
191
|
+
#
|
|
192
|
+
# wk.rows = df.shape[0]
|
|
193
|
+
# gdrive.sleep(2 * (1 + gdrive.time_fuzz * random.random()))
|
|
194
|
+
#
|
|
195
|
+
# wk.set_dataframe(df, start="A1", fit=True)
|
|
196
|
+
# gdrive.sleep(2 * (1 + gdrive.time_fuzz * random.random()))
|
|
197
|
+
#
|
|
198
|
+
# for df_result in self.variable_tables:
|
|
199
|
+
# df = df_result["df"]
|
|
200
|
+
# conf = df_result["conf"]
|
|
201
|
+
#
|
|
202
|
+
# if meta_tags is not None and type(meta_tags) is dict:
|
|
203
|
+
# for tag, value in meta_tags.items():
|
|
204
|
+
# df[tag] = value
|
|
205
|
+
#
|
|
206
|
+
# gdrive.sleep(2 * (1 + gdrive.time_fuzz * random.random()))
|
|
207
|
+
# wk = sht.add_worksheet(conf.displayname)
|
|
208
|
+
# gdrive.sleep(2 * (1 + gdrive.time_fuzz * random.random()))
|
|
209
|
+
#
|
|
210
|
+
# wk.rows = df.shape[0]
|
|
211
|
+
# gdrive.sleep(2 * (1 + gdrive.time_fuzz * random.random()))
|
|
212
|
+
#
|
|
213
|
+
# wk.set_dataframe(df, start="A1", fit=True)
|
|
214
|
+
# gdrive.sleep(2 * (1 + gdrive.time_fuzz * random.random()))
|
|
215
|
+
#
|
|
216
|
+
# sht.del_worksheet(sht.sheet1)
|
|
217
|
+
# gdrive.sleep(2 * (1 + gdrive.time_fuzz * random.random()))
|
|
218
|
+
#
|
|
219
|
+
# # TODO: add in dataframe dict with schema sheename: {dataframe,**other_args}
|
|
220
|
+
# self.info(
|
|
221
|
+
# "gsheet saved -> {}".format(os.path.join(gpath, filename))
|
|
222
|
+
# )
|
|
223
|
+
#
|
|
224
|
+
# gdrive.reset_sleep_time(old_sleep)
|
|
225
|
+
|
|
226
|
+
# @property
|
|
227
|
+
# def columns(self):
|
|
228
|
+
# if self.solved:
|
|
229
|
+
# return list(self.joined_dataframe)
|
|
230
|
+
# else:
|
|
231
|
+
# return []
|
|
232
|
+
#
|
|
233
|
+
# def plot(self, x, y, kind="line", **kwargs):
|
|
234
|
+
# """
|
|
235
|
+
# A wrapper for pandas dataframe.plot
|
|
236
|
+
# :param grid: set True if input is not False
|
|
237
|
+
# """
|
|
238
|
+
#
|
|
239
|
+
# # TODO: Add a default x iterator for what is iterated in analysis!
|
|
240
|
+
#
|
|
241
|
+
# if "grid" not in kwargs:
|
|
242
|
+
# kwargs["grid"] = True
|
|
243
|
+
#
|
|
244
|
+
# if isinstance(y, (list, tuple)):
|
|
245
|
+
# old_y = set(y)
|
|
246
|
+
# y = list([yval for yval in y if yval in self.dataframe.columns])
|
|
247
|
+
# rmv_y = set.difference(old_y, set(y))
|
|
248
|
+
# if rmv_y:
|
|
249
|
+
# self.warning(f"vars not found: {rmv_y}")
|
|
250
|
+
#
|
|
251
|
+
# if self.solved and y:
|
|
252
|
+
# df = self.dataframe
|
|
253
|
+
# return df.plot(x=x, y=y, kind=kind, **kwargs)
|
|
254
|
+
# elif y:
|
|
255
|
+
# self.warning("not solved yet!")
|
|
256
|
+
# elif self.solved:
|
|
257
|
+
# self.warning("bad input!")
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
from engforge.attributes import ATTR_BASE, AttributeInstance
|
|
2
|
+
|
|
3
|
+
import attrs, attr, uuid
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Instance & Attribute definition for integration vars
|
|
7
|
+
# Solver minimizes residual by changing independents
|
|
8
|
+
class IntegratorInstance(AttributeInstance):
|
|
9
|
+
"""A decoupled signal instance to perform operations on a system instance"""
|
|
10
|
+
|
|
11
|
+
system: "System"
|
|
12
|
+
|
|
13
|
+
# compiled info
|
|
14
|
+
var_ref: "Ref"
|
|
15
|
+
rate_ref: "Ref"
|
|
16
|
+
|
|
17
|
+
__slots__ = ["system", "solver", "var_ref", "rate_ref"]
|
|
18
|
+
|
|
19
|
+
def __init__(self, solver: "Time", system: "System") -> None:
|
|
20
|
+
self.class_attr = self.solver = solver
|
|
21
|
+
self.system = system
|
|
22
|
+
self.compile()
|
|
23
|
+
|
|
24
|
+
def compile(self):
|
|
25
|
+
self.var_ref = self.system.locate_ref(self.solver.var)
|
|
26
|
+
self.rate_ref = self.system.locate_ref(self.solver.rate)
|
|
27
|
+
self.system.debug(f"integrating {self.var_ref} with {self.rate_ref}")
|
|
28
|
+
|
|
29
|
+
def as_ref_dict(self):
|
|
30
|
+
return {"rate": self.rate_ref, "var": self.var_ref}
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def rates(self):
|
|
34
|
+
return {self.name: self.class_attr.rate}
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def integrated(self):
|
|
38
|
+
return {self.name: self.class_attr.var}
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def var(self):
|
|
42
|
+
return self.class_attr.var
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def rate(self):
|
|
46
|
+
return self.class_attr.rate
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def current_rate(self):
|
|
50
|
+
return self.rate_ref.value(self.system, self.system.last_context)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def constraint_refs(self):
|
|
54
|
+
if self.solver.slvtype in ["eq", "ineq"]:
|
|
55
|
+
return self.const_f
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def constraints(self):
|
|
60
|
+
if hasattr(self, "_constraints"):
|
|
61
|
+
return self._constraints
|
|
62
|
+
|
|
63
|
+
return self.solver.constraints
|
|
64
|
+
|
|
65
|
+
# return [{'type':self.slvtype,'var':self.solver.lhs,'value':self.const_f}]
|
|
66
|
+
# if self.solver.slvtype in ['var','obj']:
|
|
67
|
+
# return self.solver.constraints
|
|
68
|
+
# else:
|
|
69
|
+
# just me
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def slvtype(self):
|
|
73
|
+
return self.solver.slvtype
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def combos(self):
|
|
77
|
+
return self.solver.combos
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def normalize(self):
|
|
81
|
+
return self.solver.normalize
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def active(self):
|
|
85
|
+
if hasattr(self, "_active"):
|
|
86
|
+
return self._active
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# TODO: depriciate modes and update for dynamicmixin strategies
|
|
90
|
+
# TODO: add Time.add_profile(parm, time:parameter_values, combos,active) to add a transient profile to be run on the system that is selectable by the user
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Time(ATTR_BASE):
|
|
94
|
+
"""Transient is a base class for integrators over time"""
|
|
95
|
+
|
|
96
|
+
mode: str
|
|
97
|
+
var: str
|
|
98
|
+
rate: str
|
|
99
|
+
constraints: list
|
|
100
|
+
allow_constraint_override: bool = True
|
|
101
|
+
instance_class = IntegratorInstance
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def integrate(
|
|
105
|
+
cls,
|
|
106
|
+
var: str,
|
|
107
|
+
rate: "system_property",
|
|
108
|
+
mode: str = "euler",
|
|
109
|
+
active=True,
|
|
110
|
+
combos="default",
|
|
111
|
+
):
|
|
112
|
+
"""Defines an ODE like integrator that will be integrated over time with the defined integration rule.
|
|
113
|
+
|
|
114
|
+
Input should be of strings to look up the particular property or field
|
|
115
|
+
"""
|
|
116
|
+
# Create A New Signals Class
|
|
117
|
+
new_name = f"TRANSIENT_{mode}_{var}_{rate}".replace(".", "_")
|
|
118
|
+
# new_name = new_name
|
|
119
|
+
new_dict = dict(
|
|
120
|
+
mode=mode,
|
|
121
|
+
name=new_name,
|
|
122
|
+
var=var,
|
|
123
|
+
rate=rate,
|
|
124
|
+
active=active,
|
|
125
|
+
constraints=[], # TODO: parse kwargs for limits
|
|
126
|
+
combos=cls.process_combos(combos, add_combos=f"time,{var}"),
|
|
127
|
+
)
|
|
128
|
+
return cls._setup_cls(new_name, new_dict)
|
|
129
|
+
|
|
130
|
+
# make define the same as integrate
|
|
131
|
+
# @classmethod
|
|
132
|
+
# def subcls_compile(cls,**kwargs):
|
|
133
|
+
# cls.define = cls.integrate
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def class_validate(cls, instance, **kwargs):
|
|
137
|
+
from engforge.properties import system_property
|
|
138
|
+
from engforge.solver import SolveableMixin
|
|
139
|
+
|
|
140
|
+
system = cls.config_cls
|
|
141
|
+
assert issubclass(system, SolveableMixin), f"must be a solveable system"
|
|
142
|
+
|
|
143
|
+
var_type = instance.locate(cls.var)
|
|
144
|
+
if var_type is None:
|
|
145
|
+
raise Exception(f"var not found: {cls.var}")
|
|
146
|
+
assert isinstance(
|
|
147
|
+
var_type, attrs.Attribute
|
|
148
|
+
), f"bad var {cls.var} not attribute: {var_type}"
|
|
149
|
+
assert var_type.type in (
|
|
150
|
+
int,
|
|
151
|
+
float,
|
|
152
|
+
), f"bad var {cls.var} not numeric"
|
|
153
|
+
|
|
154
|
+
driv_type = instance.locate(cls.rate)
|
|
155
|
+
if driv_type is None:
|
|
156
|
+
raise Exception(f"rate not found: {cls.rate}")
|
|
157
|
+
assert isinstance(
|
|
158
|
+
driv_type, (system_property, attrs.Attribute)
|
|
159
|
+
), f"bad rate {cls.rate} type: {driv_type}"
|
|
160
|
+
if isinstance(driv_type, system_property):
|
|
161
|
+
assert driv_type.return_type in (
|
|
162
|
+
int,
|
|
163
|
+
float,
|
|
164
|
+
), f"bad var {cls.rate} not numeric"
|
|
165
|
+
|
|
166
|
+
# else: attributes are not checked, youre in command
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def add_var_constraint(cls, value, kind="min", **kwargs):
|
|
170
|
+
"""adds a `type` constraint to the solver. If value is numeric it is used as a bound with `scipy` optimize.
|
|
171
|
+
|
|
172
|
+
:param type: str, must be either min or max with var value comparison, or with a function additionally eq,ineq (same as max(0)) can be used
|
|
173
|
+
:value: either a numeric (int,float), or a function, f(system)
|
|
174
|
+
"""
|
|
175
|
+
assert cls is not Time, f"must set constraint on Time Attribute"
|
|
176
|
+
# assert not cls.constraint_exists(type=kind,var=var), f"constraint already exists!"
|
|
177
|
+
assert isinstance(value, (int, float)) or callable(
|
|
178
|
+
value
|
|
179
|
+
), f"only int,float or callables allow. Callables must take system as argument"
|
|
180
|
+
|
|
181
|
+
var = cls.var
|
|
182
|
+
assert var is not None, "must provide var on non-var solvers"
|
|
183
|
+
assert kind in ("min", "max")
|
|
184
|
+
combo_dflt = "default,lim"
|
|
185
|
+
cmbs = kwargs.get("combos", "")
|
|
186
|
+
combos = cls.process_combos(cmbs, combo_dflt, combo_dflt)
|
|
187
|
+
if isinstance(combos, str):
|
|
188
|
+
combos = combos.split(",")
|
|
189
|
+
active = kwargs.get("active", True)
|
|
190
|
+
const = {
|
|
191
|
+
"type": kind,
|
|
192
|
+
"value": value,
|
|
193
|
+
"var": var,
|
|
194
|
+
"active": active,
|
|
195
|
+
"combos": combos,
|
|
196
|
+
"combo_var": cls.name,
|
|
197
|
+
}
|
|
198
|
+
# print(const,cls.__dict__)
|
|
199
|
+
|
|
200
|
+
cinx = cls.constraint_exists(type=kind, var=var)
|
|
201
|
+
inix = cinx is not None
|
|
202
|
+
if cls.allow_constraint_override and inix:
|
|
203
|
+
# print(f'replacing constraint {cinx} with {kind} {value} {var}')
|
|
204
|
+
constraint = cls.constraints[cinx]
|
|
205
|
+
constraint.update(const)
|
|
206
|
+
elif not cls.allow_constraint_override and inix:
|
|
207
|
+
cnx = cls.constraints[cinx]
|
|
208
|
+
raise Exception(f"constraint already exists! {cnx}")
|
|
209
|
+
else:
|
|
210
|
+
cls.constraints.append(const)
|
|
211
|
+
|
|
212
|
+
@classmethod
|
|
213
|
+
def constraint_exists(cls, **kw):
|
|
214
|
+
"""check constraints on the system, return its index position if found, else None."""
|
|
215
|
+
for i, c in enumerate(cls.constraints):
|
|
216
|
+
if all([c[k] == v for k, v in kw.items()]):
|
|
217
|
+
return i
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# Support Previous API
|
|
222
|
+
TRANSIENT = Time
|