engforge 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. engforge/__init__.py +26 -0
  2. engforge/_testing_components.py +864 -0
  3. engforge/analysis.py +257 -0
  4. engforge/attr_dynamics.py +222 -0
  5. engforge/attr_plotting.py +811 -0
  6. engforge/attr_signals.py +116 -0
  7. engforge/attr_slots.py +211 -0
  8. engforge/attr_solver.py +443 -0
  9. engforge/attributes.py +326 -0
  10. engforge/common.py +76 -0
  11. engforge/component_collections.py +276 -0
  12. engforge/components.py +55 -0
  13. engforge/configuration.py +673 -0
  14. engforge/dataframe.py +185 -0
  15. engforge/datastores/__init__.py +116 -0
  16. engforge/datastores/data.py +519 -0
  17. engforge/datastores/gdocs.py +2650 -0
  18. engforge/datastores/reporting.py +935 -0
  19. engforge/datastores/secrets.py +549 -0
  20. engforge/dynamics.py +662 -0
  21. engforge/eng/__init__.py +0 -0
  22. engforge/eng/costs.py +1459 -0
  23. engforge/eng/fluid_material.py +279 -0
  24. engforge/eng/geometry.py +1060 -0
  25. engforge/eng/pipes.py +1211 -0
  26. engforge/eng/prediction.py +366 -0
  27. engforge/eng/solid_materials.py +445 -0
  28. engforge/eng/structure.py +2228 -0
  29. engforge/eng/structure_beams.py +821 -0
  30. engforge/eng/thermodynamics.py +271 -0
  31. engforge/engforge_attributes.py +380 -0
  32. engforge/env_var.py +214 -0
  33. engforge/locations.py +18 -0
  34. engforge/logging.py +290 -0
  35. engforge/patterns.py +449 -0
  36. engforge/problem_context.py +2251 -0
  37. engforge/properties.py +501 -0
  38. engforge/reporting.py +595 -0
  39. engforge/solveable.py +964 -0
  40. engforge/solver.py +316 -0
  41. engforge/solver_utils.py +726 -0
  42. engforge/system.py +143 -0
  43. engforge/system_reference.py +299 -0
  44. engforge/tabulation.py +288 -0
  45. engforge/test/__init__.py +2 -0
  46. engforge/test/_pre_test_structures.py +221 -0
  47. engforge/test/report_testing.py +158 -0
  48. engforge/test/test_airfilter.py +91 -0
  49. engforge/test/test_analysis.py +3 -0
  50. engforge/test/test_comp_iter.py +196 -0
  51. engforge/test/test_composition.py +139 -0
  52. engforge/test/test_costs.py +338 -0
  53. engforge/test/test_dynamics.py +109 -0
  54. engforge/test/test_dynamics_spaces.py +39 -0
  55. engforge/test/test_four_bar.py +97 -0
  56. engforge/test/test_modules.py +72 -0
  57. engforge/test/test_performance.py +93 -0
  58. engforge/test/test_pipes.py +49 -0
  59. engforge/test/test_problem.py +411 -0
  60. engforge/test/test_problem_deepscoping.py +68 -0
  61. engforge/test/test_slider_crank.py +51 -0
  62. engforge/test/test_solver.py +343 -0
  63. engforge/test/test_tabulation.py +233 -0
  64. engforge/typing.py +92 -0
  65. engforge-0.3.0.dist-info/METADATA +392 -0
  66. engforge-0.3.0.dist-info/RECORD +69 -0
  67. engforge-0.3.0.dist-info/WHEEL +5 -0
  68. engforge-0.3.0.dist-info/licenses/LICENSE +7 -0
  69. engforge-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,366 @@
1
+ """a module that defines PredictionMixin which uses sklearn to make predictions about results"""
2
+
3
+ # core data
4
+ import numpy
5
+ import numpy as np
6
+ import time
7
+ import pandas
8
+ import random
9
+
10
+ # store average and variance in a dict
11
+ base_stat = {"avg": 0, "var": 0}
12
+
13
+
14
+ class PredictionMixin:
15
+ train_window: int = 5000 # number of data points to use for training
16
+ prediction_goal_error: float = 0.02 # goal error for training 99%
17
+ trained: bool = False
18
+
19
+ _basis: np.ndarray = None # basis for normalizing data
20
+ _prediction_models: dict = (
21
+ None # parm: {'mod':obj,'train_time':float,'N':int,'train_score':scr}
22
+ )
23
+ _prediction_parms: list = None # list of strings to get from dataframe
24
+ _re_train_frac: float = 2 # retrain when N_data > model.N * _re_train_frac
25
+ _re_train_maxiter: int = 50 # max number of retraining iterations
26
+ _running_error: dict = None # parm: {'err':float,'N':int}
27
+ _fitted: bool = False
28
+ _training_history = None
29
+ _do_print = False
30
+
31
+ _sigma_retrain = 3 # retrain when sigma is greater than this
32
+ _X_prediction_stats: list = None # list of strings to get from dataframe
33
+ _prediction_records: list = None # made lazily, but you can replace this
34
+
35
+ @property
36
+ def prediction_records(self):
37
+ if self._prediction_records is None:
38
+ self._prediction_records = []
39
+ return self._prediction_records
40
+
41
+ @property
42
+ def _prediction_record(self):
43
+ raise NotImplementedError(f"must implement _prediction_record")
44
+
45
+ @property
46
+ def basis(self):
47
+ return self._basis
48
+
49
+ def add_prediction_record(
50
+ self, record, extra_add=True, mult_sigma=1, target_items=1000
51
+ ):
52
+ """adds a record to the prediction records, and calcultes the average and variance of the data
53
+ :param record: a dict of the record
54
+ :param extra_add: if true, the record is added to the prediction records even if the data is inbounds
55
+ :returns: a boolean indicating if the record was out of bounds of current data (therefor should be added)
56
+ """
57
+ if self._X_prediction_stats is None:
58
+ self._X_prediction_stats = {
59
+ p: base_stat.copy() for p in self._prediction_parms
60
+ }
61
+
62
+ N = len(self.prediction_records) + 1
63
+ Pf = max((N / target_items), 0.1)
64
+ J = len(self._prediction_parms)
65
+
66
+ # moving average and variance
67
+ out_of_bounds = extra_add # skip this check
68
+ choice = False
69
+ temp_stat = {}
70
+ for i, parm in enumerate(self._prediction_parms):
71
+ rec_val = record[parm]
72
+ cur_stat = self._X_prediction_stats[parm]
73
+ temp_stat[parm] = cur_stat = cur_stat.copy()
74
+ avg = cur_stat["avg"]
75
+ dev = rec_val - avg
76
+ var = cur_stat["var"]
77
+ std = var**0.5
78
+ std_err = std / N
79
+ # check out of bounds
80
+ # probability of acceptance - narrow distributions are not wanted
81
+ near_zero = abs(rec_val) / max(abs(avg), 1) <= 0.005
82
+ if (
83
+ (not choice or not out_of_bounds)
84
+ and avg != 0
85
+ and std != 0
86
+ and not near_zero
87
+ ):
88
+ g = 3 * min((abs(avg) - std) / abs(std), 1) # negative when std > avg
89
+ a = min(abs(dev) / std / J, 1)
90
+ # prob accept should be based on difference from average
91
+ prob_deny = np.e**g
92
+ prob_accept = a / Pf
93
+ std_choice = std
94
+ avg_choice = avg
95
+ dev_choice = dev
96
+ choice = random.choices([True, False], [prob_accept, prob_deny])[0]
97
+ cur_stat["avg"] = avg + dev / N
98
+ cur_stat["var"] = var + (dev**2 - var) / N
99
+
100
+ # observe point and predict to get error
101
+ self.observe_and_predict(record)
102
+
103
+ # update stats if point accepted
104
+ accepted = choice or out_of_bounds or extra_add
105
+ if accepted:
106
+ for parm, cur_stat in temp_stat.items():
107
+ self._X_prediction_stats[parm] = cur_stat # reassign (in case)
108
+
109
+ # fmt null values
110
+ if not choice:
111
+ std_choice = ""
112
+ avg_choice = ""
113
+ dev_choice = ""
114
+ prob_accept = ""
115
+ prob_deny = ""
116
+
117
+ if accepted:
118
+ if self._do_print:
119
+ self.info(
120
+ f"add record | chance: {choice and not extra_add} | {prob_accept:5.3f} | {prob_deny:5.3f} | {std_choice/avg_choice} | {dev_choice/std_choice} | {avg_choice}"
121
+ )
122
+ self._prediction_records.append(record)
123
+
124
+ try:
125
+ self.check_and_retrain(self.prediction_records)
126
+ except Exception as e:
127
+ self.warning(f"error in prediction: {e}")
128
+
129
+ def check_out_of_domain(self, record, extra_margin=1, target_items=1000):
130
+ """checks if the record is in bounds of the current data"""
131
+ if self._X_prediction_stats is None:
132
+ return True
133
+
134
+ if hasattr(self, "max_rec_parm") and self.max_rec_parm in record:
135
+ # we're not counting extemities
136
+ if record[self.max_rec_parm] > self.max_margin:
137
+ return False
138
+
139
+ N = len(self.prediction_records)
140
+ Pf = max((N / target_items), 0.1)
141
+ J = len(self._prediction_parms)
142
+
143
+ for i, parm in enumerate(self._prediction_parms):
144
+ rec_val = record[parm]
145
+ cur_stat = self._X_prediction_stats[parm]
146
+ avg = cur_stat["avg"]
147
+ dev = rec_val - avg
148
+ var = max(cur_stat["var"], 1)
149
+ std = var**0.5
150
+ std_err = std / len(self.prediction_records)
151
+ # check out of bounds
152
+ g = 3 * min((abs(avg) - std) / abs(std), 1) # negative when std > avg
153
+ a = min(abs(dev) / std / J, 1)
154
+ near_zero = abs(rec_val) / max(abs(avg), 1) <= 1e-3
155
+ prob_deny = np.e**g
156
+ prob_accept = a * extra_margin / Pf # relative to prob_deny
157
+ choice = random.choices([True, False], [prob_accept, prob_deny])[0]
158
+ if choice and not near_zero:
159
+ if self._do_print:
160
+ self.info(
161
+ f"record oob chance: {choice} | {prob_accept:5.3f} | {prob_deny:5.3f} | {std/abs(avg)} | {dev/std} | {avg} "
162
+ )
163
+ return True
164
+ return False
165
+
166
+ def train_compare(self, df, test_frac=2, train_full=False, min_rec=250):
167
+ """Use the dataframe to train the models, and compare the results to the current models using `train_frac` to divide total samples into training and testing sets, unless `train_full` is set.
168
+
169
+ :param df: dataframe to train with
170
+ :param test_frac: N/train_frac will be size of the training window
171
+ :param train_full: boolean to use full training data
172
+ :return: trained models
173
+ """
174
+ if self._prediction_models is None or self._basis is None:
175
+ return {}
176
+
177
+ if self._training_history is None:
178
+ train_iter = {}
179
+ self._training_history = []
180
+ else:
181
+ train_iter = {}
182
+
183
+ out = {}
184
+ N = len(df)
185
+ window = self.train_window
186
+ if window > (N / test_frac):
187
+ window = max(int(N / test_frac), 25)
188
+
189
+ MargRec = max(min_rec / len(self.prediction_records), 1)
190
+
191
+ self.info(f"training dataset: {N} | training window: {window}")
192
+ for parm, mod_dict in self._prediction_models.items():
193
+ mod = mod_dict["mod"]
194
+ X = df[self._prediction_parms] / self._basis
195
+ y = df[parm]
196
+ N = len(y)
197
+
198
+ stl = time.time()
199
+ weights = self.prediction_weights(df, N)
200
+ if train_full:
201
+ mod.fit(X, y, weights)
202
+ etl = time.time()
203
+ scr = mod.score(X, y, weights) / MargRec
204
+ train_iter[parm] = {"scr": scr, "time": etl - stl, "N": N}
205
+ else:
206
+ mod.fit(*self._subsample_data(X, y, window, weights))
207
+ etl = time.time()
208
+ scr = mod.score(*self._score_data(X, y, weights)) / MargRec
209
+ train_iter[parm] = {"scr": scr, "time": etl - stl, "N": N}
210
+
211
+ dt = etl - stl
212
+
213
+ self.info(
214
+ f"Prediction: {mod.__class__.__name__}| Score[{parm}] = {scr*100:3.5}% | Training Time: {dt}s"
215
+ )
216
+
217
+ out[parm] = {
218
+ "mod": mod,
219
+ "train_time": dt,
220
+ "N": N,
221
+ "train_score": scr,
222
+ }
223
+
224
+ self._running_error = {
225
+ parm: {"err": 0.5, "N": 0} for parm in self._prediction_models
226
+ }
227
+ self._prediction_models = out
228
+ self._fitted = True
229
+
230
+ # add training item to history
231
+ self._training_history.append(train_iter)
232
+
233
+ # do something when trained
234
+ self.training_callback(self._prediction_models)
235
+
236
+ return out
237
+
238
+ def training_callback(self, models):
239
+ """override to provide a callback when training is complete, such as saving the models"""
240
+ pass
241
+
242
+ def prediction_weights(self, df, window):
243
+ return np.ones(min(len(df), window))
244
+
245
+ def _subsample_data(self, X, y, window, weights):
246
+ """subsamples the data to the window size"""
247
+ return X.iloc[:window], y.iloc[:window], weights[:window]
248
+
249
+ def _score_data(self, X, y, weights):
250
+ """override this, by default, just returns the data to the score"""
251
+ return X, y, weights
252
+
253
+ def score_data(self, df):
254
+ """scores a dataframe"""
255
+
256
+ if self._prediction_models is None:
257
+ return
258
+
259
+ if not self._fitted:
260
+ return
261
+
262
+ train_iter = {}
263
+ self._training_history.append(train_iter)
264
+
265
+ scores = {}
266
+ for parm, mod_dict in self._prediction_models.items():
267
+ model = mod_dict["mod"]
268
+ X = df[self._prediction_parms] / self._basis
269
+ y = df[parm]
270
+ scr = model.score(X, y)
271
+ scores[parm] = scr
272
+ train_iter[parm] = {"scr": scr, "time": np.nan, "N": len(y)}
273
+ if self._do_print:
274
+ self.info(
275
+ f"Prediction: {model.__class__.__name__}| Score[{parm}] = {scr*100:3.5}%"
276
+ )
277
+ else:
278
+ self.debug(
279
+ f"Prediction: {model.__class__.__name__}| Score[{parm}] = {scr*100:3.5}%"
280
+ )
281
+
282
+ return scores
283
+
284
+ def observe_and_predict(self, row):
285
+ """uses the existing models to predict the row and measure the error"""
286
+ if self._prediction_models is None:
287
+ # self.warning('No models to predict with')
288
+ return
289
+
290
+ if self._running_error is None:
291
+ self._running_error = {
292
+ parm: {"err": 0, "N": 0} for parm in self._prediction_models
293
+ }
294
+
295
+ if not self._fitted:
296
+ return
297
+
298
+ if not all([p in row for p in self._prediction_parms]):
299
+ return
300
+
301
+ for parm, mod_dict in self._prediction_models.items():
302
+ model = mod_dict["mod"]
303
+ if parm not in row:
304
+ continue
305
+ X = pandas.DataFrame([{parm: row[parm] for parm in self._prediction_parms}])
306
+ y = row[parm]
307
+ x = abs(model.predict(X))
308
+ if y == 0:
309
+ if x == 0:
310
+ err = 0
311
+ else:
312
+ err = 1
313
+ else:
314
+ y = abs(y)
315
+ err = (y - x) / y
316
+ cerr = self._running_error[parm]["err"]
317
+ N = self._running_error[parm]["N"] + 1
318
+ self._running_error[parm]["err"] = cerr + (err - cerr) / N
319
+ self._running_error[parm]["N"] = N
320
+
321
+ def check_and_retrain(self, records, min_rec=None):
322
+ """Checks if more data than threshold to train or if error is sufficiently low to ignore retraining, or if more data already exists than window size (no training)"""
323
+ if self._prediction_models is None:
324
+ return
325
+
326
+ if not hasattr(self, "min_rec"):
327
+ min_rec = 50
328
+ else:
329
+ min_rec = self.min_rec
330
+
331
+ if self.trained:
332
+ if len(records) % min_rec == 0:
333
+ df = self.prediction_dataframe(records)
334
+ self.score_data(df)
335
+ return
336
+
337
+ Nrec = len(records)
338
+ if Nrec < min_rec:
339
+ return
340
+
341
+ df = self.prediction_dataframe(records)
342
+
343
+ if not self._fitted:
344
+ self.train_compare(df)
345
+ return
346
+
347
+ # an early check if the data is more than the target or if the error is less than the target
348
+ self.trained = all(
349
+ [
350
+ (1 - mod_dict["train_score"]) <= self.prediction_goal_error
351
+ for mod_dict in self._prediction_models.values()
352
+ ]
353
+ )
354
+
355
+ for parm, mod_dict in self._prediction_models.items():
356
+ model = mod_dict["mod"]
357
+ N = mod_dict["N"]
358
+ #
359
+ if Nrec > N * self._re_train_frac or (Nrec - N) > self._re_train_maxiter:
360
+ self.train_compare(df)
361
+ return
362
+
363
+ def prediction_dataframe(self, records):
364
+ df = pandas.DataFrame(records)._get_numeric_data()
365
+ df[np.isnan(df)] = 0 # zeros baybee
366
+ return df