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,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
|