engforge 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. engforge/__init__.py +26 -0
  2. engforge/_testing_components.py +864 -0
  3. engforge/analysis.py +257 -0
  4. engforge/attr_dynamics.py +222 -0
  5. engforge/attr_plotting.py +811 -0
  6. engforge/attr_signals.py +116 -0
  7. engforge/attr_slots.py +211 -0
  8. engforge/attr_solver.py +443 -0
  9. engforge/attributes.py +326 -0
  10. engforge/common.py +76 -0
  11. engforge/component_collections.py +276 -0
  12. engforge/components.py +55 -0
  13. engforge/configuration.py +673 -0
  14. engforge/dataframe.py +185 -0
  15. engforge/datastores/__init__.py +116 -0
  16. engforge/datastores/data.py +519 -0
  17. engforge/datastores/gdocs.py +2650 -0
  18. engforge/datastores/reporting.py +935 -0
  19. engforge/datastores/secrets.py +549 -0
  20. engforge/dynamics.py +662 -0
  21. engforge/eng/__init__.py +0 -0
  22. engforge/eng/costs.py +1459 -0
  23. engforge/eng/fluid_material.py +279 -0
  24. engforge/eng/geometry.py +1060 -0
  25. engforge/eng/pipes.py +1211 -0
  26. engforge/eng/prediction.py +366 -0
  27. engforge/eng/solid_materials.py +445 -0
  28. engforge/eng/structure.py +2228 -0
  29. engforge/eng/structure_beams.py +821 -0
  30. engforge/eng/thermodynamics.py +271 -0
  31. engforge/engforge_attributes.py +380 -0
  32. engforge/env_var.py +214 -0
  33. engforge/locations.py +18 -0
  34. engforge/logging.py +290 -0
  35. engforge/patterns.py +449 -0
  36. engforge/problem_context.py +2251 -0
  37. engforge/properties.py +501 -0
  38. engforge/reporting.py +595 -0
  39. engforge/solveable.py +964 -0
  40. engforge/solver.py +316 -0
  41. engforge/solver_utils.py +726 -0
  42. engforge/system.py +143 -0
  43. engforge/system_reference.py +299 -0
  44. engforge/tabulation.py +288 -0
  45. engforge/test/__init__.py +2 -0
  46. engforge/test/_pre_test_structures.py +221 -0
  47. engforge/test/report_testing.py +158 -0
  48. engforge/test/test_airfilter.py +91 -0
  49. engforge/test/test_analysis.py +3 -0
  50. engforge/test/test_comp_iter.py +196 -0
  51. engforge/test/test_composition.py +139 -0
  52. engforge/test/test_costs.py +338 -0
  53. engforge/test/test_dynamics.py +109 -0
  54. engforge/test/test_dynamics_spaces.py +39 -0
  55. engforge/test/test_four_bar.py +97 -0
  56. engforge/test/test_modules.py +72 -0
  57. engforge/test/test_performance.py +93 -0
  58. engforge/test/test_pipes.py +49 -0
  59. engforge/test/test_problem.py +411 -0
  60. engforge/test/test_problem_deepscoping.py +68 -0
  61. engforge/test/test_slider_crank.py +51 -0
  62. engforge/test/test_solver.py +343 -0
  63. engforge/test/test_tabulation.py +233 -0
  64. engforge/typing.py +92 -0
  65. engforge-0.3.0.dist-info/METADATA +392 -0
  66. engforge-0.3.0.dist-info/RECORD +69 -0
  67. engforge-0.3.0.dist-info/WHEEL +5 -0
  68. engforge-0.3.0.dist-info/licenses/LICENSE +7 -0
  69. engforge-0.3.0.dist-info/top_level.txt +1 -0
engforge/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