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/eng/geometry.py
ADDED
|
@@ -0,0 +1,1060 @@
|
|
|
1
|
+
"""These exist as in interface to sectionproperties from Pynite"""
|
|
2
|
+
|
|
3
|
+
from engforge.configuration import Configuration, forge, LoggingMixin
|
|
4
|
+
from engforge.properties import (
|
|
5
|
+
cached_system_property,
|
|
6
|
+
system_property,
|
|
7
|
+
instance_cached,
|
|
8
|
+
)
|
|
9
|
+
from engforge.typing import Options
|
|
10
|
+
from engforge.eng.prediction import PredictionMixin
|
|
11
|
+
from engforge.env_var import EnvVariable
|
|
12
|
+
import numpy
|
|
13
|
+
import attr, attrs
|
|
14
|
+
|
|
15
|
+
from scipy import optimize as skopt
|
|
16
|
+
|
|
17
|
+
from sklearn import svm
|
|
18
|
+
|
|
19
|
+
from sectionproperties.pre.geometry import Geometry
|
|
20
|
+
from sectionproperties.pre.pre import Material as sec_material
|
|
21
|
+
from sectionproperties.analysis.section import Section
|
|
22
|
+
import sectionproperties.pre.library.primitive_sections as sections
|
|
23
|
+
import numpy as np
|
|
24
|
+
import shapely
|
|
25
|
+
import attr, attrs
|
|
26
|
+
import functools
|
|
27
|
+
import itertools
|
|
28
|
+
import multiprocessing as mp
|
|
29
|
+
import threading
|
|
30
|
+
import random
|
|
31
|
+
import pandas
|
|
32
|
+
import pickle
|
|
33
|
+
import hashlib
|
|
34
|
+
import tempfile, os
|
|
35
|
+
import json
|
|
36
|
+
|
|
37
|
+
# generic cross sections from
|
|
38
|
+
# https://mechanicalbase.com/area-moment-of-inertia-calculator-of-certain-cross-sectional-shapes/
|
|
39
|
+
_temp = os.path.join(os.path.expanduser("~"), ".forge_tmp")
|
|
40
|
+
temp_path = os.path.join(_temp, "shapely_sections")
|
|
41
|
+
|
|
42
|
+
section_cache = EnvVariable(
|
|
43
|
+
"FORGE_CACHE",
|
|
44
|
+
default=temp_path,
|
|
45
|
+
desc="directory to cache section properties",
|
|
46
|
+
)
|
|
47
|
+
if "FORGE_CACHE" not in os.environ and not os.path.exists(temp_path):
|
|
48
|
+
os.makedirs(temp_path, 0o741, exist_ok=True)
|
|
49
|
+
|
|
50
|
+
section_cache.info(f"loading section from {section_cache.secret}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class GeometryLog(LoggingMixin):
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
log = GeometryLog()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def conver_np(inpt):
|
|
61
|
+
if isinstance(inpt, np.ndarray):
|
|
62
|
+
return inpt
|
|
63
|
+
elif isinstance(inpt, (list, tuple)):
|
|
64
|
+
return np.array(inpt)
|
|
65
|
+
elif isinstance(inpt, (int, float)):
|
|
66
|
+
return np.array([inpt])
|
|
67
|
+
else:
|
|
68
|
+
raise ValueError(f"got non numpy array/float: {inpt}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@attrs.define(slots=False)
|
|
72
|
+
class ParametricSpline:
|
|
73
|
+
"""a multivariate spline defined by uniform length vector input, with points P1,P2 and their slopes P1ds,P2ds"""
|
|
74
|
+
|
|
75
|
+
P1 = attrs.field(converter=conver_np)
|
|
76
|
+
P2 = attrs.field(converter=conver_np)
|
|
77
|
+
P1ds = attrs.field(converter=conver_np)
|
|
78
|
+
P2ds = attrs.field(converter=conver_np)
|
|
79
|
+
|
|
80
|
+
def __attrs_post_init__(self):
|
|
81
|
+
assert len(self.P1) == len(self.P2)
|
|
82
|
+
assert len(self.P1ds) == len(self.P2ds)
|
|
83
|
+
assert len(self.P1) == len(self.P1ds)
|
|
84
|
+
|
|
85
|
+
@functools.cached_property
|
|
86
|
+
def a0(self):
|
|
87
|
+
return self.P1
|
|
88
|
+
|
|
89
|
+
@functools.cached_property
|
|
90
|
+
def a1(self):
|
|
91
|
+
return self.P1ds
|
|
92
|
+
|
|
93
|
+
@functools.cached_property
|
|
94
|
+
def a2(self):
|
|
95
|
+
return 3 * (self.P2 - self.P1) - 2 * self.P1ds - self.P2ds
|
|
96
|
+
|
|
97
|
+
@functools.cached_property
|
|
98
|
+
def a3(self):
|
|
99
|
+
return (self.P2ds - self.P1ds - 2 * self.a2) / 3.0
|
|
100
|
+
|
|
101
|
+
def coords(self, s: float):
|
|
102
|
+
if isinstance(s, (float, int)):
|
|
103
|
+
assert s <= 1
|
|
104
|
+
assert 0 <= s
|
|
105
|
+
return self._coords(s)
|
|
106
|
+
elif isinstance(s, (list, tuple, numpy.ndarray)):
|
|
107
|
+
assert max(s) <= 1
|
|
108
|
+
assert min(s) >= 0
|
|
109
|
+
ol = [self._coords(si) for si in s]
|
|
110
|
+
return numpy.array(ol)
|
|
111
|
+
else:
|
|
112
|
+
raise ValueError(f"non array/float input {s}")
|
|
113
|
+
|
|
114
|
+
def _coords(self, s: float):
|
|
115
|
+
return self.a0 + self.a1 * s + self.a2 * s**2 + self.a3 * s**3
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# TODO: cache SectionProperty sections and develop auto-mesh refinement system.
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@forge
|
|
122
|
+
class Profile2D(Configuration, PredictionMixin):
|
|
123
|
+
name: str = attr.ib(default="generic cross section")
|
|
124
|
+
|
|
125
|
+
# provide relative interface over
|
|
126
|
+
y_bounds: tuple = None
|
|
127
|
+
x_bounds: tuple = None
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def A(self):
|
|
131
|
+
return 0
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def Ao(self):
|
|
135
|
+
"""outside area, over ride for hallow sections"""
|
|
136
|
+
return self.A
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def Ixx(self):
|
|
140
|
+
return 0
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def Iyy(self):
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def J(self):
|
|
148
|
+
return 0
|
|
149
|
+
|
|
150
|
+
def display_results(self):
|
|
151
|
+
self.info("mock section, no results to display")
|
|
152
|
+
|
|
153
|
+
def plot_mesh(self):
|
|
154
|
+
self.info("mock section, no mesh to plot")
|
|
155
|
+
|
|
156
|
+
def calculate_stress(self, N, Vx, Vy, Mxx, Myy, M11, M22, Mzz):
|
|
157
|
+
# TODO: Implement stress object, make fake mesh, and mock?
|
|
158
|
+
# sigma_n = N / self.A
|
|
159
|
+
# sigma_bx = self.Myy * self.max_y / self.Ixx
|
|
160
|
+
# sigma_by = self.Mxx * self.max_x / self.Iyy
|
|
161
|
+
self.warning(f"calculating stress in simple profile!")
|
|
162
|
+
return np.nan
|
|
163
|
+
|
|
164
|
+
def estimate_stress(self, N, Vx, Vy, Mxx, Myy, M11, M22, Mzz):
|
|
165
|
+
# TODO: Implement stress object, make fake mesh, and mock?
|
|
166
|
+
# sigma_n = N / self.A
|
|
167
|
+
# sigma_bx = self.Myy * self.max_y / self.Ixx
|
|
168
|
+
# sigma_by = self.Mxx * self.max_x / self.Iyy
|
|
169
|
+
self.warning(f"estimating stress in simple profile!")
|
|
170
|
+
return np.nan
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@forge
|
|
174
|
+
class Rectangle(Profile2D):
|
|
175
|
+
"""models rectangle with base b, and height h"""
|
|
176
|
+
|
|
177
|
+
b: float = attr.ib()
|
|
178
|
+
h: float = attr.ib()
|
|
179
|
+
name: str = attr.ib(default="rectangular section")
|
|
180
|
+
|
|
181
|
+
def __on_init__(self):
|
|
182
|
+
self.y_bounds = (self.h / 2, -self.h / 2)
|
|
183
|
+
self.x_bounds = (self.b / 2, -self.b / 2)
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def A(self):
|
|
187
|
+
return self.h * self.b
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def Ixx(self):
|
|
191
|
+
return self.b * self.h**3.0 / 12.0
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def Iyy(self):
|
|
195
|
+
return self.h * self.b**3.0 / 12.0
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def J(self):
|
|
199
|
+
return (self.h * self.b) * (self.b**2.0 + self.h**2.0) / 12
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@forge
|
|
203
|
+
class Triangle(Profile2D):
|
|
204
|
+
"""models a triangle with base, b and height h"""
|
|
205
|
+
|
|
206
|
+
b: float = attr.ib()
|
|
207
|
+
h: float = attr.ib()
|
|
208
|
+
name: str = attr.ib(default="rectangular section")
|
|
209
|
+
|
|
210
|
+
def __on_init__(self):
|
|
211
|
+
self.y_bounds = (self.h / 2, -self.h / 2)
|
|
212
|
+
self.x_bounds = (self.b / 2, -self.b / 2)
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def A(self):
|
|
216
|
+
return self.h * self.b / 2.0
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def Ixx(self):
|
|
220
|
+
return self.b * self.h**3.0 / 36.0
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def Iyy(self):
|
|
224
|
+
return self.h * self.b**3.0 / 36.0
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def J(self):
|
|
228
|
+
return (self.h * self.b) * (self.b**2.0 + self.h**2.0) / 12
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@forge
|
|
232
|
+
class Circle(Profile2D):
|
|
233
|
+
"""models a solid circle with diameter d"""
|
|
234
|
+
|
|
235
|
+
d: float = attr.ib()
|
|
236
|
+
name: str = attr.ib(default="rectangular section")
|
|
237
|
+
|
|
238
|
+
def __on_init__(self):
|
|
239
|
+
self.x_bounds = self.y_bounds = (self.d / 2, -self.d / 2)
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def A(self):
|
|
243
|
+
return (self.d / 2.0) ** 2.0 * numpy.pi
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def Ixx(self):
|
|
247
|
+
return numpy.pi * (self.d**4.0 / 64.0)
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def Iyy(self):
|
|
251
|
+
return numpy.pi * (self.d**4.0 / 64.0)
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def J(self):
|
|
255
|
+
return numpy.pi * (self.d**4.0 / 32.0)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@forge
|
|
259
|
+
class HollowCircle(Profile2D):
|
|
260
|
+
"""models a hollow circle with diameter d and thickness t"""
|
|
261
|
+
|
|
262
|
+
d: float = attr.ib()
|
|
263
|
+
t: float = attr.ib()
|
|
264
|
+
name: str = attr.ib(default="rectangular section")
|
|
265
|
+
|
|
266
|
+
def __on_init__(self):
|
|
267
|
+
self.y_bounds = (self.d / 2, -self.d / 2)
|
|
268
|
+
self.x_bounds = (self.d / 2, -self.d / 2)
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def di(self):
|
|
272
|
+
return self.d - self.t * 2
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def Ao(self):
|
|
276
|
+
"""outside area, over ride for hallow sections"""
|
|
277
|
+
return (self.d**2.0) / 4.0 * numpy.pi
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def A(self):
|
|
281
|
+
return (self.d**2.0 - self.di**2.0) / 4.0 * numpy.pi
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def Ixx(self):
|
|
285
|
+
return numpy.pi * ((self.d**4.0 - self.di**4.0) / 64.0)
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def Iyy(self):
|
|
289
|
+
return numpy.pi * ((self.d**4.0 - self.di**4.0) / 64.0)
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def J(self):
|
|
293
|
+
return numpy.pi * ((self.d**4.0 - self.di**4.0) / 32.0)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# ADVANCED CUSTOM SECTIONS
|
|
297
|
+
def get_mesh_size(inst):
|
|
298
|
+
if isinstance(inst.shape, Geometry):
|
|
299
|
+
shape = inst.shape.geom
|
|
300
|
+
else:
|
|
301
|
+
shape = inst.shape
|
|
302
|
+
|
|
303
|
+
dec = inst.mesh_extent_decimation
|
|
304
|
+
x, y = shape.exterior.coords.xy
|
|
305
|
+
dx = abs(max(x) - min(x))
|
|
306
|
+
dy = abs(max(y) - min(y))
|
|
307
|
+
ddx = np.abs(np.diff(x))
|
|
308
|
+
ddx = ddx[ddx > inst.min_mesh_size].tolist()
|
|
309
|
+
ddy = np.abs(np.diff(y))
|
|
310
|
+
ddy = ddy[ddy > inst.min_mesh_size].tolist()
|
|
311
|
+
|
|
312
|
+
A = shape.area
|
|
313
|
+
dAmin = A * 0.95 / (inst.goal_elements)
|
|
314
|
+
|
|
315
|
+
cans = [dx / dec, dy / dec]
|
|
316
|
+
if ddx:
|
|
317
|
+
cans.append(min(ddx))
|
|
318
|
+
if ddy:
|
|
319
|
+
cans.append(min(ddy))
|
|
320
|
+
|
|
321
|
+
ms = max(min(cans), inst.min_mesh_size)
|
|
322
|
+
return max(ms**2.0, dAmin) # length to area conversion
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def calculate_stress(
|
|
326
|
+
section,
|
|
327
|
+
n=0,
|
|
328
|
+
vx=0,
|
|
329
|
+
vy=0,
|
|
330
|
+
mxx=0,
|
|
331
|
+
myy=0,
|
|
332
|
+
mzz=0,
|
|
333
|
+
raw=False,
|
|
334
|
+
row=False,
|
|
335
|
+
record=True,
|
|
336
|
+
value=True,
|
|
337
|
+
) -> float:
|
|
338
|
+
"""returns the maximum vonmises stress in the section and returns the ratio of the allowable stress, also known as the failure fracion
|
|
339
|
+
:param raw: if raw is true, the stress object is returned, otherwise the failure fraction is returned
|
|
340
|
+
"""
|
|
341
|
+
inp = dict(n=n, vx=vx, vy=vy, mxx=mxx, myy=myy, mzz=mzz)
|
|
342
|
+
|
|
343
|
+
stress = section._sec.calculate_stress(**inp).get_stress()[0]
|
|
344
|
+
fail_stress = section.determine_failure_stress(stress)
|
|
345
|
+
inp["fail_stress"] = fail_stress
|
|
346
|
+
inp["fail_frac"] = ff = fail_stress / section.material.allowable_stress
|
|
347
|
+
inp["fails"] = int(ff >= 1)
|
|
348
|
+
|
|
349
|
+
if record:
|
|
350
|
+
section.record_stress(inp)
|
|
351
|
+
|
|
352
|
+
if value:
|
|
353
|
+
return fail_stress
|
|
354
|
+
|
|
355
|
+
if raw:
|
|
356
|
+
return stress
|
|
357
|
+
|
|
358
|
+
if row:
|
|
359
|
+
return inp
|
|
360
|
+
|
|
361
|
+
return ff
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@forge(hash=False)
|
|
365
|
+
class ShapelySection(Profile2D):
|
|
366
|
+
"""a 2D profile that takes a shapely section to calculate section properties, use a sectionproperties section with hidden variable `_geo` to bypass shape calculation"""
|
|
367
|
+
|
|
368
|
+
name: str = attrs.field(default="shapely section")
|
|
369
|
+
shape: shapely.Polygon = attrs.field()
|
|
370
|
+
|
|
371
|
+
# Mesh sizing
|
|
372
|
+
coarse: bool = attrs.field(default=False)
|
|
373
|
+
mesh_extent_decimation = attrs.field(default=100)
|
|
374
|
+
min_mesh_angle: float = attrs.field(default=20) # below 20.7 garunteed to work
|
|
375
|
+
min_mesh_size: float = attrs.field(default=1e-5) # multiply by min
|
|
376
|
+
goal_elements: float = attrs.field(default=1000) # multiply by min
|
|
377
|
+
_mesh_size: float = attrs.field(default=attrs.Factory(get_mesh_size, True))
|
|
378
|
+
|
|
379
|
+
material: sec_material = attrs.field(default=None)
|
|
380
|
+
failure_mode = Options("von_mises", "max_norm", "maximum_strain")
|
|
381
|
+
|
|
382
|
+
# Stress classification & prediction
|
|
383
|
+
prediction: bool = attr.field(default=False)
|
|
384
|
+
prediction_goal_error: float = attrs.field(default=0.025)
|
|
385
|
+
max_records: list = attr.field(default=10000)
|
|
386
|
+
prediction_records: list
|
|
387
|
+
near_margin = 0.1
|
|
388
|
+
max_margin = 1.5
|
|
389
|
+
save_threshold = 0.90
|
|
390
|
+
max_rec_parm = "fail_frac"
|
|
391
|
+
|
|
392
|
+
_use_symmetric: bool = attrs.field(default=True)
|
|
393
|
+
_prediction_parms = ["n", "vx", "vy", "mxx", "myy", "mzz"]
|
|
394
|
+
_do_print: bool = attrs.field(default=False)
|
|
395
|
+
_sec: Section = None
|
|
396
|
+
_geo: Geometry = None
|
|
397
|
+
_A: float
|
|
398
|
+
_symmetric: bool
|
|
399
|
+
_subclass_init: bool = False
|
|
400
|
+
|
|
401
|
+
def __on_init__(self):
|
|
402
|
+
self.init_with_material(self.material)
|
|
403
|
+
|
|
404
|
+
if self.prediction:
|
|
405
|
+
self.add_prediction_record(
|
|
406
|
+
{
|
|
407
|
+
"n": 0,
|
|
408
|
+
"vx": 0,
|
|
409
|
+
"vy": 0,
|
|
410
|
+
"mxx": 0,
|
|
411
|
+
"myy": 0,
|
|
412
|
+
"mzz": 0,
|
|
413
|
+
"fails": 0,
|
|
414
|
+
"fail_frac": 0,
|
|
415
|
+
}
|
|
416
|
+
)
|
|
417
|
+
self._symmetric = self.check_symmetric()
|
|
418
|
+
if self._symmetric and self._use_symmetric:
|
|
419
|
+
self._prediction_models = {
|
|
420
|
+
"fails": {
|
|
421
|
+
"mod": svm.SVC(C=2000, gamma=0.1, probability=True),
|
|
422
|
+
"N": 0,
|
|
423
|
+
},
|
|
424
|
+
"fail_frac": {"mod": svm.SVR(C=5, gamma=0.5), "N": 0},
|
|
425
|
+
}
|
|
426
|
+
else:
|
|
427
|
+
self._prediction_models = {
|
|
428
|
+
"fails": {
|
|
429
|
+
"mod": svm.SVC(C=5000, gamma=5, probability=True),
|
|
430
|
+
"N": 0,
|
|
431
|
+
},
|
|
432
|
+
"fail_frac": {"mod": svm.SVR(C=10, gamma=0.5), "N": 0},
|
|
433
|
+
}
|
|
434
|
+
# self.determine_failure_front(pareto_front=False)
|
|
435
|
+
# self.basis_expand(expand_values=[0.75,0.5,0.1],Nparm=3,est=True)
|
|
436
|
+
|
|
437
|
+
def training_callback(self, models):
|
|
438
|
+
"""when training is complete save the model to a pickle with `ShapelySection_<hash>.pkl`"""
|
|
439
|
+
score = models["fail_frac"]["train_score"]
|
|
440
|
+
Nscore = models["fail_frac"]["N"]
|
|
441
|
+
test_score = score * Nscore # compare to meta file
|
|
442
|
+
|
|
443
|
+
# Opt out of saving if score is below threshold
|
|
444
|
+
if score < self.save_threshold:
|
|
445
|
+
return
|
|
446
|
+
# Opt out of saving if score is worse than previous
|
|
447
|
+
if os.path.exists(self.meta_path):
|
|
448
|
+
with open(self.meta_path, "r") as f:
|
|
449
|
+
meta = json.load(f)
|
|
450
|
+
if test_score < meta["test_score"]:
|
|
451
|
+
self.info(
|
|
452
|
+
f'new model score {test_score} is worse than {meta["test_score"]}, not saving'
|
|
453
|
+
)
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
new_meta = {"test_score": test_score, "N": Nscore, "train_score": score}
|
|
457
|
+
# add a meta json file to show info regarding data (num points better ect)
|
|
458
|
+
with open(self.meta_path, "w") as f:
|
|
459
|
+
json.dump(new_meta, f)
|
|
460
|
+
|
|
461
|
+
# Finally save the geometry
|
|
462
|
+
self.info(f"saving model with score: {Nscore}x{score}=>{test_score}")
|
|
463
|
+
fil = self.cache_path
|
|
464
|
+
with open(fil, "wb") as f:
|
|
465
|
+
pickle.dump(self, f)
|
|
466
|
+
self.info(f"saved section to {fil}")
|
|
467
|
+
|
|
468
|
+
@property
|
|
469
|
+
def section_cache(self) -> str:
|
|
470
|
+
return section_cache.secret
|
|
471
|
+
|
|
472
|
+
@property
|
|
473
|
+
def cache_name(self) -> str:
|
|
474
|
+
return f"ShapelySection_{self.hash_id()}.pkl"
|
|
475
|
+
|
|
476
|
+
@property
|
|
477
|
+
def meta_name(self) -> str:
|
|
478
|
+
return f"ShapelySection_meta_{self.hash_id()}.json"
|
|
479
|
+
|
|
480
|
+
@property
|
|
481
|
+
def cache_path(self):
|
|
482
|
+
return os.path.join(self.section_cache, self.cache_name)
|
|
483
|
+
|
|
484
|
+
@property
|
|
485
|
+
def meta_path(self):
|
|
486
|
+
return os.path.join(self.section_cache, self.meta_name)
|
|
487
|
+
|
|
488
|
+
@classmethod
|
|
489
|
+
def from_cache(cls, hash_id):
|
|
490
|
+
cchc = section_cache.secret
|
|
491
|
+
log.info(f"loading section {hash_id} from cache {cchc}")
|
|
492
|
+
fil = os.path.join(cchc, f"ShapelySection_{hash_id}.pkl")
|
|
493
|
+
with open(fil, "rb") as f:
|
|
494
|
+
model = pickle.load(f)
|
|
495
|
+
return model
|
|
496
|
+
|
|
497
|
+
def prediction_weights(self, df, window, initial_weight=10):
|
|
498
|
+
weights = numpy.ones(min(len(df), window))
|
|
499
|
+
weights[0] = initial_weight**2 # zero value is important!
|
|
500
|
+
weights[: getattr(self, "N_base", 100)] = initial_weight # then base values
|
|
501
|
+
if hasattr(self, "N_pareto"):
|
|
502
|
+
weights[: getattr(self, "N_pareto")] = (
|
|
503
|
+
initial_weight**0.5
|
|
504
|
+
) # then pareto values
|
|
505
|
+
# Dont emphasise fit above max margin
|
|
506
|
+
dm = (df.fail_frac - self.max_margin).to_numpy()
|
|
507
|
+
penalize_inx = dm > 0
|
|
508
|
+
weights[penalize_inx] = np.maximum(1.0 / ((1.0 + dm[penalize_inx])), 0.1)
|
|
509
|
+
return weights
|
|
510
|
+
|
|
511
|
+
def _subsample_data(self, X, y, window, weights):
|
|
512
|
+
"""subsamples the data to the window size"""
|
|
513
|
+
inx = getattr(self, "N_pareto", getattr(self, "N_base", window))
|
|
514
|
+
x1 = X.iloc[:inx]
|
|
515
|
+
y1 = y.iloc[:inx]
|
|
516
|
+
w1 = weights[:inx]
|
|
517
|
+
if inx != window:
|
|
518
|
+
x2 = X.iloc[inx:].sample(frac=0.5)
|
|
519
|
+
y2 = y.iloc[x2.index]
|
|
520
|
+
w2 = weights[x2.index]
|
|
521
|
+
return (
|
|
522
|
+
pandas.concat((x1, x2)),
|
|
523
|
+
pandas.concat((y1, y2)),
|
|
524
|
+
numpy.concatenate((w1, w2)),
|
|
525
|
+
)
|
|
526
|
+
return X.iloc[:window], y.iloc[:window], weights[:window]
|
|
527
|
+
|
|
528
|
+
def reset_prediction(self):
|
|
529
|
+
self._fitted = False
|
|
530
|
+
self._basis = None
|
|
531
|
+
self._training_history = None
|
|
532
|
+
self._running_error = None
|
|
533
|
+
self.add_prediction_record(
|
|
534
|
+
{
|
|
535
|
+
"n": 0,
|
|
536
|
+
"vx": 0,
|
|
537
|
+
"vy": 0,
|
|
538
|
+
"mxx": 0,
|
|
539
|
+
"myy": 0,
|
|
540
|
+
"mzz": 0,
|
|
541
|
+
"fails": 0,
|
|
542
|
+
"fail_frac": 0,
|
|
543
|
+
}
|
|
544
|
+
)
|
|
545
|
+
# self._symmetric = self.check_symmetric()
|
|
546
|
+
|
|
547
|
+
@property
|
|
548
|
+
def mesh_size(self):
|
|
549
|
+
return self._mesh_size
|
|
550
|
+
|
|
551
|
+
@property
|
|
552
|
+
def _prediction_record(self):
|
|
553
|
+
"""not a property of state, just return an empty dict, we add record manually"""
|
|
554
|
+
return {}
|
|
555
|
+
|
|
556
|
+
@mesh_size.setter
|
|
557
|
+
def mesh_size(self, value):
|
|
558
|
+
# print(f'setting mesh size to {value}')
|
|
559
|
+
# BUG: something going on with setattrs on mesh_size, setting to None, hacky fix to set __dict__ directly
|
|
560
|
+
_mesh_size = max(value, self.min_mesh_area)
|
|
561
|
+
self.__dict__["_mesh_size"] = _mesh_size
|
|
562
|
+
self.mesh_section()
|
|
563
|
+
|
|
564
|
+
def init_with_material(self, material=None):
|
|
565
|
+
if self._sec is not None:
|
|
566
|
+
raise Exception(f"already initalized!")
|
|
567
|
+
|
|
568
|
+
if isinstance(self.shape, Geometry):
|
|
569
|
+
self._geo = self.shape
|
|
570
|
+
if self._geo.material and self.material:
|
|
571
|
+
self.warning(
|
|
572
|
+
f"overriding material {self._geo.material} with {self.material}"
|
|
573
|
+
)
|
|
574
|
+
self._geo.material = self.material
|
|
575
|
+
elif self._geo.material:
|
|
576
|
+
self.info(f"setting beam material from section")
|
|
577
|
+
self.material = self._geo.material
|
|
578
|
+
elif self.material:
|
|
579
|
+
self._geo.material = self.material
|
|
580
|
+
elif isinstance(self.shape, shapely.Polygon):
|
|
581
|
+
self._geo = Geometry(self.shape, self.material)
|
|
582
|
+
else:
|
|
583
|
+
raise ValueException(f"got invalid shape: {self.shape}")
|
|
584
|
+
|
|
585
|
+
self.calculate_mesh_size()
|
|
586
|
+
self.mesh_section()
|
|
587
|
+
|
|
588
|
+
def calculate_mesh_size(self):
|
|
589
|
+
self.mesh_size = get_mesh_size(self)
|
|
590
|
+
|
|
591
|
+
@property
|
|
592
|
+
def min_mesh_area(self):
|
|
593
|
+
if isinstance(self.shape, Geometry):
|
|
594
|
+
shape = self.shape.geom
|
|
595
|
+
else:
|
|
596
|
+
shape = self.shape
|
|
597
|
+
A = shape.area
|
|
598
|
+
dAmin = A * 0.95 / (self.goal_elements)
|
|
599
|
+
return dAmin
|
|
600
|
+
|
|
601
|
+
def mesh_section(self):
|
|
602
|
+
"""caches section properties and mesh"""
|
|
603
|
+
self._cross_section = None # reset cross section
|
|
604
|
+
self._mesh = self._geo.create_mesh(
|
|
605
|
+
mesh_sizes=self.mesh_size,
|
|
606
|
+
coarse=self.coarse,
|
|
607
|
+
min_angle=self.min_mesh_angle,
|
|
608
|
+
)
|
|
609
|
+
self._sec = Section(self._geo)
|
|
610
|
+
self._sec.calculate_geometric_properties()
|
|
611
|
+
self._sec.calculate_warping_properties()
|
|
612
|
+
self._sec.calculate_frame_properties()
|
|
613
|
+
|
|
614
|
+
self._A = self._sec.get_area()
|
|
615
|
+
if self.material:
|
|
616
|
+
self._Ixx, self._Iyy, self._Ixy = self._sec.get_eic(e_ref=self.material)
|
|
617
|
+
self._J = self._sec.get_ej()
|
|
618
|
+
else:
|
|
619
|
+
self._Ixx, self._Iyy, self._Ixy = self._sec.get_ic()
|
|
620
|
+
self._J = self._sec.get_j()
|
|
621
|
+
|
|
622
|
+
self.calculate_bounds()
|
|
623
|
+
|
|
624
|
+
def calculate_bounds(self):
|
|
625
|
+
self.debug(f"calculating shape bounds!")
|
|
626
|
+
xcg, ycg = self._geo.calculate_centroid()
|
|
627
|
+
minx, maxx, miny, maxy = self._geo.calculate_extents()
|
|
628
|
+
self.y_bounds = (miny - ycg, maxy - ycg)
|
|
629
|
+
self.x_bounds = (minx - xcg, maxx - xcg)
|
|
630
|
+
|
|
631
|
+
@property
|
|
632
|
+
def A(self):
|
|
633
|
+
return self._A
|
|
634
|
+
|
|
635
|
+
@property
|
|
636
|
+
def Ao(self):
|
|
637
|
+
"""outside area, over ride for hallow sections"""
|
|
638
|
+
return self.A
|
|
639
|
+
|
|
640
|
+
@property
|
|
641
|
+
def Ixx(self):
|
|
642
|
+
return self._Ixx
|
|
643
|
+
|
|
644
|
+
@property
|
|
645
|
+
def Iyy(self):
|
|
646
|
+
return self._Iyy
|
|
647
|
+
|
|
648
|
+
@property
|
|
649
|
+
def J(self):
|
|
650
|
+
return self._J
|
|
651
|
+
|
|
652
|
+
@property
|
|
653
|
+
def Ixy(self):
|
|
654
|
+
return self._Ixy
|
|
655
|
+
|
|
656
|
+
def display_results(self):
|
|
657
|
+
self.info("mock section, no results to display")
|
|
658
|
+
|
|
659
|
+
def plot_mesh(self):
|
|
660
|
+
self._sec.display_mesh_info()
|
|
661
|
+
|
|
662
|
+
def plot_mesh(self):
|
|
663
|
+
return self._sec.plot_centroids()
|
|
664
|
+
|
|
665
|
+
def calculate_stress(self, n=0, vx=0, vy=0, mxx=0, myy=0, mzz=0, **kw) -> float:
|
|
666
|
+
return calculate_stress(
|
|
667
|
+
self, n=n, vx=vx, vy=vy, mxx=mxx, myy=myy, mzz=mzz, **kw
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
def estimate_stress(
|
|
671
|
+
self,
|
|
672
|
+
n=0,
|
|
673
|
+
vx=0,
|
|
674
|
+
vy=0,
|
|
675
|
+
mxx=0,
|
|
676
|
+
myy=0,
|
|
677
|
+
mzz=0,
|
|
678
|
+
value=False,
|
|
679
|
+
calc_margin=2,
|
|
680
|
+
min_est_records=100,
|
|
681
|
+
calc_every=25,
|
|
682
|
+
pre_train_margin=2,
|
|
683
|
+
force_calc=False,
|
|
684
|
+
) -> float:
|
|
685
|
+
"""uses a support vector machine to estimate stresses and returns the ratio of the allowable stress, also known as the failure fracion if prediction is set to True, otherwise calculates stress"""
|
|
686
|
+
|
|
687
|
+
Nrec = 0
|
|
688
|
+
under_size = True
|
|
689
|
+
if self.prediction:
|
|
690
|
+
pr = self.prediction_records
|
|
691
|
+
Nrec = len(pr) if pr else 0
|
|
692
|
+
under_size = Nrec <= min_est_records
|
|
693
|
+
|
|
694
|
+
do_calc = not self.prediction or not self._fitted or under_size
|
|
695
|
+
if do_calc or force_calc:
|
|
696
|
+
if self._do_print and self.prediction:
|
|
697
|
+
print(f"calc till {len(self.prediction_records)} <= {min_est_records}")
|
|
698
|
+
stress = calculate_stress(
|
|
699
|
+
self, n=n, vx=vx, vy=vy, mxx=mxx, myy=myy, mzz=mzz, value=value
|
|
700
|
+
)
|
|
701
|
+
return stress
|
|
702
|
+
else:
|
|
703
|
+
parms = self._prediction_parms
|
|
704
|
+
data = dict(n=n, vx=vx, vy=vy, mxx=mxx, myy=myy, mzz=mzz)
|
|
705
|
+
|
|
706
|
+
# Format data for prediction
|
|
707
|
+
if self._symmetric and self._use_symmetric:
|
|
708
|
+
inp = {k: abs(data[k] / v) for k, v in zip(parms, self._basis)}
|
|
709
|
+
else:
|
|
710
|
+
inp = {k: data[k] / v for k, v in zip(parms, self._basis)}
|
|
711
|
+
X = pandas.DataFrame([inp])
|
|
712
|
+
val = self._prediction_models["fail_frac"]["mod"].predict(X)[0]
|
|
713
|
+
|
|
714
|
+
# Provide margin of error and use estimate where possible
|
|
715
|
+
if not self.trained:
|
|
716
|
+
# calc when in doubt
|
|
717
|
+
calc_margin = calc_margin * pre_train_margin
|
|
718
|
+
|
|
719
|
+
# calculate stress if close to failure within saftey margin
|
|
720
|
+
err = 1 - val
|
|
721
|
+
mrg = self.fail_frac_criteria(calc_margin=calc_margin)
|
|
722
|
+
do_calc = abs(err) <= mrg or all([calc_every, (Nrec % calc_every) == 0])
|
|
723
|
+
oob = val <= self.max_margin and self.check_out_of_domain(data, 0.1)
|
|
724
|
+
if self._do_print:
|
|
725
|
+
self.info(
|
|
726
|
+
f'{"calc" if do_calc or oob else "est"} stress {abs(err):5.3f}>{mrg*calc_margin:5.3f}| {calc_margin} | oob {oob}'
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
if do_calc:
|
|
730
|
+
return self.calculate_stress(
|
|
731
|
+
n=n, vx=vx, vy=vy, mxx=mxx, myy=myy, mzz=mzz, value=value
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
elif oob:
|
|
735
|
+
# if self._do_print:
|
|
736
|
+
# self.info(f'out of domain {val:5.3f}<={self.max_margin:5.3f}')
|
|
737
|
+
return self.calculate_stress(
|
|
738
|
+
n=n, vx=vx, vy=vy, mxx=mxx, myy=myy, mzz=mzz, value=value
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
# otherwise prediction is value
|
|
742
|
+
if value:
|
|
743
|
+
return val * self.material.allowable_stress
|
|
744
|
+
|
|
745
|
+
return val
|
|
746
|
+
|
|
747
|
+
def fail_frac_criteria(self, calc_margin=2, min_rec=1000):
|
|
748
|
+
MargRec = max(min_rec / len(self._prediction_records), 1)
|
|
749
|
+
|
|
750
|
+
if self._training_history:
|
|
751
|
+
score = self._training_history[-1]["fail_frac"]["scr"]
|
|
752
|
+
else:
|
|
753
|
+
score = self._prediction_models["fail_frac"]["train_score"]
|
|
754
|
+
|
|
755
|
+
mrg = max(abs((1 - score)), self.prediction_goal_error)
|
|
756
|
+
return mrg * calc_margin * MargRec
|
|
757
|
+
|
|
758
|
+
def estimate_failure(self, n=0, vx=0, vy=0, mxx=0, myy=0, mzz=0) -> float:
|
|
759
|
+
"""uses a support vector machine to estimate stresses and returns a number zero or one to indicate failure, if prediction is set to True, otherwise calculates stress"""
|
|
760
|
+
if not self.prediction or not self._fitted:
|
|
761
|
+
ff = calculate_stress(
|
|
762
|
+
self,
|
|
763
|
+
n=n,
|
|
764
|
+
vx=vx,
|
|
765
|
+
vy=vy,
|
|
766
|
+
mxx=mxx,
|
|
767
|
+
myy=myy,
|
|
768
|
+
mzz=mzz,
|
|
769
|
+
row=True,
|
|
770
|
+
value=False,
|
|
771
|
+
)
|
|
772
|
+
return ff["fails"]
|
|
773
|
+
else:
|
|
774
|
+
# TODO: add logic to determine if calculation should be done close to failure
|
|
775
|
+
|
|
776
|
+
parms = self._prediction_parms
|
|
777
|
+
data = dict(n=n, vx=vx, vy=vy, mxx=mxx, myy=myy, mzz=mzz)
|
|
778
|
+
if self._symmetric and self._use_symmetric:
|
|
779
|
+
inp = {k: abs(data[k] / v) for k, v in zip(parms, self._basis)}
|
|
780
|
+
else:
|
|
781
|
+
inp = {k: data[k] / v for k, v in zip(parms, self._basis)}
|
|
782
|
+
X = pandas.DataFrame([inp])
|
|
783
|
+
return self._prediction_models["fails"]["mod"].predict(X)[0]
|
|
784
|
+
|
|
785
|
+
def record_stress(self, stress_dict):
|
|
786
|
+
"""determines if stress record should be added to prediction_records"""
|
|
787
|
+
if not self.prediction:
|
|
788
|
+
return
|
|
789
|
+
if len(self.prediction_records) > self.max_records:
|
|
790
|
+
return
|
|
791
|
+
# Add the data to the stress records
|
|
792
|
+
# TODO: add logic to determine if stress record should be added to prediction_records
|
|
793
|
+
ff = stress_dict["fail_frac"]
|
|
794
|
+
if ff == 0:
|
|
795
|
+
# null included
|
|
796
|
+
return
|
|
797
|
+
|
|
798
|
+
# Convert to positive domain
|
|
799
|
+
if self._symmetric and self._use_symmetric:
|
|
800
|
+
stress_dict = {k: abs(v) for k, v in stress_dict.items()}
|
|
801
|
+
|
|
802
|
+
if not getattr(self, "N_base", None):
|
|
803
|
+
max_margin = None
|
|
804
|
+
else:
|
|
805
|
+
max_margin = self.max_margin
|
|
806
|
+
|
|
807
|
+
if self.near_margin and abs(ff - 1) < self.near_margin:
|
|
808
|
+
# near failure always add to map resolution
|
|
809
|
+
self.add_prediction_record(stress_dict)
|
|
810
|
+
elif max_margin and ff < max_margin * 2:
|
|
811
|
+
# under max marging
|
|
812
|
+
self.add_prediction_record(stress_dict, False, 0.1)
|
|
813
|
+
elif not self.near_margin and not max_margin:
|
|
814
|
+
# no checks
|
|
815
|
+
self.add_prediction_record(stress_dict, False)
|
|
816
|
+
|
|
817
|
+
def determine_failure_stress(self, stress_obj):
|
|
818
|
+
"""uses the failure mode to compare to allowable stress"""
|
|
819
|
+
if self.failure_mode == "von_mises":
|
|
820
|
+
return stress_obj["sig_vm"].max()
|
|
821
|
+
elif self.failure_mode == "max_norm":
|
|
822
|
+
return np.nan # TODO: make this work
|
|
823
|
+
elif self.failure_mode == "maximum_strain":
|
|
824
|
+
return np.nan # TODO: make this work
|
|
825
|
+
else:
|
|
826
|
+
raise ValueError(f"invalid failure mode: {self.failure_mode}")
|
|
827
|
+
|
|
828
|
+
def fail_learning(self, X, parm, base_kw, mult=1):
|
|
829
|
+
"""optimizes stress until failure, given base_kw arguments for extra parameters"""
|
|
830
|
+
base_kw = base_kw.copy()
|
|
831
|
+
inpt = {parm: mult * X}
|
|
832
|
+
base_kw.update(inpt)
|
|
833
|
+
|
|
834
|
+
base_kw["value"] = False # override value for calcstress to fail frac
|
|
835
|
+
ff = self.calculate_stress(**base_kw).item()
|
|
836
|
+
return 1 - ff
|
|
837
|
+
|
|
838
|
+
def solve_fail(self, fail_parm, base_kw, guess=None, tol=1e-4, mult=1):
|
|
839
|
+
if guess is None:
|
|
840
|
+
guess = 1000 * random.random()
|
|
841
|
+
kw = {}
|
|
842
|
+
# if self._basis is not None:
|
|
843
|
+
# fpinx = self._prediction_parms.index(fail_parm)
|
|
844
|
+
# bracket = (0,self._basis[fpinx]*1.25)
|
|
845
|
+
# kw['bracket'] = bracket
|
|
846
|
+
|
|
847
|
+
ans = skopt.root_scalar(
|
|
848
|
+
self.fail_learning,
|
|
849
|
+
x0=guess * 0.1,
|
|
850
|
+
x1=guess,
|
|
851
|
+
xtol=tol,
|
|
852
|
+
args=(fail_parm, base_kw, mult),
|
|
853
|
+
) # ,**kw)
|
|
854
|
+
if self._do_print:
|
|
855
|
+
self.info(
|
|
856
|
+
f"{mult}x{fail_parm:<6}| success: {ans.converged} , ans:{ans.root}, base: {base_kw}"
|
|
857
|
+
)
|
|
858
|
+
else:
|
|
859
|
+
self.debug(
|
|
860
|
+
f"{mult}x{fail_parm:<6}| success: {ans.converged} , ans:{ans.root}, base: {base_kw}"
|
|
861
|
+
)
|
|
862
|
+
if ans.converged:
|
|
863
|
+
return ans.root
|
|
864
|
+
return 1e6
|
|
865
|
+
|
|
866
|
+
# Determine Outer Bound Of Failures
|
|
867
|
+
def determine_failure_front(self, pareto_inx=[0.5, 0.1], pareto_front=False):
|
|
868
|
+
self.info(
|
|
869
|
+
f"determining failure front for cross section, with pareto inx: {pareto_inx}"
|
|
870
|
+
)
|
|
871
|
+
null_kw = {}
|
|
872
|
+
if self._symmetric:
|
|
873
|
+
mvec = [1]
|
|
874
|
+
else:
|
|
875
|
+
mvec = [-1, 1]
|
|
876
|
+
|
|
877
|
+
if self._basis is None:
|
|
878
|
+
self.info(f"determining normalization basis")
|
|
879
|
+
res = {}
|
|
880
|
+
for mult in mvec:
|
|
881
|
+
res[mult] = {}
|
|
882
|
+
for p in self._prediction_parms:
|
|
883
|
+
res[mult][p] = self.solve_fail(p, null_kw, mult)
|
|
884
|
+
|
|
885
|
+
self._basis = np.array(
|
|
886
|
+
[
|
|
887
|
+
max([abs(v.get(p, 1e6)) for k, v in res.items()])
|
|
888
|
+
for p in self._prediction_parms
|
|
889
|
+
]
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
self.N_base = len(self.prediction_records)
|
|
893
|
+
|
|
894
|
+
# Second Pareto Value Calc
|
|
895
|
+
if pareto_front:
|
|
896
|
+
for mult in mvec:
|
|
897
|
+
for aux_frac in pareto_inx:
|
|
898
|
+
# TODO: expand parato combos past 2
|
|
899
|
+
for fail_parm, aux_parm in itertools.combinations(
|
|
900
|
+
self._prediction_parms, 2
|
|
901
|
+
):
|
|
902
|
+
ainx = self._prediction_parms.index(aux_parm)
|
|
903
|
+
aux_val = self._basis[ainx] * aux_frac
|
|
904
|
+
base_kw = {aux_parm: aux_val}
|
|
905
|
+
finx = self._prediction_parms.index(fail_parm)
|
|
906
|
+
guesstimate = self._basis[finx] * (1 - aux_frac)
|
|
907
|
+
self.solve_fail(
|
|
908
|
+
fail_parm, base_kw, guess=guesstimate, mult=mult
|
|
909
|
+
)
|
|
910
|
+
if not self._symmetric:
|
|
911
|
+
self.solve_fail(
|
|
912
|
+
fail_parm,
|
|
913
|
+
base_kw,
|
|
914
|
+
guess=guesstimate,
|
|
915
|
+
mult=-1 * mult,
|
|
916
|
+
) # alternato
|
|
917
|
+
|
|
918
|
+
self.N_pareto = len(self.prediction_records)
|
|
919
|
+
|
|
920
|
+
def basis_expand(
|
|
921
|
+
self,
|
|
922
|
+
expand_values=[0.9, 0.75, 0.5, 0.1, 0.01],
|
|
923
|
+
Nparm=4,
|
|
924
|
+
est=True,
|
|
925
|
+
normalize=True,
|
|
926
|
+
):
|
|
927
|
+
"""run combinations of parameters and permutations of weights against the basis values to populate the stress records, by default using estimation logic to speed up the process"""
|
|
928
|
+
i = 0
|
|
929
|
+
Nparm = min(min(Nparm, len(self._prediction_parms)), len(expand_values))
|
|
930
|
+
for parms in itertools.combinations(self._prediction_parms, Nparm):
|
|
931
|
+
for weight in itertools.permutations(expand_values, Nparm):
|
|
932
|
+
wt = sum(weight)
|
|
933
|
+
q = max(wt, 1) if normalize else 1
|
|
934
|
+
inxs = [self._prediction_parms.index(p) for p in parms]
|
|
935
|
+
base_kw = {
|
|
936
|
+
p: w * self._basis[i] / q for p, w, i in zip(parms, weight, inxs)
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
# print(base_kw)
|
|
940
|
+
if est:
|
|
941
|
+
self.estimate_stress(**base_kw)
|
|
942
|
+
else:
|
|
943
|
+
self.calculate_stress(**base_kw)
|
|
944
|
+
i += 1
|
|
945
|
+
if i % 100 == 0:
|
|
946
|
+
self.info(f"basis expansion... {i}")
|
|
947
|
+
|
|
948
|
+
def random_force_input(self, exp_mg=12, exp_min=6, min_ex=1, max_ex=3):
|
|
949
|
+
porp = np.random.random(size=6) ** np.random.randint(
|
|
950
|
+
numpy.random.randint(min_ex, max_ex),
|
|
951
|
+
numpy.random.randint(exp_min, exp_mg),
|
|
952
|
+
size=(6,),
|
|
953
|
+
)
|
|
954
|
+
stress = porp * self._basis
|
|
955
|
+
return {k: v for k, v in zip(self._prediction_parms, stress)}
|
|
956
|
+
|
|
957
|
+
def train_until_valid(self, print_interval=50, max_iter=1000, est=False):
|
|
958
|
+
"""trains the prediction models until the error is below the goal error"""
|
|
959
|
+
i = 0
|
|
960
|
+
goal = self.prediction_goal_error
|
|
961
|
+
while not self.trained:
|
|
962
|
+
inp = self.random_force_input()
|
|
963
|
+
if est:
|
|
964
|
+
self.estimate_stress(**inp)
|
|
965
|
+
else:
|
|
966
|
+
self.calculate_stress(**inp)
|
|
967
|
+
|
|
968
|
+
if i % print_interval == 0:
|
|
969
|
+
self.info(f"training... current error: {self._training_history[-1]}")
|
|
970
|
+
i += 1
|
|
971
|
+
if i >= max_iter:
|
|
972
|
+
self.info(f"training... max iterations reached")
|
|
973
|
+
return
|
|
974
|
+
|
|
975
|
+
# Geometric Prediction Solutions
|
|
976
|
+
def check_symmetric(self, precision=3, Nincr=180):
|
|
977
|
+
"""checks if the section is symmetric about the x and y axis, by finding the intersection of"""
|
|
978
|
+
if isinstance(self.shape, Geometry):
|
|
979
|
+
shape = self.shape.geom
|
|
980
|
+
else:
|
|
981
|
+
shape = self.shape
|
|
982
|
+
x, y = (np.array(v) for v in shape.exterior.coords.xy)
|
|
983
|
+
xc = shape.centroid.x
|
|
984
|
+
yc = shape.centroid.y
|
|
985
|
+
dx = x - xc
|
|
986
|
+
dy = y - yc
|
|
987
|
+
|
|
988
|
+
dth = np.arctan2(dx, dy)
|
|
989
|
+
rth = (dx**2 + dy**2) ** 0.5
|
|
990
|
+
inx = np.cumsum(np.ones(dth.size)) - 1
|
|
991
|
+
|
|
992
|
+
Nincr = 1000
|
|
993
|
+
Imax = inx.max()
|
|
994
|
+
inx2 = np.linspace(0, Imax, Nincr)
|
|
995
|
+
oppo = lambda i: (i + Imax / 2) % Imax
|
|
996
|
+
|
|
997
|
+
R = np.interp(inx2, inx, rth)
|
|
998
|
+
T = np.interp(inx2, inx, dth)
|
|
999
|
+
X = np.cos(T) * R
|
|
1000
|
+
Y = np.sin(T) * R
|
|
1001
|
+
|
|
1002
|
+
# finds set of radius at a given angle
|
|
1003
|
+
def find_radii(targetth):
|
|
1004
|
+
"""targetth must be positive from 0->pi"""
|
|
1005
|
+
r = dth - targetth
|
|
1006
|
+
# print(r)
|
|
1007
|
+
if np.all(r < 0):
|
|
1008
|
+
r = r + np.pi
|
|
1009
|
+
elif np.all(r > 0):
|
|
1010
|
+
r = r - np.pi
|
|
1011
|
+
sgn = np.concatenate([r[1:] * r[:-1], [r[0] * r[-1]]])
|
|
1012
|
+
possibles = np.where(sgn < 0)[0]
|
|
1013
|
+
# print(f'\n{targetth}')
|
|
1014
|
+
out = set()
|
|
1015
|
+
for poss in possibles:
|
|
1016
|
+
poss2 = int((poss + 1) % Imax)
|
|
1017
|
+
x_ = np.array([r[poss], r[poss2]])
|
|
1018
|
+
y_ = np.array([inx[poss], inx[poss2]])
|
|
1019
|
+
itarget = (0 - x_[0]) * (y_[1] - y_[0]) / (x_[1] - x_[0]) + y_[0]
|
|
1020
|
+
r_ = np.interp(itarget, inx2, R)
|
|
1021
|
+
out.add(round(r_, precision))
|
|
1022
|
+
# print(itarget,r_)
|
|
1023
|
+
return out
|
|
1024
|
+
|
|
1025
|
+
# check symmetry from 0-180 deg
|
|
1026
|
+
syms = []
|
|
1027
|
+
for targetth in np.linspace(0, np.pi, Nincr):
|
|
1028
|
+
o1 = find_radii(targetth)
|
|
1029
|
+
o2 = find_radii(targetth - np.pi)
|
|
1030
|
+
sym_point = len(o1.intersection(o2)) >= 1
|
|
1031
|
+
syms.append(sym_point)
|
|
1032
|
+
|
|
1033
|
+
return np.all(syms)
|
|
1034
|
+
|
|
1035
|
+
def __hash__(self):
|
|
1036
|
+
"""uniqueness based on geometry and material"""
|
|
1037
|
+
# print('hash shape...')
|
|
1038
|
+
if isinstance(self.shape, Geometry):
|
|
1039
|
+
shape = self.shape.geom
|
|
1040
|
+
else:
|
|
1041
|
+
shape = self.shape
|
|
1042
|
+
vals = (
|
|
1043
|
+
shape.wkb_hex,
|
|
1044
|
+
str(hash(self.material)),
|
|
1045
|
+
str(self.goal_elements),
|
|
1046
|
+
str(self.min_mesh_angle),
|
|
1047
|
+
)
|
|
1048
|
+
h = hashlib.md5()
|
|
1049
|
+
for hv in vals:
|
|
1050
|
+
h.update(hv.encode())
|
|
1051
|
+
return int(h.hexdigest(), 16)
|
|
1052
|
+
|
|
1053
|
+
def hash_id(self) -> str:
|
|
1054
|
+
"""string for saving to persisting"""
|
|
1055
|
+
return str(hash(self))
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
ALL_CROSSSECTIONS = [
|
|
1059
|
+
cs for cs in locals() if type(cs) is type and issubclass(cs, Profile2D)
|
|
1060
|
+
]
|