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,2228 @@
|
|
|
1
|
+
import attr
|
|
2
|
+
import numpy
|
|
3
|
+
import functools
|
|
4
|
+
import shapely
|
|
5
|
+
import pandas
|
|
6
|
+
from shapely.geometry import Polygon, Point
|
|
7
|
+
|
|
8
|
+
from engforge.tabulation import (
|
|
9
|
+
TABLE_TYPES,
|
|
10
|
+
NUMERIC_VALIDATOR,
|
|
11
|
+
system_property,
|
|
12
|
+
)
|
|
13
|
+
from engforge.configuration import forge, Configuration
|
|
14
|
+
from engforge.components import Component
|
|
15
|
+
|
|
16
|
+
# from engforge.analysis import Analysis
|
|
17
|
+
from engforge.properties import system_property
|
|
18
|
+
from engforge.system import System
|
|
19
|
+
from engforge.component_collections import ComponentDict
|
|
20
|
+
from engforge.eng.solid_materials import *
|
|
21
|
+
from engforge.common import *
|
|
22
|
+
from engforge.logging import log, LoggingMixin
|
|
23
|
+
from engforge.eng.costs import CostModel, cost_property
|
|
24
|
+
from engforge.attr_slots import Slot
|
|
25
|
+
from engforge.eng.prediction import PredictionMixin
|
|
26
|
+
from engforge.problem_context import ProblemExec
|
|
27
|
+
|
|
28
|
+
import sectionproperties
|
|
29
|
+
import sectionproperties.pre.geometry as geometry
|
|
30
|
+
import sectionproperties.pre.library.primitive_sections as sections
|
|
31
|
+
import Pynite as pynite
|
|
32
|
+
import numpy as np
|
|
33
|
+
from scipy import optimize as sciopt
|
|
34
|
+
import pandas as pd
|
|
35
|
+
import collections
|
|
36
|
+
import numpy as np
|
|
37
|
+
import itertools as itert
|
|
38
|
+
import asyncio
|
|
39
|
+
import weakref
|
|
40
|
+
from sklearn import svm
|
|
41
|
+
|
|
42
|
+
from engforge.typing import Options
|
|
43
|
+
from engforge.eng.structure_beams import Beam, rotation_matrix_from_vectors
|
|
44
|
+
|
|
45
|
+
ray_ok = False
|
|
46
|
+
try:
|
|
47
|
+
import ray
|
|
48
|
+
|
|
49
|
+
ray_ok = True
|
|
50
|
+
except Exception as e:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class StructureLog(LoggingMixin):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
log = StructureLog()
|
|
59
|
+
|
|
60
|
+
SECTIONS = {
|
|
61
|
+
k: v
|
|
62
|
+
for k, v in filter(
|
|
63
|
+
lambda kv: (
|
|
64
|
+
issubclass(kv[1], geometry.Geometry) if type(kv[1]) is type else False
|
|
65
|
+
),
|
|
66
|
+
sections.__dict__.items(),
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class StopAnalysis(Exception):
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
nonetype = type(None)
|
|
76
|
+
|
|
77
|
+
norm = np.linalg.norm
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def sort_max_estimated_failures(failures):
|
|
81
|
+
"""a generator to iterated through (beam,combo,x) as they return the highest fail_fraction for structures"""
|
|
82
|
+
|
|
83
|
+
failures = {
|
|
84
|
+
(beamnm, combo, x): dat
|
|
85
|
+
for beamnm, estfail in failures.items()
|
|
86
|
+
for (combo, x), dat in estfail.items()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (beamnm, combo, x), dat in sorted(
|
|
90
|
+
failures.items(), key=lambda kv: kv[1]["fail_frac"], reverse=True
|
|
91
|
+
):
|
|
92
|
+
yield (beamnm, combo, x), dat
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def check_failures(failures: dict) -> bool:
|
|
96
|
+
"""check if any failures exist in the nested dictonary and return True if there are"""
|
|
97
|
+
|
|
98
|
+
if "mesh_failures" in failures and failures["mesh_failures"]:
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
if "actual" in failures and failures["actual"]:
|
|
102
|
+
for bm, cases in failures["actual"].items():
|
|
103
|
+
for (combo, x), dat in cases.items():
|
|
104
|
+
if "fails" in dat and dat["fails"]:
|
|
105
|
+
return True
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def check_est_failures(failures: dict, top=True) -> bool:
|
|
110
|
+
"""check if any failures exist in the nested dictonary and return True if there are"""
|
|
111
|
+
for bm, cases in failures.items():
|
|
112
|
+
if isinstance(cases, dict):
|
|
113
|
+
if top:
|
|
114
|
+
log.info(f"checking est fail {bm}")
|
|
115
|
+
for k, v in cases.items():
|
|
116
|
+
if isinstance(v, dict):
|
|
117
|
+
val = check_est_failures(v, top=False)
|
|
118
|
+
if val:
|
|
119
|
+
return True
|
|
120
|
+
elif v:
|
|
121
|
+
return True
|
|
122
|
+
elif cases:
|
|
123
|
+
return True
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# Mesh Utils
|
|
128
|
+
def quad_stress_tensor(
|
|
129
|
+
quad, r=0, s=0, combo="gravity", inf_val=1e24, filter_infeasable=True
|
|
130
|
+
):
|
|
131
|
+
"""determines stresses from a plate or quad"""
|
|
132
|
+
q = quad
|
|
133
|
+
S_xx, S_yy, Txy = q.membrane(r, s, combo)
|
|
134
|
+
Qx, Qy = q.shear(r, s, combo)
|
|
135
|
+
|
|
136
|
+
S_xx, S_yy, Txy = float(S_xx), float(S_yy), float(Txy)
|
|
137
|
+
|
|
138
|
+
T_zx = float(Qx) / q.t
|
|
139
|
+
T_yz = float(Qy) / q.t
|
|
140
|
+
|
|
141
|
+
out = np.array([[S_xx, Txy, T_zx], [Txy, S_yy, T_yz], [T_zx, T_yz, 0.0]])
|
|
142
|
+
|
|
143
|
+
if filter_infeasable:
|
|
144
|
+
# nan converted to zero
|
|
145
|
+
out[np.isnan(out)] = 0
|
|
146
|
+
|
|
147
|
+
# convert infite value to a maximum
|
|
148
|
+
infinx = np.isinf(out)
|
|
149
|
+
infs = out[infinx]
|
|
150
|
+
out[infinx] = np.sign(infs) * inf_val
|
|
151
|
+
|
|
152
|
+
return out
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def node_pt(node):
|
|
156
|
+
return np.array([node.X, node.Y, node.Z])
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def node_info(quad, material):
|
|
160
|
+
# make point arrays
|
|
161
|
+
ind = node_pt(quad.i_node)
|
|
162
|
+
jnd = node_pt(quad.j_node)
|
|
163
|
+
nnd = node_pt(quad.n_node)
|
|
164
|
+
mnd = node_pt(quad.m_node)
|
|
165
|
+
|
|
166
|
+
# area of triangle = 0.5 (V1 x V2)
|
|
167
|
+
C1 = np.cross(jnd - ind, nnd - ind)
|
|
168
|
+
C2 = np.cross(jnd - mnd, nnd - mnd)
|
|
169
|
+
|
|
170
|
+
# info!
|
|
171
|
+
A = float((norm(C1) + norm(C2)) * 0.5)
|
|
172
|
+
V = A * quad.t
|
|
173
|
+
mass = V * material.density
|
|
174
|
+
|
|
175
|
+
# output
|
|
176
|
+
out = dict(
|
|
177
|
+
A=A,
|
|
178
|
+
cog=(ind + jnd + mnd + mnd) / 4,
|
|
179
|
+
V=V,
|
|
180
|
+
mass=mass,
|
|
181
|
+
cost=mass * material.cost_per_kg,
|
|
182
|
+
allowable_stress=material.allowable_stress,
|
|
183
|
+
)
|
|
184
|
+
return out
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def calculate_von_mises_stress(stress_tensor):
|
|
188
|
+
e_val, e_vec = np.linalg.eigh(stress_tensor)
|
|
189
|
+
p3, p2, p1 = np.sort(e_val)
|
|
190
|
+
return 0.707 * ((p1 - p2) ** 2 + (p2 - p3) ** 2 + (p1 - p3) ** 2) ** 0.5
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def calculate_quad_vonmises(quad, combo) -> dict:
|
|
194
|
+
"""looks at 9 points over a quad to determine its von mises stresses
|
|
195
|
+
:returns: a dictinary of"""
|
|
196
|
+
qvm = {}
|
|
197
|
+
for r, s in itert.product([-1, 0, 1], [-1, 0, 1]):
|
|
198
|
+
S = quad_stress_tensor(q, r, s, combo)
|
|
199
|
+
qvm[(r, s)] = calculate_von_mises_stress(S)
|
|
200
|
+
return qvm
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@forge
|
|
204
|
+
class BeamDict(ComponentDict):
|
|
205
|
+
component_type: type = attrs.field(default=Beam)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# TODO: Make analysis, where each load case is a row, but how sytactically?
|
|
209
|
+
@forge
|
|
210
|
+
class Structure(System, CostModel, PredictionMixin):
|
|
211
|
+
"""A integration between sectionproperties and Pynite, with a focus on ease of use
|
|
212
|
+
|
|
213
|
+
Right now we just need an integration between Sections+Materials and Members, to find, CG, and inertial components
|
|
214
|
+
|
|
215
|
+
Possible Future Additions:
|
|
216
|
+
1) Structure Motion (free body ie vehicle , multi-strucutre ie robots)
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
frame = None # pynite.FEModel3D
|
|
220
|
+
|
|
221
|
+
# _beams: dict = None
|
|
222
|
+
_materials: dict = None
|
|
223
|
+
|
|
224
|
+
default_material: SolidMaterial = attrs.field(default=None)
|
|
225
|
+
|
|
226
|
+
solve_method = Options("linear", "normal", "pdelta")
|
|
227
|
+
|
|
228
|
+
# Default Loading!
|
|
229
|
+
add_gravity_force: bool = attrs.field(default=True)
|
|
230
|
+
gravity_dir: str = attrs.field(default="FZ")
|
|
231
|
+
gravity_mag: float = attrs.field(default=9.81)
|
|
232
|
+
gravity_scalar: float = attrs.field(default=-1)
|
|
233
|
+
gravity_name: str = attrs.field(default="gravity")
|
|
234
|
+
gravity_cases: list = attrs.field(default=["gravity"])
|
|
235
|
+
default_case: str = attrs.field(default="gravity")
|
|
236
|
+
default_combo: str = attrs.field(default="gravity")
|
|
237
|
+
|
|
238
|
+
# this orchestrates load retrieval
|
|
239
|
+
check_statics: bool = attrs.field(default=True)
|
|
240
|
+
current_combo: str = attrs.field(default="gravity")
|
|
241
|
+
# iteration combos iterates over LoadCombos by default
|
|
242
|
+
iteration_combos: list = attrs.field(default=None)
|
|
243
|
+
|
|
244
|
+
# merge_duplicate_nodes by default
|
|
245
|
+
merge_nodes: bool = attrs.field(default=True)
|
|
246
|
+
tolerance: float = attrs.field(default=1e-3)
|
|
247
|
+
|
|
248
|
+
# beams
|
|
249
|
+
beams = Slot.define_iterator(BeamDict, wide=True)
|
|
250
|
+
|
|
251
|
+
# per execute failure analysis
|
|
252
|
+
# calculate actual failure will estimate then run failure analysis for bad cases
|
|
253
|
+
# calculate full failure will run failure analysis without stoping at first failure
|
|
254
|
+
calculate_actual_failure: bool = attrs.field(default=True)
|
|
255
|
+
calculate_full_failure: bool = attrs.field(default=False)
|
|
256
|
+
failure_solve_method = Options("bisect", "root")
|
|
257
|
+
failure_records: list = attrs.field(default=None)
|
|
258
|
+
|
|
259
|
+
# prediciton calculation
|
|
260
|
+
prediction: bool = attrs.field(default=False)
|
|
261
|
+
max_records: int = attrs.field(default=10000)
|
|
262
|
+
_prediction_parms: list = attrs.field(default=None)
|
|
263
|
+
|
|
264
|
+
min_rec = 10
|
|
265
|
+
near_margin = 0.1
|
|
266
|
+
max_margin = 1.5
|
|
267
|
+
max_guess = 1e8
|
|
268
|
+
|
|
269
|
+
current_failure_summary = None
|
|
270
|
+
_always_save_data = True # we dont respond to inputs so use this
|
|
271
|
+
_meshes = None
|
|
272
|
+
_any_solved = False
|
|
273
|
+
|
|
274
|
+
def __on_init__(self):
|
|
275
|
+
self.current_failure_summary = None
|
|
276
|
+
self._current_combo_failure_analysis = None
|
|
277
|
+
self._materials = weakref.WeakValueDictionary()
|
|
278
|
+
self._meshes = weakref.WeakValueDictionary()
|
|
279
|
+
self.initalize_structure()
|
|
280
|
+
self.frame.add_load_combo(
|
|
281
|
+
self.default_combo, {self.gravity_name: 1.0}, "gravity"
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# turn on prediction of failures with these models
|
|
285
|
+
if self.prediction:
|
|
286
|
+
d = {
|
|
287
|
+
"fails": {
|
|
288
|
+
"mod": svm.SVC(C=1000, gamma=0.1, probability=True),
|
|
289
|
+
"N": 0,
|
|
290
|
+
},
|
|
291
|
+
"fail_frac": {"mod": svm.SVR(C=1, gamma=0.1), "N": 0},
|
|
292
|
+
"beam_fail_frac": {"mod": svm.SVR(C=1, gamma=0.1), "N": 0},
|
|
293
|
+
"mesh_fail_frac": {"mod": svm.SVR(C=1, gamma=0.1), "N": 0},
|
|
294
|
+
}
|
|
295
|
+
self._prediction_models = d
|
|
296
|
+
|
|
297
|
+
# finally create the structure
|
|
298
|
+
self.create_structure()
|
|
299
|
+
|
|
300
|
+
def create_structure(self):
|
|
301
|
+
"""
|
|
302
|
+
Use this method to create a structure on init.
|
|
303
|
+
|
|
304
|
+
# Example code for creating a structure
|
|
305
|
+
frame = Structure()
|
|
306
|
+
frame.add_material("default_material", 2E9, 8E8, 0.3, 7850)
|
|
307
|
+
|
|
308
|
+
# Add nodes
|
|
309
|
+
node1 = frame.add_node("Node1", 0, 0, 0)
|
|
310
|
+
node2 = frame.add_node("Node2", 0, 0, 10)
|
|
311
|
+
|
|
312
|
+
# Add elements
|
|
313
|
+
element = frame.add_member("Element1", "Node1", "Node2", "default_material")
|
|
314
|
+
|
|
315
|
+
# Add supports
|
|
316
|
+
frame.def_support("Node1", True, True, True, True, True, True)
|
|
317
|
+
frame.def_support("Node2", True, True, True, True, True, True)
|
|
318
|
+
|
|
319
|
+
# Add loads
|
|
320
|
+
self.add_node_load("Node2", "FZ", -100)
|
|
321
|
+
|
|
322
|
+
# Solve the structure
|
|
323
|
+
self.run() #calls execute!
|
|
324
|
+
"""
|
|
325
|
+
pass
|
|
326
|
+
|
|
327
|
+
# Execution
|
|
328
|
+
def execute(self, combos: list = None, save=True, record=True, *args, **kwargs):
|
|
329
|
+
"""wrapper allowing saving of data by load combo"""
|
|
330
|
+
|
|
331
|
+
# the string input case, with csv support
|
|
332
|
+
if isinstance(combos, str):
|
|
333
|
+
combos = combos.split(",") # will be a list!
|
|
334
|
+
|
|
335
|
+
self.info(f"running with combos: {combos} | {args} | {kwargs} ")
|
|
336
|
+
self._run_id = int(uuid.uuid4())
|
|
337
|
+
iteration_combos = self.iteration_combos
|
|
338
|
+
if iteration_combos is None:
|
|
339
|
+
iteration_combos = list(self.LoadCombos.keys())
|
|
340
|
+
|
|
341
|
+
for combo in iteration_combos:
|
|
342
|
+
if combos is not None and combo not in combos:
|
|
343
|
+
continue
|
|
344
|
+
with ProblemExec(self, {}) as px:
|
|
345
|
+
# reset cached failures
|
|
346
|
+
self.current_failure_summary = None
|
|
347
|
+
self._current_combo_failure_analysis = None
|
|
348
|
+
|
|
349
|
+
# run the analysis
|
|
350
|
+
# self.index += 1
|
|
351
|
+
combo_possible = self.struct_pre_execute(combo) # can change combo
|
|
352
|
+
if combo_possible:
|
|
353
|
+
self.current_combo = combo_possible
|
|
354
|
+
else:
|
|
355
|
+
self.current_combo = combo
|
|
356
|
+
combo = self.current_combo
|
|
357
|
+
|
|
358
|
+
self.info(
|
|
359
|
+
f"running load combo : {combo} with solver {self.solve_method}"
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
if "check_statics" not in kwargs:
|
|
363
|
+
kwargs["check_statics"] = self.check_statics
|
|
364
|
+
|
|
365
|
+
if self.solve_method == "linear":
|
|
366
|
+
self.analyze_linear(load_combos=[combo], *args, **kwargs)
|
|
367
|
+
elif self.solve_method == "normal":
|
|
368
|
+
self.analyze(load_combos=[combo], *args, **kwargs)
|
|
369
|
+
elif self.solve_method == "pdelta":
|
|
370
|
+
self.analyze_PDelta(load_combos=[combo], *args, **kwargs)
|
|
371
|
+
|
|
372
|
+
self._any_solved = True # Flag system properties to save
|
|
373
|
+
self.struct_post_execute(combo)
|
|
374
|
+
|
|
375
|
+
# backup data saver.
|
|
376
|
+
self.current_failures() # cache failures
|
|
377
|
+
self._anything_changed = True # change costs
|
|
378
|
+
|
|
379
|
+
if save:
|
|
380
|
+
# self.save_data(index=self.index, force=True)
|
|
381
|
+
px.save_data()
|
|
382
|
+
|
|
383
|
+
# record results for prediction
|
|
384
|
+
if record and self.prediction:
|
|
385
|
+
res = self._prediction_record
|
|
386
|
+
if res:
|
|
387
|
+
self.record_result(res)
|
|
388
|
+
|
|
389
|
+
# Solver Mechanics
|
|
390
|
+
def struct_root_failure(
|
|
391
|
+
self,
|
|
392
|
+
x,
|
|
393
|
+
fail_parm,
|
|
394
|
+
base_kw,
|
|
395
|
+
mult=1,
|
|
396
|
+
target=1,
|
|
397
|
+
sols=None,
|
|
398
|
+
save=False,
|
|
399
|
+
*args,
|
|
400
|
+
**kw,
|
|
401
|
+
):
|
|
402
|
+
assert target > 0, "target must be positive"
|
|
403
|
+
bkw = base_kw.copy()
|
|
404
|
+
bkw[fail_parm] = x * mult
|
|
405
|
+
self.setattrs(bkw)
|
|
406
|
+
self.info(f"solving root iter: {bkw}")
|
|
407
|
+
if args or kw:
|
|
408
|
+
self.info(f"adding args to solver: {args} | {kw}")
|
|
409
|
+
self.execute(save=save, *args, **kw)
|
|
410
|
+
wrong_side = (mult * x) < 0
|
|
411
|
+
ff = self.max_est_fail_frac
|
|
412
|
+
bf = self.max_beam_est_fail_frac
|
|
413
|
+
mf = self.max_mesh_est_fail_frac
|
|
414
|
+
if wrong_side:
|
|
415
|
+
# min = 1 with an gradual but exponential value from 0
|
|
416
|
+
l10 = max(np.log10(abs(ff) + 1), 1) + 1
|
|
417
|
+
exp = min(abs(ff), l10) ** 0.5
|
|
418
|
+
res = max(target * np.e**exp, target)
|
|
419
|
+
else:
|
|
420
|
+
res = target - ff
|
|
421
|
+
|
|
422
|
+
if sols is not None:
|
|
423
|
+
sols.append({"x": x, "ff": ff, "obj": res, "kw": bkw, "mf": mf, "bf": bf})
|
|
424
|
+
|
|
425
|
+
self.info(f"ran: {bkw} -> {ff:5.4f} | {res:5.4f}")
|
|
426
|
+
return res
|
|
427
|
+
|
|
428
|
+
def determine_failure_load(
|
|
429
|
+
self,
|
|
430
|
+
fail_parm,
|
|
431
|
+
base_kw,
|
|
432
|
+
mult=1,
|
|
433
|
+
target_failure_frac=1.0,
|
|
434
|
+
guess=None,
|
|
435
|
+
tol=5e-3,
|
|
436
|
+
return_sol=False,
|
|
437
|
+
max_tries=5,
|
|
438
|
+
*args,
|
|
439
|
+
**kw,
|
|
440
|
+
):
|
|
441
|
+
# TODO: add reversion logic
|
|
442
|
+
if not hasattr(self, "_max_parm_dict"):
|
|
443
|
+
self._max_parm_dict = {}
|
|
444
|
+
tries = 0
|
|
445
|
+
while tries < max_tries:
|
|
446
|
+
try:
|
|
447
|
+
if guess is None:
|
|
448
|
+
guess = mult * 1e6
|
|
449
|
+
if fail_parm not in self._max_parm_dict:
|
|
450
|
+
self._max_parm_dict[fail_parm] = self.max_guess
|
|
451
|
+
maxx = self.max_guess * mult
|
|
452
|
+
else:
|
|
453
|
+
self.max_guess = self._max_parm_dict[fail_parm]
|
|
454
|
+
maxx = self.max_guess * mult
|
|
455
|
+
|
|
456
|
+
solutions = []
|
|
457
|
+
|
|
458
|
+
# allow passing of args to solver
|
|
459
|
+
if args or kw:
|
|
460
|
+
self.info(f"adding args to solver: {args} | {kw}")
|
|
461
|
+
func = lambda x, *argsolv: self.struct_root_failure(
|
|
462
|
+
x, *argsolv, *args, **kw
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
if self.failure_solve_method == "bisect":
|
|
466
|
+
ans = sciopt.root_scalar(
|
|
467
|
+
func,
|
|
468
|
+
x0=1000,
|
|
469
|
+
x1=guess,
|
|
470
|
+
rtol=tol,
|
|
471
|
+
xtol=tol,
|
|
472
|
+
args=(
|
|
473
|
+
fail_parm,
|
|
474
|
+
base_kw,
|
|
475
|
+
mult,
|
|
476
|
+
target_failure_frac,
|
|
477
|
+
solutions,
|
|
478
|
+
),
|
|
479
|
+
bracket=(0, maxx),
|
|
480
|
+
)
|
|
481
|
+
self.info(
|
|
482
|
+
f"{mult}x{fail_parm:<6}| success: {ans.converged} , ans:{ans.root}, base: {base_kw}"
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
else: # root
|
|
486
|
+
ans = sciopt.root_scalar(
|
|
487
|
+
func,
|
|
488
|
+
x0=100,
|
|
489
|
+
x1=guess,
|
|
490
|
+
rtol=tol,
|
|
491
|
+
xtol=tol,
|
|
492
|
+
args=(
|
|
493
|
+
fail_parm,
|
|
494
|
+
base_kw,
|
|
495
|
+
mult,
|
|
496
|
+
target_failure_frac,
|
|
497
|
+
solutions,
|
|
498
|
+
),
|
|
499
|
+
)
|
|
500
|
+
self.info(
|
|
501
|
+
f"{mult}x{fail_parm:<6}| success: {ans.converged} , ans:{ans.root}, base: {base_kw}"
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
if return_sol:
|
|
505
|
+
return solutions
|
|
506
|
+
|
|
507
|
+
if ans.converged:
|
|
508
|
+
self.setattrs({fail_parm: ans.root})
|
|
509
|
+
self.execute(
|
|
510
|
+
save=True,
|
|
511
|
+
*args,
|
|
512
|
+
**{k: v for k, v in kw.items() if k != "save"},
|
|
513
|
+
)
|
|
514
|
+
return ans.root
|
|
515
|
+
|
|
516
|
+
elif solutions:
|
|
517
|
+
x = np.array([s["x"] for s in solutions])
|
|
518
|
+
f = np.array([s["ff"] for s in solutions])
|
|
519
|
+
b = np.array([s["bf"] for s in solutions])
|
|
520
|
+
m = np.array([s["mf"] for s in solutions])
|
|
521
|
+
return # TODO: fit the best solution using regression
|
|
522
|
+
return np.nan
|
|
523
|
+
|
|
524
|
+
except ValueError as e:
|
|
525
|
+
# increase margin
|
|
526
|
+
self.info(f"increasing {fail_parm} max guess by 100x")
|
|
527
|
+
new_max = self._max_parm_dict[fail_parm] * 100
|
|
528
|
+
self._max_parm_dict[fail_parm] = new_max
|
|
529
|
+
tries += 1
|
|
530
|
+
|
|
531
|
+
except Exception as e:
|
|
532
|
+
self.error(f"unknown error: {e}")
|
|
533
|
+
raise e
|
|
534
|
+
|
|
535
|
+
def _X_prediction_dict(self, base_obj=None) -> dict:
|
|
536
|
+
if not self.prediction or not self._prediction_parms:
|
|
537
|
+
return {}
|
|
538
|
+
if base_obj is None:
|
|
539
|
+
base_obj = self
|
|
540
|
+
d = {k: getattr(base_obj, k, None) for k in self._prediction_parms}
|
|
541
|
+
return d
|
|
542
|
+
|
|
543
|
+
@property
|
|
544
|
+
def _prediction_record(self) -> dict:
|
|
545
|
+
"""returns the analysis result for prediction"""
|
|
546
|
+
d = self._X_prediction_dict(self)
|
|
547
|
+
d["fail_frac"] = ff = self.max_est_fail_frac
|
|
548
|
+
d["beam_fail_frac"] = self.max_beam_est_fail_frac
|
|
549
|
+
d["mesh_fail_frac"] = self.max_mesh_est_fail_frac
|
|
550
|
+
d["fails"] = int(ff >= 1 - 1e-6)
|
|
551
|
+
return d
|
|
552
|
+
|
|
553
|
+
def record_result(self, stress_dict):
|
|
554
|
+
"""determines if stress record should be added to prediction_records"""
|
|
555
|
+
if not self.prediction:
|
|
556
|
+
return
|
|
557
|
+
|
|
558
|
+
if len(self.prediction_records) > self.max_records:
|
|
559
|
+
return
|
|
560
|
+
|
|
561
|
+
ff = stress_dict["fail_frac"]
|
|
562
|
+
|
|
563
|
+
# Add the data to the stress records
|
|
564
|
+
if self.near_margin and abs(ff - 1) < self.near_margin:
|
|
565
|
+
self.add_prediction_record(stress_dict)
|
|
566
|
+
elif self.max_margin and ff < self.max_margin * 2:
|
|
567
|
+
self.add_prediction_record(stress_dict, False)
|
|
568
|
+
elif not self.near_margin and not self.max_margin:
|
|
569
|
+
self.add_prediction_record(stress_dict, False)
|
|
570
|
+
|
|
571
|
+
def save_data(self, *args, **kw):
|
|
572
|
+
if self._any_solved:
|
|
573
|
+
super().save_data(*args, **kw)
|
|
574
|
+
# self._any_solved = False
|
|
575
|
+
else:
|
|
576
|
+
self.info(f"nothing to save, run() structure first")
|
|
577
|
+
|
|
578
|
+
def struct_pre_execute(self, combo=None):
|
|
579
|
+
"""yours to override to prep solver or dataframe
|
|
580
|
+
:returns: combo to run, or None if want to continue with current iteration combos
|
|
581
|
+
"""
|
|
582
|
+
pass
|
|
583
|
+
|
|
584
|
+
def struct_post_execute(self, combo=None):
|
|
585
|
+
"""yours to override dataframe"""
|
|
586
|
+
pass
|
|
587
|
+
|
|
588
|
+
def initalize_structure(self):
|
|
589
|
+
self.frame = pynite.FEModel3D()
|
|
590
|
+
# self.beams = {} # this is for us!
|
|
591
|
+
self._materials = {}
|
|
592
|
+
self.debug("created frame...")
|
|
593
|
+
|
|
594
|
+
if self.default_material:
|
|
595
|
+
material = self.default_material
|
|
596
|
+
uid = material.unique_id
|
|
597
|
+
if uid not in self._materials:
|
|
598
|
+
self.debug(f"add material {uid}")
|
|
599
|
+
self.frame.add_material(
|
|
600
|
+
uid, material.E, material.G, material.nu, material.rho
|
|
601
|
+
)
|
|
602
|
+
self._materials[uid] = material
|
|
603
|
+
|
|
604
|
+
# Merged Feature Calls
|
|
605
|
+
def add_eng_material(self, material: "SolidMaterial") -> str:
|
|
606
|
+
"""adds the material to the structure and returns the pynite id"""
|
|
607
|
+
if material.unique_id not in self._materials:
|
|
608
|
+
self.add_material(
|
|
609
|
+
material.unique_id,
|
|
610
|
+
material.E,
|
|
611
|
+
material.G,
|
|
612
|
+
material.nu,
|
|
613
|
+
material.rho,
|
|
614
|
+
)
|
|
615
|
+
self._materials[material.unique_id] = material
|
|
616
|
+
|
|
617
|
+
return material.unique_id
|
|
618
|
+
|
|
619
|
+
def mesh_extents(self) -> dict:
|
|
620
|
+
mesh_extents = {}
|
|
621
|
+
for mn, m in self.Meshes.items():
|
|
622
|
+
minX, maxX = None, None
|
|
623
|
+
minY, maxY = None, None
|
|
624
|
+
minZ, maxZ = None, None
|
|
625
|
+
for e in m.elements.values():
|
|
626
|
+
for nn in ["i", "j", "n", "m"]:
|
|
627
|
+
node = getattr(e, f"{nn}_node")
|
|
628
|
+
X, Y, Z = node.X, node.Y, node.Z
|
|
629
|
+
if minX is None or X < minX:
|
|
630
|
+
minX = X
|
|
631
|
+
if maxX is None or X > maxX:
|
|
632
|
+
maxX = X
|
|
633
|
+
if minY is None or Y < minY:
|
|
634
|
+
minY = Y
|
|
635
|
+
if maxY is None or Y > maxY:
|
|
636
|
+
maxY = Y
|
|
637
|
+
if minZ is None or Z < minZ:
|
|
638
|
+
minZ = Z
|
|
639
|
+
if maxZ is None or Z > maxZ:
|
|
640
|
+
maxZ = Z
|
|
641
|
+
mesh_extents[mn] = dict(
|
|
642
|
+
minX=minX, maxX=maxX, minY=minY, maxY=maxY, minZ=minZ, maxZ=maxZ
|
|
643
|
+
)
|
|
644
|
+
return mesh_extents
|
|
645
|
+
|
|
646
|
+
@property
|
|
647
|
+
def quad_info(self) -> dict:
|
|
648
|
+
"""return a dictonary of quads with their mass properties like cog, mass, area and volume"""
|
|
649
|
+
if hasattr(self, "_quad_info"):
|
|
650
|
+
return self._quad_info
|
|
651
|
+
|
|
652
|
+
self._quad_info = {}
|
|
653
|
+
for mn, mesh in self.Meshes.items():
|
|
654
|
+
for qn, q in mesh.elements.items():
|
|
655
|
+
mat = self._materials[mesh.material_name]
|
|
656
|
+
self._quad_info[qn] = node_info(q, mat)
|
|
657
|
+
|
|
658
|
+
return self._quad_info.copy()
|
|
659
|
+
|
|
660
|
+
def add_mesh(self, meshtype, *args, **kwargs):
|
|
661
|
+
"""maps to appropriate Pynite mesh generator but also applies loads
|
|
662
|
+
|
|
663
|
+
Currently these types are supported ['rect','annulus','cylinder','triangle']
|
|
664
|
+
"""
|
|
665
|
+
mesh = None
|
|
666
|
+
types = ["rect", "annulus", "cylinder", "triangle"]
|
|
667
|
+
if meshtype not in types:
|
|
668
|
+
raise KeyError(f"type {type} not in {types}")
|
|
669
|
+
|
|
670
|
+
if meshtype == "rect":
|
|
671
|
+
mesh = self.add_rectangle_mesh(*args, **kwargs)
|
|
672
|
+
elif meshtype == "annulus":
|
|
673
|
+
mesh = self.add_annulus_mesh(*args, **kwargs)
|
|
674
|
+
elif meshtype == "cylinder":
|
|
675
|
+
mesh = self.add_cylinder_mesh(*args, **kwargs)
|
|
676
|
+
elif meshtype == "triangle":
|
|
677
|
+
mesh = self.add_frustrum_mesh(*args, **kwargs)
|
|
678
|
+
elif meshtype == "cylinderset":
|
|
679
|
+
mesh = self.add_mesh_cylinders(*args, **kwargs)
|
|
680
|
+
|
|
681
|
+
assert mesh is not None, f"no mesh made!"
|
|
682
|
+
|
|
683
|
+
self._add_mesh(mesh, self.frame.Meshes[mesh])
|
|
684
|
+
|
|
685
|
+
def _add_mesh(self, meshname, mesh):
|
|
686
|
+
self._meshes[meshname] = mesh
|
|
687
|
+
|
|
688
|
+
# Ensure custom mesh is in frame meshes
|
|
689
|
+
if meshname not in self.frame.Meshes:
|
|
690
|
+
self.frame.Meshes[meshname] = mesh
|
|
691
|
+
|
|
692
|
+
if self.merge_nodes:
|
|
693
|
+
self.merge_duplicate_nodes(tolerance=self.tolerance)
|
|
694
|
+
|
|
695
|
+
if self.add_gravity_force:
|
|
696
|
+
self.apply_gravity_to_mesh(meshname)
|
|
697
|
+
|
|
698
|
+
def apply_gravity_to_mesh(self, meshname):
|
|
699
|
+
self.debug(f"applying gravity to {meshname}")
|
|
700
|
+
mesh = self._meshes[meshname]
|
|
701
|
+
|
|
702
|
+
mat = self._materials[mesh.material_name]
|
|
703
|
+
rho = mat.rho
|
|
704
|
+
|
|
705
|
+
for elid, elmt in mesh.elements.items():
|
|
706
|
+
# make node vectors
|
|
707
|
+
n_vecs = {}
|
|
708
|
+
nodes = {}
|
|
709
|
+
for nname in ["i_node", "j_node", "m_node", "n_node"]:
|
|
710
|
+
ni = getattr(elmt, nname)
|
|
711
|
+
nodes[nname] = ni
|
|
712
|
+
n_vecs[nname] = numpy.array([ni.X, ni.Y, ni.Z])
|
|
713
|
+
|
|
714
|
+
ni = n_vecs["i_node"]
|
|
715
|
+
nj = n_vecs["j_node"]
|
|
716
|
+
nm = n_vecs["m_node"]
|
|
717
|
+
nn = n_vecs["n_node"]
|
|
718
|
+
|
|
719
|
+
# find element area
|
|
720
|
+
im = ni - nm
|
|
721
|
+
jn = nj - nn
|
|
722
|
+
|
|
723
|
+
A = numpy.linalg.norm(numpy.cross(im, jn)) / 2
|
|
724
|
+
|
|
725
|
+
# get gravity_forcemultiply area x thickness x density/4
|
|
726
|
+
mass = A * elmt.t * rho
|
|
727
|
+
fg = mass * self.gravity_mag * self.gravity_scalar
|
|
728
|
+
fnode = fg / 4
|
|
729
|
+
|
|
730
|
+
# apply forces to corner nodes / 4
|
|
731
|
+
for case in self.gravity_cases:
|
|
732
|
+
for nname, disnode in nodes.items():
|
|
733
|
+
self.add_node_load(
|
|
734
|
+
disnode.name,
|
|
735
|
+
self.gravity_dir,
|
|
736
|
+
fnode,
|
|
737
|
+
case=self.gravity_name,
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
def add_member(self, name, node1, node2, section, material=None, **kwargs):
|
|
741
|
+
assert node1 in self.nodes
|
|
742
|
+
assert node2 in self.nodes
|
|
743
|
+
|
|
744
|
+
if material is None and self.default_material:
|
|
745
|
+
material = self.default_material
|
|
746
|
+
elif hasattr(section, "material"):
|
|
747
|
+
material = section.material
|
|
748
|
+
else:
|
|
749
|
+
raise ValueError("material not defined as input or from default sources!")
|
|
750
|
+
|
|
751
|
+
uid = material.unique_id
|
|
752
|
+
if uid not in self._materials:
|
|
753
|
+
self.debug(f"add material {uid}")
|
|
754
|
+
self.frame.add_material(
|
|
755
|
+
uid, material.E, material.G, material.nu, material.rho
|
|
756
|
+
)
|
|
757
|
+
self._materials[uid] = material
|
|
758
|
+
|
|
759
|
+
beam_attrs = {k: v for k, v in kwargs.items() if k in Beam.input_attrs()}
|
|
760
|
+
kwargs = {k: v for k, v in kwargs.items() if k not in beam_attrs}
|
|
761
|
+
|
|
762
|
+
B = beam = Beam(
|
|
763
|
+
structure=self,
|
|
764
|
+
name=name,
|
|
765
|
+
material=material,
|
|
766
|
+
section=section,
|
|
767
|
+
**beam_attrs,
|
|
768
|
+
)
|
|
769
|
+
self.beams[name] = beam
|
|
770
|
+
self.frame.add_member(
|
|
771
|
+
name,
|
|
772
|
+
i_node=node1,
|
|
773
|
+
j_node=node2,
|
|
774
|
+
material_name=uid,
|
|
775
|
+
Iy=B.Iy,
|
|
776
|
+
Iz=B.Ix,
|
|
777
|
+
J=B.J,
|
|
778
|
+
A=B.A,
|
|
779
|
+
**kwargs,
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
if self.add_gravity_force:
|
|
783
|
+
beam.apply_gravity_force(z_dir=self.gravity_dir, z_mag=self.gravity_scalar)
|
|
784
|
+
|
|
785
|
+
return beam
|
|
786
|
+
|
|
787
|
+
def add_member_with(
|
|
788
|
+
self, name, node1, node2, E, G, Iy, Ix, J, A, material_rho=0, **kwargs
|
|
789
|
+
):
|
|
790
|
+
"""a way to add specific beam properties to calculate stress,
|
|
791
|
+
This way will currently not caluclate resulatant beam load.
|
|
792
|
+
#TOOD: Add in a mock section_properties for our own stress calcs
|
|
793
|
+
"""
|
|
794
|
+
assert node1 in self.nodes
|
|
795
|
+
assert node2 in self.nodes
|
|
796
|
+
|
|
797
|
+
material = SolidMaterial(
|
|
798
|
+
density=material_rho, elastic_modulus=E, shear_modulus=G
|
|
799
|
+
)
|
|
800
|
+
uid = material.unique_id
|
|
801
|
+
if uid not in self._materials:
|
|
802
|
+
self.debug(f"add material {uid}")
|
|
803
|
+
self.frame.add_material(
|
|
804
|
+
uid, material.E, material.G, material.nu, material.rho
|
|
805
|
+
)
|
|
806
|
+
self._materials[uid] = material
|
|
807
|
+
|
|
808
|
+
B = beam = Beam(
|
|
809
|
+
structure=self,
|
|
810
|
+
name=name,
|
|
811
|
+
material_name=material,
|
|
812
|
+
in_Iy=Iy,
|
|
813
|
+
in_Ix=Ix,
|
|
814
|
+
in_J=J,
|
|
815
|
+
in_A=A,
|
|
816
|
+
section=None,
|
|
817
|
+
)
|
|
818
|
+
self.beams[name] = beam
|
|
819
|
+
|
|
820
|
+
if self.add_gravity_force:
|
|
821
|
+
beam.apply_gravity_force(z_dir=self.gravity_dir, z_mag=self.gravity_scalar)
|
|
822
|
+
|
|
823
|
+
self.frame.add_member(
|
|
824
|
+
name,
|
|
825
|
+
i_node=node1,
|
|
826
|
+
j_node=node2,
|
|
827
|
+
material=uid,
|
|
828
|
+
Iy=Iy,
|
|
829
|
+
Iz=Ix,
|
|
830
|
+
J=J,
|
|
831
|
+
A=A,
|
|
832
|
+
**kwargs,
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
return beam
|
|
836
|
+
|
|
837
|
+
# OUTPUT
|
|
838
|
+
|
|
839
|
+
def beam_dataframe(self, univ_parms: list = None, add_columns: list = None):
|
|
840
|
+
"""creates a dataframe entry for each beam and combo
|
|
841
|
+
:param univ_parms: keys represent the dataframe parm, and the values represent the lookup value
|
|
842
|
+
"""
|
|
843
|
+
|
|
844
|
+
df = self.dataframe
|
|
845
|
+
|
|
846
|
+
# determine the possible beam names
|
|
847
|
+
beams = set([f"beams_{k}" for k in self.members])
|
|
848
|
+
beam_cols = df.columns[df.columns.str.startswith("beams_")].tolist()
|
|
849
|
+
beam_parms = set(
|
|
850
|
+
[
|
|
851
|
+
col.replace(k + "_", "")
|
|
852
|
+
for col in beam_cols
|
|
853
|
+
for k in beams
|
|
854
|
+
if col.startswith(k + "_")
|
|
855
|
+
]
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
if univ_parms is None:
|
|
859
|
+
univ_parms = beam_parms # do not filter parms
|
|
860
|
+
|
|
861
|
+
if add_columns is None:
|
|
862
|
+
add_columns = []
|
|
863
|
+
|
|
864
|
+
# loop through parms and beams reorganizing the data:
|
|
865
|
+
# TODO: look up potential multi-index or pivot method for pandas
|
|
866
|
+
beam_data = []
|
|
867
|
+
for i in range(len(df)):
|
|
868
|
+
row = df.iloc[i]
|
|
869
|
+
add_dat = {k: row[k] for k in add_columns}
|
|
870
|
+
for beam in beams:
|
|
871
|
+
bc = add_dat.copy() # this is the data entry
|
|
872
|
+
bc["name"] = beam
|
|
873
|
+
for parm in beam_parms:
|
|
874
|
+
if parm not in univ_parms:
|
|
875
|
+
continue
|
|
876
|
+
# if parm not in row:
|
|
877
|
+
# continue
|
|
878
|
+
k = f"{beam}_{parm}"
|
|
879
|
+
if k in row:
|
|
880
|
+
v = row[k]
|
|
881
|
+
# if 'Z1' in k:
|
|
882
|
+
# print(v)
|
|
883
|
+
bc[parm] = v
|
|
884
|
+
|
|
885
|
+
beam_data.append(bc) # uno mas
|
|
886
|
+
|
|
887
|
+
dfb = pd.DataFrame(beam_data)
|
|
888
|
+
return dfb
|
|
889
|
+
|
|
890
|
+
@property
|
|
891
|
+
def cog(self):
|
|
892
|
+
XM_beam = numpy.sum(
|
|
893
|
+
[bm.mass * bm.centroid3d for bm in self.beams.values()], axis=0
|
|
894
|
+
)
|
|
895
|
+
XM_quad = numpy.sum(
|
|
896
|
+
[q["mass"] * q["cog"] for q in self.quad_info.values()], axis=0
|
|
897
|
+
)
|
|
898
|
+
XM = XM_beam + XM_quad
|
|
899
|
+
return XM / self.mass
|
|
900
|
+
|
|
901
|
+
@system_property
|
|
902
|
+
def mass(self) -> float:
|
|
903
|
+
"""sum of all beams and quad masses"""
|
|
904
|
+
return sum([sum(d.values()) for d in self.masses().values()])
|
|
905
|
+
|
|
906
|
+
def masses(self) -> dict:
|
|
907
|
+
"""return a dictionary of beam & quad mass"""
|
|
908
|
+
out = {}
|
|
909
|
+
out["beams"] = beams = {}
|
|
910
|
+
out["quads"] = quads = {}
|
|
911
|
+
for beamname, bm in self.beams.items():
|
|
912
|
+
beams[beamname] = bm.mass
|
|
913
|
+
for quadname, q in self.quad_info.items():
|
|
914
|
+
quads[quadname] = q["mass"]
|
|
915
|
+
return out
|
|
916
|
+
|
|
917
|
+
@property
|
|
918
|
+
def structure_cost_beams(self) -> float:
|
|
919
|
+
"""sum of all beams and quad cost"""
|
|
920
|
+
return sum([sum(self.costs["beams"].values())])
|
|
921
|
+
|
|
922
|
+
@property
|
|
923
|
+
def structure_cost_panels(self) -> float:
|
|
924
|
+
"""sum of all panels cost"""
|
|
925
|
+
return sum([sum(self.costs["quads"].values())])
|
|
926
|
+
|
|
927
|
+
@cost_property(category="mfg,material,panels")
|
|
928
|
+
def panels_cost(self):
|
|
929
|
+
return self.structure_cost_panels
|
|
930
|
+
|
|
931
|
+
@cost_property(category="mfg,material,beams")
|
|
932
|
+
def beam_cost(self) -> float:
|
|
933
|
+
"""sum of all beams cost"""
|
|
934
|
+
return sum([bm.cost for bm in self.beams.values()])
|
|
935
|
+
|
|
936
|
+
@system_property
|
|
937
|
+
def structure_cost(self) -> float:
|
|
938
|
+
"""sum of all beams and quad cost"""
|
|
939
|
+
return self.structure_cost_beams + self.structure_cost_panels
|
|
940
|
+
|
|
941
|
+
@solver_cached
|
|
942
|
+
def costs(self) -> dict:
|
|
943
|
+
"""return a dictionary of beam & quad costs"""
|
|
944
|
+
out = {}
|
|
945
|
+
out["beams"] = beams = {}
|
|
946
|
+
out["quads"] = quads = {}
|
|
947
|
+
for beamname, bm in self.beams.items():
|
|
948
|
+
beams[beamname] = bm.cost
|
|
949
|
+
for quadname, q in self.quad_info.items():
|
|
950
|
+
quads[quadname] = q["cost"]
|
|
951
|
+
return out
|
|
952
|
+
|
|
953
|
+
def visulize(self, **kwargs):
|
|
954
|
+
from Pynite import Visualization
|
|
955
|
+
|
|
956
|
+
if "combo_name" not in kwargs:
|
|
957
|
+
kwargs["combo_name"] = self.default_combo
|
|
958
|
+
Visualization.render_model(self.frame, **kwargs)
|
|
959
|
+
|
|
960
|
+
# TODO: add mesh stress / deflection info min/max ect.
|
|
961
|
+
@property
|
|
962
|
+
def node_dataframe(self):
|
|
963
|
+
# out = {}
|
|
964
|
+
rows = []
|
|
965
|
+
for case in self.frame.LoadCombos:
|
|
966
|
+
for node in self.nodes.values():
|
|
967
|
+
if case not in node.DX:
|
|
968
|
+
continue
|
|
969
|
+
row = {
|
|
970
|
+
"case": case,
|
|
971
|
+
"name": node.name,
|
|
972
|
+
"dx": node.DX[case],
|
|
973
|
+
"dy": node.DY[case],
|
|
974
|
+
"dz": node.DZ[case],
|
|
975
|
+
"rx": node.RX[case],
|
|
976
|
+
"ry": node.RY[case],
|
|
977
|
+
"rz": node.RZ[case],
|
|
978
|
+
"rxfx": node.RxnFX[case],
|
|
979
|
+
"rxfy": node.RxnFY[case],
|
|
980
|
+
"rxfz": node.RxnFZ[case],
|
|
981
|
+
"rxmx": node.RxnMX[case],
|
|
982
|
+
"rxmy": node.RxnMY[case],
|
|
983
|
+
"rxmz": node.RxnMZ[case],
|
|
984
|
+
}
|
|
985
|
+
rows.append(row)
|
|
986
|
+
|
|
987
|
+
# out[case] = pandas.DataFrame(rows)
|
|
988
|
+
df = pandas.DataFrame(rows)
|
|
989
|
+
df.set_index(["case", "name"], inplace=True)
|
|
990
|
+
return df
|
|
991
|
+
|
|
992
|
+
@property
|
|
993
|
+
def INERTIA(self):
|
|
994
|
+
"""Combines all the mass moments of inerita from the internal beams (and other future items!) into one for the sturcture with the parallel axis theorm"""
|
|
995
|
+
cg = self.cog
|
|
996
|
+
I = numpy.zeros((3, 3))
|
|
997
|
+
|
|
998
|
+
for name, beam in self.beams.items():
|
|
999
|
+
self.debug(f"adding {name} inertia...")
|
|
1000
|
+
I += beam.GLOBAL_INERTIA
|
|
1001
|
+
|
|
1002
|
+
for quad in self.quad_info.items():
|
|
1003
|
+
qcog = quad["cog"] - cg
|
|
1004
|
+
qmass = quad["mass"]
|
|
1005
|
+
|
|
1006
|
+
qI = qmass * np.eye(3) * qcog * qcog.T
|
|
1007
|
+
I += qI
|
|
1008
|
+
|
|
1009
|
+
return I
|
|
1010
|
+
|
|
1011
|
+
@property
|
|
1012
|
+
def Fg(self):
|
|
1013
|
+
"""force of gravity"""
|
|
1014
|
+
return numpy.array([0, 0, -self.mass * g])
|
|
1015
|
+
|
|
1016
|
+
def __getattr__(self, attr):
|
|
1017
|
+
# log.info(f'get {attr}')
|
|
1018
|
+
if attr in dir(self.frame):
|
|
1019
|
+
return getattr(self.frame, attr)
|
|
1020
|
+
# if attr in dir(self):
|
|
1021
|
+
# return getattr(self,attr)
|
|
1022
|
+
# raise AttributeError(f'{attr} not found!')
|
|
1023
|
+
return self.__getattribute__(attr)
|
|
1024
|
+
|
|
1025
|
+
def __dir__(self):
|
|
1026
|
+
return sorted(
|
|
1027
|
+
set(
|
|
1028
|
+
dir(super(Structure, self))
|
|
1029
|
+
+ dir(Structure)
|
|
1030
|
+
+ dir(self.frame)
|
|
1031
|
+
+ dir(self.__class__)
|
|
1032
|
+
)
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
@property
|
|
1036
|
+
def nodes(self):
|
|
1037
|
+
return self.frame.Nodes
|
|
1038
|
+
|
|
1039
|
+
@property
|
|
1040
|
+
def members(self):
|
|
1041
|
+
return self.frame.Members
|
|
1042
|
+
|
|
1043
|
+
# Remote Utility Calls (pynite frame integration)
|
|
1044
|
+
def merge_displacement_results(self, combo, D):
|
|
1045
|
+
"""adds a remotely calculated displacement vector and applies to nodes ala pynite convention"""
|
|
1046
|
+
self.frame._D[combo] = D
|
|
1047
|
+
|
|
1048
|
+
for node in self.frame.Nodes.values():
|
|
1049
|
+
node.DX[combo] = D[node.ID * 6 + 0, 0]
|
|
1050
|
+
node.DY[combo] = D[node.ID * 6 + 1, 0]
|
|
1051
|
+
node.DZ[combo] = D[node.ID * 6 + 2, 0]
|
|
1052
|
+
node.RX[combo] = D[node.ID * 6 + 3, 0]
|
|
1053
|
+
node.RY[combo] = D[node.ID * 6 + 4, 0]
|
|
1054
|
+
node.RZ[combo] = D[node.ID * 6 + 5, 0]
|
|
1055
|
+
|
|
1056
|
+
def prep_remote_analysis(self):
|
|
1057
|
+
"""copies Pynite analysis pre-functions"""
|
|
1058
|
+
# Generate all meshes
|
|
1059
|
+
for mesh in self.frame.Meshes.values():
|
|
1060
|
+
if mesh.is_generated == False:
|
|
1061
|
+
mesh.generate()
|
|
1062
|
+
|
|
1063
|
+
# Activate all springs and members for all load combinations
|
|
1064
|
+
for spring in self.frame.Springs.values():
|
|
1065
|
+
for combo_name in self.LoadCombos.keys():
|
|
1066
|
+
spring.active[combo_name] = True
|
|
1067
|
+
|
|
1068
|
+
# Activate all physical members for all load combinations
|
|
1069
|
+
for phys_member in self.frame.Members.values():
|
|
1070
|
+
for combo_name in self.frame.LoadCombos.keys():
|
|
1071
|
+
phys_member.active[combo_name] = True
|
|
1072
|
+
|
|
1073
|
+
# Assign an internal ID to all nodes and elements in the model
|
|
1074
|
+
self._renumber()
|
|
1075
|
+
|
|
1076
|
+
# Get the auxiliary list used to determine how the matrices will be partitioned
|
|
1077
|
+
D1_indices, D2_indices, D2 = self._aux_list()
|
|
1078
|
+
|
|
1079
|
+
# Failure Analysis Interface (Override your for your cases!)
|
|
1080
|
+
def failure_load_combos(
|
|
1081
|
+
self, report_info: dict, run_full=False, remote_sync=None, **kw
|
|
1082
|
+
):
|
|
1083
|
+
"""returns or yields a set of load combo names to check failure for.
|
|
1084
|
+
You're encouraged to override this for your specific cases
|
|
1085
|
+
:param report_info: the report that is generated in `run_failure_analysis` which will dynamically change with
|
|
1086
|
+
:yields: the combo name
|
|
1087
|
+
:returns: None when done
|
|
1088
|
+
"""
|
|
1089
|
+
failures = report_info["failures"]
|
|
1090
|
+
|
|
1091
|
+
# gravity case first
|
|
1092
|
+
o = [self.default_combo]
|
|
1093
|
+
yield o[0]
|
|
1094
|
+
if report_info["fails"]:
|
|
1095
|
+
if not run_full:
|
|
1096
|
+
return # you are weak
|
|
1097
|
+
|
|
1098
|
+
iteration_combos = self.iteration_combos
|
|
1099
|
+
if iteration_combos is None:
|
|
1100
|
+
iteration_combos = list(self.LoadCombos.keys())
|
|
1101
|
+
|
|
1102
|
+
for combo_name, combo in self.LoadCombos.items():
|
|
1103
|
+
if combo_name not in iteration_combos:
|
|
1104
|
+
continue
|
|
1105
|
+
|
|
1106
|
+
if combo_name != self.default_combo:
|
|
1107
|
+
yield combo_name
|
|
1108
|
+
|
|
1109
|
+
# allow time for combo to run
|
|
1110
|
+
if remote_sync:
|
|
1111
|
+
d = remote_sync.ensure.remote(combo_name)
|
|
1112
|
+
ray.wait([d]) # ,timeout=remote_timeout)
|
|
1113
|
+
|
|
1114
|
+
cfail = failures[combo_name]
|
|
1115
|
+
if check_failures(cfail):
|
|
1116
|
+
if not run_full:
|
|
1117
|
+
return # you are no good
|
|
1118
|
+
|
|
1119
|
+
def failure_load_exithandler(self, combo, res, report_info, run_full):
|
|
1120
|
+
"""a callback that is passed the structural combo, the case results
|
|
1121
|
+
|
|
1122
|
+
Override this function to provide custom exit capability
|
|
1123
|
+
:param combo: the load combo analyzed
|
|
1124
|
+
:param res: the combo failure results
|
|
1125
|
+
:param report_info: the report dictionary
|
|
1126
|
+
:run_full: option to run everything
|
|
1127
|
+
"""
|
|
1128
|
+
if check_failures(res):
|
|
1129
|
+
self.warning(f"combo {combo} failed!")
|
|
1130
|
+
report_info["fails"] = True
|
|
1131
|
+
if not run_full:
|
|
1132
|
+
raise StopAnalysis()
|
|
1133
|
+
|
|
1134
|
+
def run_failure_analysis(self, SF=1.0, run_full=False, **kwargs):
|
|
1135
|
+
"""
|
|
1136
|
+
Failure Determination:
|
|
1137
|
+
Beam Stress Estiates are used to determine if a 2D FEA beam/combo analysis should be run. The maximum beam vonmises stress is compared the the beam material allowable stress / saftey factor.
|
|
1138
|
+
|
|
1139
|
+
Override:
|
|
1140
|
+
Depending on number of load cases, this analysis could take a while. Its encouraged to use a similar pattern of check and exit to reduce load combo size in an effort to fail early. Override this function if nessicary.
|
|
1141
|
+
|
|
1142
|
+
Case / Beam Ordering:
|
|
1143
|
+
runs the gravity case first and checks for failure to exit early,
|
|
1144
|
+
then all other combos are run
|
|
1145
|
+
:returns: a dictionary with 2D fea failures and estimated failures as well for each beam
|
|
1146
|
+
"""
|
|
1147
|
+
|
|
1148
|
+
report = {"fails": False} # innocent till proven guilty
|
|
1149
|
+
report["failures"] = failures = {}
|
|
1150
|
+
try:
|
|
1151
|
+
# Run the rest of the load cases
|
|
1152
|
+
for combo in self.failure_load_combos(report, **kwargs):
|
|
1153
|
+
# failure_load_combos returns none on exit
|
|
1154
|
+
if combo is None:
|
|
1155
|
+
return report
|
|
1156
|
+
|
|
1157
|
+
self.run(combo=combo)
|
|
1158
|
+
fail_res = self.run_combo_failure_analysis(combo, run_full, SF)
|
|
1159
|
+
failures[combo] = fail_res
|
|
1160
|
+
# will call
|
|
1161
|
+
self.failure_load_exithandler(combo, fail_res, report, run_full)
|
|
1162
|
+
|
|
1163
|
+
except KeyboardInterrupt:
|
|
1164
|
+
return report
|
|
1165
|
+
|
|
1166
|
+
except StopAnalysis:
|
|
1167
|
+
return report
|
|
1168
|
+
|
|
1169
|
+
except Exception as e:
|
|
1170
|
+
self.error(e, "issue in failur analysis")
|
|
1171
|
+
|
|
1172
|
+
return report # tada!
|
|
1173
|
+
|
|
1174
|
+
# Custom Failure Analysis Tools
|
|
1175
|
+
def check_failures(self, *args, **kwargs) -> bool:
|
|
1176
|
+
"""exposes check_failures"""
|
|
1177
|
+
return check_failures(*args, **kwargs)
|
|
1178
|
+
|
|
1179
|
+
def check_est_failures(self, *args, **kwargs) -> bool:
|
|
1180
|
+
"""exposes check_est_failures"""
|
|
1181
|
+
return check_est_failures(*args, **kwargs)
|
|
1182
|
+
|
|
1183
|
+
@property
|
|
1184
|
+
def full_failures(self):
|
|
1185
|
+
"""return all failure estimates for all combos"""
|
|
1186
|
+
return self.estimated_failures_report()
|
|
1187
|
+
|
|
1188
|
+
def check_mesh_failures(
|
|
1189
|
+
self, combo, run_full=False, SF=1.0, return_complete=False
|
|
1190
|
+
) -> dict:
|
|
1191
|
+
"""Returns the von-mises stress / allowable (failure fraction) stresses for each quad in each mesh.
|
|
1192
|
+
:param run_full: if false will exit early on first failure
|
|
1193
|
+
:returns: a dictinary of quad names and their failure fraction
|
|
1194
|
+
"""
|
|
1195
|
+
self.info(f"checking mesh failure {combo}")
|
|
1196
|
+
|
|
1197
|
+
stable = {}
|
|
1198
|
+
failures = {}
|
|
1199
|
+
summary = {"failures": failures, "stable": stable}
|
|
1200
|
+
|
|
1201
|
+
quad_info = self.quad_info
|
|
1202
|
+
for meshname, mesh in self._meshes.items():
|
|
1203
|
+
matid = mesh.material_name
|
|
1204
|
+
mat = self._materials[matid]
|
|
1205
|
+
allowable = mat.allowable_stress
|
|
1206
|
+
for quadname, quad in mesh.elements.items():
|
|
1207
|
+
for r, s in itert.product([-1, 0, 1], [-1, 0, 1]):
|
|
1208
|
+
S = quad_stress_tensor(quad, r, s, combo)
|
|
1209
|
+
vm = calculate_von_mises_stress(S)
|
|
1210
|
+
fail_frac = vm / allowable
|
|
1211
|
+
if vm * SF > allowable:
|
|
1212
|
+
if quadname not in failures:
|
|
1213
|
+
failures[quadname] = fail_frac
|
|
1214
|
+
elif fail_frac > failures[quadname]:
|
|
1215
|
+
failures[quadname] = fail_frac
|
|
1216
|
+
|
|
1217
|
+
if not run_full:
|
|
1218
|
+
if return_complete:
|
|
1219
|
+
return summary
|
|
1220
|
+
return failures
|
|
1221
|
+
else:
|
|
1222
|
+
if quadname not in stable:
|
|
1223
|
+
stable[quadname] = fail_frac
|
|
1224
|
+
elif fail_frac > stable[quadname]:
|
|
1225
|
+
stable[quadname] = fail_frac
|
|
1226
|
+
|
|
1227
|
+
if return_complete:
|
|
1228
|
+
return summary
|
|
1229
|
+
return failures
|
|
1230
|
+
|
|
1231
|
+
def mesh_stress_dataframe(self, combo=None):
|
|
1232
|
+
"""returns a dataframe of each quad, its x,y,z coords, vonmises stress and other mass/cost properties in a dataframe"""
|
|
1233
|
+
if combo is None:
|
|
1234
|
+
combo = self.current_combo
|
|
1235
|
+
max_vm = {}
|
|
1236
|
+
for qn, q in self.Quads.items():
|
|
1237
|
+
qvm = {}
|
|
1238
|
+
for r, s in itert.product([-1, 0, 1], [-1, 0, 1]):
|
|
1239
|
+
S = quad_stress_tensor(q, r, s, combo)
|
|
1240
|
+
qvm[(r, s)] = calculate_von_mises_stress(S)
|
|
1241
|
+
max_vm[qn] = max(qvm.values())
|
|
1242
|
+
|
|
1243
|
+
mesh_data = []
|
|
1244
|
+
|
|
1245
|
+
for qn, maxvm in max_vm.items():
|
|
1246
|
+
qinfo = self.quad_info[qn].copy()
|
|
1247
|
+
x, y, z = qinfo.pop("cog").tolist()
|
|
1248
|
+
mesh_data.append({"x": x, "y": y, "z": z, "vm": maxvm, **qinfo})
|
|
1249
|
+
|
|
1250
|
+
return pd.DataFrame(mesh_data)
|
|
1251
|
+
|
|
1252
|
+
def estimated_failures_report(
|
|
1253
|
+
self,
|
|
1254
|
+
concentrationFactor=1,
|
|
1255
|
+
methodFactor=1,
|
|
1256
|
+
fail_frac_criteria=None,
|
|
1257
|
+
combos: list = None,
|
|
1258
|
+
) -> dict:
|
|
1259
|
+
"""uses the beam estimated stresses with specific adders to account for beam 2D stress concentrations and general inaccuracy as well as a fail_fraction to analyze. The estimated loads are very pessimistic with a 250x overestimate"""
|
|
1260
|
+
|
|
1261
|
+
# the string input case, with csv support
|
|
1262
|
+
if isinstance(combos, str):
|
|
1263
|
+
combos = combos.split(",") # will be a list!
|
|
1264
|
+
|
|
1265
|
+
# what fraction of allowable stress is generated for this case
|
|
1266
|
+
if fail_frac_criteria is None:
|
|
1267
|
+
fail_1 = None
|
|
1268
|
+
else:
|
|
1269
|
+
fail_1 = fail_frac_criteria / (concentrationFactor * methodFactor)
|
|
1270
|
+
|
|
1271
|
+
failures = collections.defaultdict(dict)
|
|
1272
|
+
|
|
1273
|
+
# df = self.dataframe
|
|
1274
|
+
run_combos = df.current_combo.to_list()
|
|
1275
|
+
|
|
1276
|
+
failures_count = 0
|
|
1277
|
+
cases = 0
|
|
1278
|
+
|
|
1279
|
+
iteration_combos = self.iteration_combos
|
|
1280
|
+
if iteration_combos is None:
|
|
1281
|
+
iteration_combos = list(self.LoadCombos.keys())
|
|
1282
|
+
|
|
1283
|
+
# gather forces for each beam
|
|
1284
|
+
for nm, beam in self.beams.items():
|
|
1285
|
+
self.debug(f"estimate beam stress: {beam.name}")
|
|
1286
|
+
|
|
1287
|
+
# try each load combo
|
|
1288
|
+
for combo in iteration_combos:
|
|
1289
|
+
if combo not in run_combos:
|
|
1290
|
+
continue # theres, no point
|
|
1291
|
+
|
|
1292
|
+
if combos and combo not in combos:
|
|
1293
|
+
continue # you are not the chose one(s)
|
|
1294
|
+
|
|
1295
|
+
# MUST Loop over beams pos and combos to get the correct data for general case of all combos
|
|
1296
|
+
# combo_df = df[df.current_combo == combo]
|
|
1297
|
+
# combo_dict = combo_df.to_dict("records")
|
|
1298
|
+
# combo_dict = combo_dict[-1] # last one is the combo in the case of repeats
|
|
1299
|
+
# loop positions start,mid,end
|
|
1300
|
+
for x in [0, 0.5, 1.0]:
|
|
1301
|
+
cases += 1
|
|
1302
|
+
f = beam.get_forces_at(x, combo=combo)
|
|
1303
|
+
beam_fail_frac = beam.estimate_stress(
|
|
1304
|
+
**f, force_calc=self.calculate_actual_failure
|
|
1305
|
+
)
|
|
1306
|
+
st = beam_fail_frac * beam.material.allowable_stress
|
|
1307
|
+
|
|
1308
|
+
fails = False
|
|
1309
|
+
if fail_1 is None:
|
|
1310
|
+
deltadf = abs(1 - beam_fail_frac)
|
|
1311
|
+
fails = deltadf <= beam.section.fail_frac_criteria()
|
|
1312
|
+
elif beam_fail_frac >= fail_1:
|
|
1313
|
+
fails = True
|
|
1314
|
+
|
|
1315
|
+
# record!
|
|
1316
|
+
if fails:
|
|
1317
|
+
self.debug(
|
|
1318
|
+
f"estimated beam {beam.name} failure at L={x*100:3.2f}% | {combo}"
|
|
1319
|
+
)
|
|
1320
|
+
failures_count += 1
|
|
1321
|
+
|
|
1322
|
+
failures[nm][(combo, x)] = _d = {
|
|
1323
|
+
"beam": nm,
|
|
1324
|
+
"est_stress": st,
|
|
1325
|
+
"combo": combo,
|
|
1326
|
+
# "x": x,
|
|
1327
|
+
"allowable_stress": beam.material.allowable_stress,
|
|
1328
|
+
"fail_frac": beam_fail_frac,
|
|
1329
|
+
"loads": f,
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
# failures[nm][(combo, x)].update(
|
|
1333
|
+
# **{
|
|
1334
|
+
# k.split(nm + ".")[-1] if nm in k else k: v
|
|
1335
|
+
# for k, v in combo_dict.items()
|
|
1336
|
+
# if ("beams" not in k or nm in k) and k not in _d
|
|
1337
|
+
# }
|
|
1338
|
+
# )
|
|
1339
|
+
|
|
1340
|
+
if failures_count:
|
|
1341
|
+
self.warning(
|
|
1342
|
+
f"{combos}| {failures_count} @ {failures_count*100/cases:3.2f}% estimated failures: {set(failures.keys())}"
|
|
1343
|
+
)
|
|
1344
|
+
|
|
1345
|
+
return failures
|
|
1346
|
+
|
|
1347
|
+
def current_failures(
|
|
1348
|
+
self,
|
|
1349
|
+
concentrationFactor=1,
|
|
1350
|
+
methodFactor=1,
|
|
1351
|
+
fail_frac_criteria=0.999,
|
|
1352
|
+
) -> dict:
|
|
1353
|
+
"""uses the beam estimated stresses with specific adders to account for beam 2D stress concentrations and general inaccuracy as well as a fail_fraction to analyze. The estimated loads are very pessimistic with a 250x overestimate by default for beams. mesh stresses are more accurate and failure is determined by fail_fac (von-mises stress / allowable stress)/fail_frac_criteria"""
|
|
1354
|
+
|
|
1355
|
+
# what fraction of allowable stress is generated for this case
|
|
1356
|
+
# what fraction of allowable stress is generated for this case
|
|
1357
|
+
|
|
1358
|
+
fail_1 = fail_frac_criteria / (concentrationFactor * methodFactor)
|
|
1359
|
+
|
|
1360
|
+
# FIXME: only run current combo (remove combo storage)
|
|
1361
|
+
failures = collections.defaultdict(dict)
|
|
1362
|
+
stable = collections.defaultdict(dict)
|
|
1363
|
+
summary = {"beam_failures": failures, "beams_stable": stable}
|
|
1364
|
+
|
|
1365
|
+
failures_count = 0
|
|
1366
|
+
checks = 0
|
|
1367
|
+
beams = len(self.beams)
|
|
1368
|
+
|
|
1369
|
+
combo = self.current_combo
|
|
1370
|
+
self.info(f"determining failures for {self.current_combo}")
|
|
1371
|
+
|
|
1372
|
+
# self._lock_est_failures = True
|
|
1373
|
+
# data_dict = self.data_dict
|
|
1374
|
+
self._lock_est_failures = False
|
|
1375
|
+
|
|
1376
|
+
# gather forces for each beam
|
|
1377
|
+
for nm, beam in self.beams.items():
|
|
1378
|
+
self.debug(f"estimate beam stress: {beam.name}")
|
|
1379
|
+
|
|
1380
|
+
checks += 1
|
|
1381
|
+
beam_fail_frac = beam.fail_factor_estimate
|
|
1382
|
+
|
|
1383
|
+
fails = False
|
|
1384
|
+
if beam_fail_frac >= fail_1:
|
|
1385
|
+
fails = True
|
|
1386
|
+
|
|
1387
|
+
if fails:
|
|
1388
|
+
self.debug(f"estimated beam {beam.name} failure")
|
|
1389
|
+
failures_count += 1
|
|
1390
|
+
|
|
1391
|
+
failures[nm][combo] = _d = {
|
|
1392
|
+
"beam": nm,
|
|
1393
|
+
# "est_stress": st,
|
|
1394
|
+
"combo": combo,
|
|
1395
|
+
# "x": x,
|
|
1396
|
+
# "allowable_stress": beam.material.allowable_stress,
|
|
1397
|
+
"fail_frac": beam_fail_frac,
|
|
1398
|
+
"fail_critera": fail_frac_criteria,
|
|
1399
|
+
# "loads": f,
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
# failures[nm][combo].update(
|
|
1403
|
+
# **{
|
|
1404
|
+
# k.split(nm + ".")[-1] if nm in k else k: v
|
|
1405
|
+
# for k, v in data_dict.items()
|
|
1406
|
+
# if ("beams" not in k or nm in k) and k not in _d
|
|
1407
|
+
# }
|
|
1408
|
+
# )
|
|
1409
|
+
else:
|
|
1410
|
+
stable[nm][combo] = _d = {
|
|
1411
|
+
"beam": nm,
|
|
1412
|
+
# "est_stress": st,
|
|
1413
|
+
"combo": combo,
|
|
1414
|
+
# "x": x,
|
|
1415
|
+
# "allowable_stress": beam.material.allowable_stress,
|
|
1416
|
+
"fail_frac": beam_fail_frac,
|
|
1417
|
+
"fail_critera": fail_frac_criteria,
|
|
1418
|
+
# "loads": f,
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
# add mesh failures
|
|
1422
|
+
mesh_summary = self.check_mesh_failures(
|
|
1423
|
+
combo,
|
|
1424
|
+
run_full=self.calculate_full_failure,
|
|
1425
|
+
SF=1.0,
|
|
1426
|
+
return_complete=True,
|
|
1427
|
+
)
|
|
1428
|
+
mesh_failures = mesh_summary["failures"]
|
|
1429
|
+
mesh_success = mesh_summary["stable"]
|
|
1430
|
+
summary["mesh_failures"] = mesh_failures
|
|
1431
|
+
summary["mesh_stable"] = mesh_success
|
|
1432
|
+
|
|
1433
|
+
# update failures
|
|
1434
|
+
mesh_failures_count = len([v for k, v in mesh_failures.items() if v > 0.99])
|
|
1435
|
+
|
|
1436
|
+
max_fail_frac = max(
|
|
1437
|
+
[0]
|
|
1438
|
+
+ [
|
|
1439
|
+
v["fail_frac"]
|
|
1440
|
+
for beam, fails in failures.items()
|
|
1441
|
+
for k, v in fails.items()
|
|
1442
|
+
]
|
|
1443
|
+
)
|
|
1444
|
+
max_stable_frac = max(
|
|
1445
|
+
[0]
|
|
1446
|
+
+ [
|
|
1447
|
+
v["fail_frac"]
|
|
1448
|
+
for beam, stables in stable.items()
|
|
1449
|
+
for k, v in stables.items()
|
|
1450
|
+
]
|
|
1451
|
+
)
|
|
1452
|
+
max_mesh_fail_frac = max([0] + [v for k, v in mesh_failures.items()])
|
|
1453
|
+
max_mesh_stable_frac = max([0] + [v for k, v in mesh_success.items()])
|
|
1454
|
+
# Max mesh failure fracs
|
|
1455
|
+
max_beam_fail_frac = max(max_fail_frac, max_stable_frac)
|
|
1456
|
+
max_mesh_frac = max(max_mesh_fail_frac, max_mesh_stable_frac)
|
|
1457
|
+
max_fail_frac = max(max_beam_fail_frac, max_mesh_frac)
|
|
1458
|
+
|
|
1459
|
+
if failures_count:
|
|
1460
|
+
self.warning(
|
|
1461
|
+
f"{combo}| {failures_count} @ {failures_count*100/checks:3.2f}% estimated failures: {set(failures.keys())}"
|
|
1462
|
+
)
|
|
1463
|
+
summary["mesh_fail_count"] = mesh_failures_count
|
|
1464
|
+
summary["beam_fail_count"] = failures_count
|
|
1465
|
+
summary["failures_count"] = failures_count + mesh_failures_count
|
|
1466
|
+
summary["beams"] = beams
|
|
1467
|
+
summary["meshs"] = len(mesh_failures)
|
|
1468
|
+
summary["checks"] = checks + len(self.quad_info)
|
|
1469
|
+
summary["max_mesh_fail_frac"] = max_mesh_frac
|
|
1470
|
+
summary["max_beam_fail_frac"] = max_beam_fail_frac
|
|
1471
|
+
summary["max_fail_frac"] = max_fail_frac
|
|
1472
|
+
|
|
1473
|
+
self.current_failure_summary = summary
|
|
1474
|
+
|
|
1475
|
+
return summary
|
|
1476
|
+
|
|
1477
|
+
# Failure Analysis Properties
|
|
1478
|
+
# FIXME: needs a complete rethink as far as caching and reporting properties, consider removing or replacing
|
|
1479
|
+
# @system_property
|
|
1480
|
+
@property
|
|
1481
|
+
def max_est_fail_frac(self) -> float:
|
|
1482
|
+
"""estimated failure fraction for the current result"""
|
|
1483
|
+
fc = self._current_failures
|
|
1484
|
+
if isinstance(fc, dict):
|
|
1485
|
+
return fc["max_fail_frac"]
|
|
1486
|
+
self.warning(f"could not get estimated beam failure frac")
|
|
1487
|
+
return fc
|
|
1488
|
+
|
|
1489
|
+
# @system_property
|
|
1490
|
+
@property
|
|
1491
|
+
def max_beam_est_fail_frac(self) -> float:
|
|
1492
|
+
fc = self._current_failures
|
|
1493
|
+
if isinstance(fc, dict):
|
|
1494
|
+
return fc["max_beam_fail_frac"]
|
|
1495
|
+
self.warning(f"could not get estimated beam failure frac")
|
|
1496
|
+
return fc
|
|
1497
|
+
|
|
1498
|
+
# @system_property
|
|
1499
|
+
@property
|
|
1500
|
+
def max_mesh_est_fail_frac(self) -> float:
|
|
1501
|
+
fc = self._current_failures
|
|
1502
|
+
if isinstance(fc, dict):
|
|
1503
|
+
return fc["max_mesh_fail_frac"]
|
|
1504
|
+
self.warning(f"could not get estimated failure frac")
|
|
1505
|
+
return fc
|
|
1506
|
+
|
|
1507
|
+
# @system_property
|
|
1508
|
+
@property
|
|
1509
|
+
def estimated_failure_count(self) -> float:
|
|
1510
|
+
fc = self._current_failures
|
|
1511
|
+
if isinstance(fc, dict):
|
|
1512
|
+
return fc["failures_count"]
|
|
1513
|
+
self.warning("could not get estimated failure count")
|
|
1514
|
+
return fc
|
|
1515
|
+
|
|
1516
|
+
# @system_property
|
|
1517
|
+
@property
|
|
1518
|
+
def estimated_beam_failure_count(self) -> float:
|
|
1519
|
+
fc = self._current_failures
|
|
1520
|
+
if isinstance(fc, dict):
|
|
1521
|
+
return fc["beam_fail_count"]
|
|
1522
|
+
self.warning(f"could not get estimated beam failure count")
|
|
1523
|
+
return fc
|
|
1524
|
+
|
|
1525
|
+
# @system_property
|
|
1526
|
+
@property
|
|
1527
|
+
def estimated_mesh_failure_count(self) -> float:
|
|
1528
|
+
fc = self._current_failures
|
|
1529
|
+
if isinstance(fc, dict):
|
|
1530
|
+
return fc["mesh_fail_count"]
|
|
1531
|
+
self.warning(f"could not get estimated mesh failure count")
|
|
1532
|
+
return fc
|
|
1533
|
+
|
|
1534
|
+
# @system_property
|
|
1535
|
+
@property
|
|
1536
|
+
def actual_failure_count(self) -> float:
|
|
1537
|
+
if not self.calculate_actual_failure:
|
|
1538
|
+
return None
|
|
1539
|
+
|
|
1540
|
+
afc = self._actual_failures
|
|
1541
|
+
if isinstance(afc, dict):
|
|
1542
|
+
return afc["failure_count"]
|
|
1543
|
+
self.warning(f"could not get actual failure count")
|
|
1544
|
+
return afc
|
|
1545
|
+
|
|
1546
|
+
# @system_property
|
|
1547
|
+
@property
|
|
1548
|
+
def actual_beam_failure_count(self) -> float:
|
|
1549
|
+
if not self.calculate_actual_failure:
|
|
1550
|
+
return None
|
|
1551
|
+
|
|
1552
|
+
afc = self._actual_failures
|
|
1553
|
+
if isinstance(afc, dict):
|
|
1554
|
+
return afc["beam_failures_count"]
|
|
1555
|
+
self.warning(f"could not get actual beam failure count")
|
|
1556
|
+
return afc
|
|
1557
|
+
|
|
1558
|
+
# @system_property
|
|
1559
|
+
@property
|
|
1560
|
+
def actual_mesh_failure_count(self) -> float:
|
|
1561
|
+
if not self.calculate_actual_failure:
|
|
1562
|
+
return None
|
|
1563
|
+
|
|
1564
|
+
afc = self._actual_failures
|
|
1565
|
+
if isinstance(afc, dict):
|
|
1566
|
+
return afc["mesh_failure_count"]
|
|
1567
|
+
self.warning(f"could not get actual mesh failure count")
|
|
1568
|
+
return afc
|
|
1569
|
+
|
|
1570
|
+
@property
|
|
1571
|
+
def _current_failures(self):
|
|
1572
|
+
if hasattr(self, "_lock_est_failures") and self._lock_est_failures:
|
|
1573
|
+
self.info(f"current failures locked")
|
|
1574
|
+
return np.nan
|
|
1575
|
+
if self.current_failure_summary is None:
|
|
1576
|
+
fc = self.current_failures()
|
|
1577
|
+
else:
|
|
1578
|
+
fc = self.current_failure_summary
|
|
1579
|
+
return fc
|
|
1580
|
+
|
|
1581
|
+
@property
|
|
1582
|
+
def _actual_failures(self):
|
|
1583
|
+
if not self.calculate_actual_failure:
|
|
1584
|
+
return None
|
|
1585
|
+
|
|
1586
|
+
if hasattr(self, "_lock_est_failures") and self._lock_est_failures:
|
|
1587
|
+
return np.nan
|
|
1588
|
+
|
|
1589
|
+
if self._current_combo_failure_analysis is None:
|
|
1590
|
+
if self.current_failure_summary is None:
|
|
1591
|
+
fc = self.current_failures()
|
|
1592
|
+
else:
|
|
1593
|
+
fc = self.current_failure_summary
|
|
1594
|
+
afc = self.run_combo_failure_analysis(
|
|
1595
|
+
combo=self.current_combo,
|
|
1596
|
+
run_full=self.calculate_full_failure,
|
|
1597
|
+
SF=1.0,
|
|
1598
|
+
fail_fast=not self.calculate_full_failure,
|
|
1599
|
+
failures=fc["beam_failures"],
|
|
1600
|
+
)
|
|
1601
|
+
else:
|
|
1602
|
+
afc = self._current_combo_failure_analysis
|
|
1603
|
+
return afc
|
|
1604
|
+
|
|
1605
|
+
def run_combo_failure_analysis(
|
|
1606
|
+
self,
|
|
1607
|
+
combo,
|
|
1608
|
+
run_full: bool = False,
|
|
1609
|
+
SF: float = 1.0,
|
|
1610
|
+
run_section_failure=None,
|
|
1611
|
+
fail_fast=True,
|
|
1612
|
+
failures=None,
|
|
1613
|
+
):
|
|
1614
|
+
"""runs a single load combo and adds 2d section failures"""
|
|
1615
|
+
self.info(f"determine combo {combo} failures...")
|
|
1616
|
+
|
|
1617
|
+
if failures is None:
|
|
1618
|
+
failures = self.estimated_failures_report(combos=combo)
|
|
1619
|
+
|
|
1620
|
+
if run_section_failure is None:
|
|
1621
|
+
run_section_failure = self.run_failure_sections
|
|
1622
|
+
args = (failures, run_full, SF, fail_fast)
|
|
1623
|
+
else:
|
|
1624
|
+
args = (self, failures, run_full, SF, fail_fast)
|
|
1625
|
+
|
|
1626
|
+
out = {}
|
|
1627
|
+
if combo == self.current_combo:
|
|
1628
|
+
self._current_combo_failure_analysis = out
|
|
1629
|
+
|
|
1630
|
+
out["beam_failures_count"] = 0
|
|
1631
|
+
out["mesh_failure_count"] = 0
|
|
1632
|
+
out["failure_count"] = 0
|
|
1633
|
+
|
|
1634
|
+
out["estimate"] = failures
|
|
1635
|
+
out["actual"] = check_fail = {}
|
|
1636
|
+
out["mesh_failures"] = mesh_failures = self.check_mesh_failures(
|
|
1637
|
+
combo, run_full, SF
|
|
1638
|
+
)
|
|
1639
|
+
# perform 2d analysis if any estimated
|
|
1640
|
+
if mesh_failures and not run_full:
|
|
1641
|
+
return out
|
|
1642
|
+
|
|
1643
|
+
elif self.calculate_actual_failure and check_est_failures(failures):
|
|
1644
|
+
self.info(f"testing actual failures...")
|
|
1645
|
+
out["actual"] = dfail = run_section_failure(*args)
|
|
1646
|
+
act_fails = {
|
|
1647
|
+
x: f
|
|
1648
|
+
for beam, cfail in dfail.items()
|
|
1649
|
+
for (c, x), f in cfail.items()
|
|
1650
|
+
if f["fails"]
|
|
1651
|
+
}
|
|
1652
|
+
out["beam_failures_count"] = len(act_fails)
|
|
1653
|
+
out["mesh_failure_count"] = len(mesh_failures)
|
|
1654
|
+
out["failure_count"] = len(act_fails) + len(mesh_failures)
|
|
1655
|
+
if dfail and not run_full:
|
|
1656
|
+
self.info(f"found actual failure...")
|
|
1657
|
+
return out
|
|
1658
|
+
|
|
1659
|
+
else:
|
|
1660
|
+
self.info(f"no estimated failures found")
|
|
1661
|
+
|
|
1662
|
+
return out
|
|
1663
|
+
|
|
1664
|
+
def run_failure_sections(
|
|
1665
|
+
self, failures, run_full: bool = False, SF: float = 1.0, fail_fast=True
|
|
1666
|
+
):
|
|
1667
|
+
"""takes estimated failures and runs 2D section FEA on those cases"""
|
|
1668
|
+
secton_results = {}
|
|
1669
|
+
|
|
1670
|
+
# random beam analysis
|
|
1671
|
+
# for beamnm,est_fail_cases in failpairs:
|
|
1672
|
+
failed_beams = set()
|
|
1673
|
+
failed_pos = set()
|
|
1674
|
+
fail_count = 0
|
|
1675
|
+
for (beamnm, combo, x), dat in sort_max_estimated_failures(failures):
|
|
1676
|
+
c = combo
|
|
1677
|
+
self.debug(
|
|
1678
|
+
f'running beam sections: {beamnm},{combo} @ {x} {dat["fail_frac"]*100.}%'
|
|
1679
|
+
)
|
|
1680
|
+
beam = self.beams[beamnm]
|
|
1681
|
+
|
|
1682
|
+
# prepare for output
|
|
1683
|
+
if beamnm not in secton_results:
|
|
1684
|
+
secton_results[beamnm] = {}
|
|
1685
|
+
|
|
1686
|
+
f = dat["loads"]
|
|
1687
|
+
s = beam.get_stress_with_forces(**f)
|
|
1688
|
+
|
|
1689
|
+
secton_results[beamnm][(combo, x)] = d_ = dat.copy()
|
|
1690
|
+
d_["combo"] = combo
|
|
1691
|
+
d_["x"] = x
|
|
1692
|
+
d_["beam"] = beamnm
|
|
1693
|
+
d_["stress_analysis"] = s
|
|
1694
|
+
d_["stress_results"] = sss = s.get_stress()
|
|
1695
|
+
d_["stress_vm_max"] = max([max(ss["sig_vm"]) for ss in sss])
|
|
1696
|
+
d_["fail_frac"] = ff = max(
|
|
1697
|
+
[max(ss["sig_vm"] / beam.material.allowable_stress) for ss in sss]
|
|
1698
|
+
)
|
|
1699
|
+
d_["fails"] = fail = ff > 1 / SF
|
|
1700
|
+
|
|
1701
|
+
if fail:
|
|
1702
|
+
fail_count += 1
|
|
1703
|
+
failed_beams.add(beamnm)
|
|
1704
|
+
failed_pos.add(x)
|
|
1705
|
+
self.debug(f"beam {beamnm} failed @ {x*100:3.0f}%| {c}")
|
|
1706
|
+
if fail_fast:
|
|
1707
|
+
return secton_results
|
|
1708
|
+
if not run_full:
|
|
1709
|
+
break # next beam!
|
|
1710
|
+
else:
|
|
1711
|
+
self.debug(f"beam {beamnm} ok @ {x*100:3.0f}%| {c}")
|
|
1712
|
+
|
|
1713
|
+
if failed_beams or failed_pos:
|
|
1714
|
+
self.warning(
|
|
1715
|
+
f"{fail_count:3.0f} across {failed_beams} @ {[f'{p*100:3.0f}' for p in failed_pos]}%| {c}"
|
|
1716
|
+
)
|
|
1717
|
+
|
|
1718
|
+
return secton_results
|
|
1719
|
+
|
|
1720
|
+
|
|
1721
|
+
# TODO: make a multiprocessing version of section failure analysis using shared memory or something
|
|
1722
|
+
|
|
1723
|
+
|
|
1724
|
+
# Remote Sync Util (locally run with section)
|
|
1725
|
+
def run_combo_failure_analysis(inst, combo, run_full: bool = False, SF: float = 1.0):
|
|
1726
|
+
"""runs a single load combo and adds 2d section failures"""
|
|
1727
|
+
inst.resetSystemLogs()
|
|
1728
|
+
# use parallel failure section
|
|
1729
|
+
return inst.run_combo_failure_analysis(
|
|
1730
|
+
combo, run_full, SF, run_section_failure=run_failure_sections
|
|
1731
|
+
)
|
|
1732
|
+
|
|
1733
|
+
|
|
1734
|
+
def run_failure_sections(
|
|
1735
|
+
struct,
|
|
1736
|
+
failures,
|
|
1737
|
+
run_full: bool = False,
|
|
1738
|
+
SF: float = 1.0,
|
|
1739
|
+
fail_fast=True,
|
|
1740
|
+
group_size=12,
|
|
1741
|
+
):
|
|
1742
|
+
"""takes estimated failures and runs 2D section FEA on those cases"""
|
|
1743
|
+
|
|
1744
|
+
struct.resetSystemLogs()
|
|
1745
|
+
|
|
1746
|
+
secton_results = {}
|
|
1747
|
+
skip_beams = set()
|
|
1748
|
+
if not hasattr(struct, "_putbeam"):
|
|
1749
|
+
struct._putbeam = put_beams = {}
|
|
1750
|
+
else:
|
|
1751
|
+
put_beams = struct._putbeam
|
|
1752
|
+
|
|
1753
|
+
cur = []
|
|
1754
|
+
|
|
1755
|
+
for (beamnm, c, x), dat in sort_max_estimated_failures(failures):
|
|
1756
|
+
struct.info(
|
|
1757
|
+
f'run remote beam sections: {beamnm},{c} @ {x} {dat["fail_frac"]*100.}%'
|
|
1758
|
+
)
|
|
1759
|
+
if beamnm in skip_beams:
|
|
1760
|
+
continue
|
|
1761
|
+
|
|
1762
|
+
# prep for output
|
|
1763
|
+
if beamnm not in secton_results:
|
|
1764
|
+
secton_results[beamnm] = {}
|
|
1765
|
+
beam = struct.beams[beamnm]
|
|
1766
|
+
|
|
1767
|
+
forces = beam.get_forces_at(x, c)
|
|
1768
|
+
|
|
1769
|
+
# lazy beam puts
|
|
1770
|
+
if beamnm not in put_beams:
|
|
1771
|
+
beam_ref = ray.put(beam._section_properties)
|
|
1772
|
+
put_beams[beamnm] = beam_ref
|
|
1773
|
+
else:
|
|
1774
|
+
beam_ref = put_beams[beamnm]
|
|
1775
|
+
|
|
1776
|
+
cur.append(
|
|
1777
|
+
remote_section.remote(
|
|
1778
|
+
beam_ref,
|
|
1779
|
+
beam.name,
|
|
1780
|
+
forces,
|
|
1781
|
+
beam.material.allowable_stress,
|
|
1782
|
+
c,
|
|
1783
|
+
x,
|
|
1784
|
+
SF=SF,
|
|
1785
|
+
)
|
|
1786
|
+
)
|
|
1787
|
+
|
|
1788
|
+
# run the damn thing
|
|
1789
|
+
if len(cur) >= group_size:
|
|
1790
|
+
struct.info(f"waiting on {group_size}")
|
|
1791
|
+
ray.wait(cur)
|
|
1792
|
+
|
|
1793
|
+
for res in ray.get(cur):
|
|
1794
|
+
fail = res["fails"]
|
|
1795
|
+
beamnm = res["beam"]
|
|
1796
|
+
combo = res["combo"]
|
|
1797
|
+
x = res["x"]
|
|
1798
|
+
secton_results[res["beam"]][(combo, x)] = res
|
|
1799
|
+
|
|
1800
|
+
if fail:
|
|
1801
|
+
struct.warning(f"beam {beamnm} failed @ {x*100:3.0f}%| {c}")
|
|
1802
|
+
if fail_fast:
|
|
1803
|
+
return secton_results
|
|
1804
|
+
if not run_full:
|
|
1805
|
+
skip_beams.add(beamnm)
|
|
1806
|
+
else:
|
|
1807
|
+
struct.debug(f"beam {beamnm} ok @ {x*100:3.0f}%| {c}")
|
|
1808
|
+
|
|
1809
|
+
cur = []
|
|
1810
|
+
struct.info(f"done waiting, continue...")
|
|
1811
|
+
|
|
1812
|
+
# finish them!
|
|
1813
|
+
if cur:
|
|
1814
|
+
struct.info(f"waiting on {len(cur)}")
|
|
1815
|
+
ray.wait(cur)
|
|
1816
|
+
struct.info(f"done, continue...")
|
|
1817
|
+
|
|
1818
|
+
for res in ray.get(cur):
|
|
1819
|
+
fail = res["fails"]
|
|
1820
|
+
beamnm = res["beam"]
|
|
1821
|
+
combo = res["combo"]
|
|
1822
|
+
x = res["x"]
|
|
1823
|
+
secton_results[res["beam"]][(combo, x)] = res
|
|
1824
|
+
|
|
1825
|
+
if fail:
|
|
1826
|
+
struct.warning(f"beam {beamnm} failed @ {x*100:3.0f}%| {c}")
|
|
1827
|
+
if fail_fast:
|
|
1828
|
+
return secton_results
|
|
1829
|
+
if not run_full:
|
|
1830
|
+
skip_beams.add(beamnm)
|
|
1831
|
+
else:
|
|
1832
|
+
struct.debug(f"beam {beamnm} ok @ {x*100:3.0f}%| {c}")
|
|
1833
|
+
|
|
1834
|
+
return secton_results
|
|
1835
|
+
|
|
1836
|
+
|
|
1837
|
+
# Remote Analysis
|
|
1838
|
+
def parallel_run_failure_analysis(struct, SF=1.0, run_full=False, purge=False, **kw):
|
|
1839
|
+
"""
|
|
1840
|
+
Failure Determination:
|
|
1841
|
+
Beam Stress Estiates are used to determine if a 2D FEA beam/combo analysis should be run. The maximum beam vonmises stress is compared the the beam material allowable stress / saftey factor.
|
|
1842
|
+
|
|
1843
|
+
Override:
|
|
1844
|
+
Depending on number of load cases, this analysis could take a while. Its encouraged to use a similar pattern of check and exit to reduce load combo size in an effort to fail early. Override this function if nessicary.
|
|
1845
|
+
|
|
1846
|
+
Case / Beam Ordering:
|
|
1847
|
+
runs the gravity case first and checks for failure to exit early,
|
|
1848
|
+
then all other combos are run
|
|
1849
|
+
:returns: a dictionary with 2D fea failures and esimated failures as well for each beam
|
|
1850
|
+
"""
|
|
1851
|
+
|
|
1852
|
+
report = {"fails": False} # innocent till proven guilty
|
|
1853
|
+
report["failures"] = failures = {}
|
|
1854
|
+
|
|
1855
|
+
try:
|
|
1856
|
+
# Run the rest of the load cases
|
|
1857
|
+
for combo in struct.failure_load_combos(report, run_full, **kw):
|
|
1858
|
+
# TODO: handle running list of combos to do simultanious running
|
|
1859
|
+
|
|
1860
|
+
# run and store data
|
|
1861
|
+
struct.run(combos=combo)
|
|
1862
|
+
# struct_ref = ray.put(struct)
|
|
1863
|
+
|
|
1864
|
+
# check failures
|
|
1865
|
+
fail = run_combo_failure_analysis(struct, combo, run_full, SF)
|
|
1866
|
+
|
|
1867
|
+
failures[combo] = fail
|
|
1868
|
+
struct.failure_load_exithandler(combo, fail, report, run_full)
|
|
1869
|
+
|
|
1870
|
+
if purge:
|
|
1871
|
+
purge_combo(struct, combo)
|
|
1872
|
+
|
|
1873
|
+
except KeyboardInterrupt:
|
|
1874
|
+
return report
|
|
1875
|
+
|
|
1876
|
+
except Exception as e:
|
|
1877
|
+
struct.error(e, "issue in failur analysis")
|
|
1878
|
+
|
|
1879
|
+
return report # tada!
|
|
1880
|
+
|
|
1881
|
+
|
|
1882
|
+
def purge_combo(struct, combo):
|
|
1883
|
+
struct._D.pop(combo, None)
|
|
1884
|
+
for node in struct.Nodes.values():
|
|
1885
|
+
node.DX.pop(combo, None)
|
|
1886
|
+
node.DY.pop(combo, None)
|
|
1887
|
+
node.DZ.pop(combo, None)
|
|
1888
|
+
node.RX.pop(combo, None)
|
|
1889
|
+
node.RY.pop(combo, None)
|
|
1890
|
+
node.RZ.pop(combo, None)
|
|
1891
|
+
|
|
1892
|
+
|
|
1893
|
+
if ray_ok:
|
|
1894
|
+
# Remote Capabilities
|
|
1895
|
+
@ray.remote(max_calls=5)
|
|
1896
|
+
def remote_run_combo(ref_structure, combo):
|
|
1897
|
+
log = StructureLog()
|
|
1898
|
+
try:
|
|
1899
|
+
sync_ref = ref_structure["actor_ref"]
|
|
1900
|
+
struct_ref = ref_structure["struct_ref"]
|
|
1901
|
+
log.info(f"got {ref_structure}")
|
|
1902
|
+
struct = ray.get(struct_ref)
|
|
1903
|
+
# sync = ray.get(sync_ref)
|
|
1904
|
+
sync = sync_ref
|
|
1905
|
+
# try:
|
|
1906
|
+
struct.resetSystemLogs()
|
|
1907
|
+
log.info(f"start combo {combo}")
|
|
1908
|
+
# log.info(struct.logger.__dict__)
|
|
1909
|
+
struct.run(combos=combo)
|
|
1910
|
+
log.info(f"combo done {combo}")
|
|
1911
|
+
# assert len(struct.table) == 1, 'bad! sync only at beginning and end'
|
|
1912
|
+
row = struct.table[1]
|
|
1913
|
+
out = {
|
|
1914
|
+
"row": row,
|
|
1915
|
+
"D": struct._D[combo],
|
|
1916
|
+
"combo": combo,
|
|
1917
|
+
"st_id": struct.run_id,
|
|
1918
|
+
}
|
|
1919
|
+
log.info(f"putting results for {combo}")
|
|
1920
|
+
d = sync.put_combo.remote(out)
|
|
1921
|
+
ray.wait([d])
|
|
1922
|
+
return out
|
|
1923
|
+
|
|
1924
|
+
except Exception as e:
|
|
1925
|
+
log.error(e, "issue with remote run")
|
|
1926
|
+
raise e
|
|
1927
|
+
|
|
1928
|
+
@ray.remote(max_retries=-1, retry_exceptions=True)
|
|
1929
|
+
def remote_section(
|
|
1930
|
+
beamsection,
|
|
1931
|
+
beamname,
|
|
1932
|
+
forces,
|
|
1933
|
+
allowable_stress,
|
|
1934
|
+
combo,
|
|
1935
|
+
x,
|
|
1936
|
+
SF=1.0,
|
|
1937
|
+
return_section=False,
|
|
1938
|
+
):
|
|
1939
|
+
"""optimized method to run 2d fea and return a failure determination"""
|
|
1940
|
+
# TODO: allow post processing ops
|
|
1941
|
+
s = beamsection.calculate_stress(**forces)
|
|
1942
|
+
|
|
1943
|
+
sss = s.get_stress()
|
|
1944
|
+
d_ = {"loads": forces, "beam": beamname, "combo": combo, "x": x}
|
|
1945
|
+
if return_section:
|
|
1946
|
+
d_["stress_analysis"] = s
|
|
1947
|
+
d_["stress_results"] = sss
|
|
1948
|
+
d_["stress_vm_max"] = max([max(ss["sig_vm"]) for ss in sss])
|
|
1949
|
+
d_["fail_frac"] = ff = max([max(ss["sig_vm"] / allowable_stress) for ss in sss])
|
|
1950
|
+
d_["fails"] = fail = ff > 1 / SF
|
|
1951
|
+
|
|
1952
|
+
return d_
|
|
1953
|
+
|
|
1954
|
+
@ray.remote
|
|
1955
|
+
def parallel_combo_run(combo, ref_structure, run_full=False, SF=1.0):
|
|
1956
|
+
# run and store data
|
|
1957
|
+
# self.struct.run(combos=combo)
|
|
1958
|
+
|
|
1959
|
+
log = StructureLog()
|
|
1960
|
+
|
|
1961
|
+
sync = ref_structure["actor_ref"]
|
|
1962
|
+
struct_ref = ref_structure["struct_ref"]
|
|
1963
|
+
log.info(f"running structure {combo}")
|
|
1964
|
+
d = remote_run_combo.remote(ref_structure, combo)
|
|
1965
|
+
log.info(f"waiting on combo {combo}")
|
|
1966
|
+
ray.wait([d])
|
|
1967
|
+
|
|
1968
|
+
# check failures
|
|
1969
|
+
log.info(f"failure analysis {combo}")
|
|
1970
|
+
fail = remote_combo_failure_analysis(ref_structure, combo, run_full, SF)
|
|
1971
|
+
d = sync.put_failure.remote(combo, fail, run_full)
|
|
1972
|
+
log.info(f"failure analysis done {combo}")
|
|
1973
|
+
return ray.get(d)
|
|
1974
|
+
|
|
1975
|
+
#
|
|
1976
|
+
# @ray.remote
|
|
1977
|
+
# class RemoteStructuralAnalysis:
|
|
1978
|
+
# """represents a stateful object in a ray cloud contetxt"""
|
|
1979
|
+
#
|
|
1980
|
+
# def __init__(self, structure, *kw):
|
|
1981
|
+
# self.struct = structure
|
|
1982
|
+
# self.struct.resetSystemLogs()
|
|
1983
|
+
# self.struct.prep_remote_analysis()
|
|
1984
|
+
#
|
|
1985
|
+
# # reference tracking
|
|
1986
|
+
# self.combos_run = {}
|
|
1987
|
+
# self.failed = False
|
|
1988
|
+
# self.report = {"fails": self.failed}
|
|
1989
|
+
# self.report["failures"] = failures = {}
|
|
1990
|
+
# self.failures = failures
|
|
1991
|
+
# self.put_beams = {}
|
|
1992
|
+
#
|
|
1993
|
+
# # can run all combos from base
|
|
1994
|
+
# self.struct_ref = ray.put(self.struct)
|
|
1995
|
+
#
|
|
1996
|
+
# # put beams sections for quick analysis
|
|
1997
|
+
# for beamnm, beam in self.struct.beams.items():
|
|
1998
|
+
# beam_ref = ray.put(beam._section_properties)
|
|
1999
|
+
# self.put_beams[beamnm] = beam_ref
|
|
2000
|
+
#
|
|
2001
|
+
# def get(self, run_id=None):
|
|
2002
|
+
# self.struct.info(f"getting {len(self.table)}")
|
|
2003
|
+
# if run_id is None:
|
|
2004
|
+
# return self.struct._table
|
|
2005
|
+
# else:
|
|
2006
|
+
# return [v for v in self.table if v["st_id"] == run_id]
|
|
2007
|
+
#
|
|
2008
|
+
# def put_combo(self, inv):
|
|
2009
|
+
# """add the data from remotely running the load combo"""
|
|
2010
|
+
#
|
|
2011
|
+
# self.struct.info(f"adding {len(self.struct.table)+1}")
|
|
2012
|
+
# self.struct._table[len(self.struct.table) + 1] = inv["row"]
|
|
2013
|
+
# self.struct.merge_displacement_results(inv["combo"], inv["D"])
|
|
2014
|
+
# self.combos_run[inv["combo"]] = inv
|
|
2015
|
+
# self.struct.info(f'finished analysis of {inv["combo"]}')
|
|
2016
|
+
#
|
|
2017
|
+
# def put_failure(self, combo, fail, run_full=False):
|
|
2018
|
+
# self.struct.info(f"finished failure calc of {combo}")
|
|
2019
|
+
# self.failures[combo] = fail
|
|
2020
|
+
# self.struct.failure_load_exithandler(combo, fail, self.report, run_full)
|
|
2021
|
+
#
|
|
2022
|
+
# async def wait_and_get_failure(self, combo, wait=1, timeout=None):
|
|
2023
|
+
# if combo in self.failures:
|
|
2024
|
+
# return self.failures[combo]
|
|
2025
|
+
#
|
|
2026
|
+
# timeelapsed = 0
|
|
2027
|
+
#
|
|
2028
|
+
# # wait loop
|
|
2029
|
+
# while combo not in self.failures:
|
|
2030
|
+
# self.struct.info(f"waiting on {combo}")
|
|
2031
|
+
# if timeout is not None and timeelapsed > timeout:
|
|
2032
|
+
# raise TimeoutError(f"combo {combo} not found within timeout")
|
|
2033
|
+
# await asyncio.sleep(wait)
|
|
2034
|
+
# timeelapsed += wait
|
|
2035
|
+
#
|
|
2036
|
+
# return self.failures[combo]
|
|
2037
|
+
#
|
|
2038
|
+
# def get_beam_forces(self, beam_name, combo, x):
|
|
2039
|
+
# beam = self.struct.beams[beam_name]
|
|
2040
|
+
# return beam.get_forces_at(x, combo)
|
|
2041
|
+
#
|
|
2042
|
+
# def get_beam_info(self, beam_name):
|
|
2043
|
+
# r = self.put_beams[beam_name]
|
|
2044
|
+
# beam = self.struct.beams[beam_name]
|
|
2045
|
+
# out = {"ref": r, "allowable": beam.material.allowable_stress}
|
|
2046
|
+
# return out
|
|
2047
|
+
#
|
|
2048
|
+
# def estimated_failures(self, combos):
|
|
2049
|
+
# return self.struct.estimated_failures_report(combos=combos)
|
|
2050
|
+
#
|
|
2051
|
+
# def return_structure(self):
|
|
2052
|
+
# return self.struct
|
|
2053
|
+
#
|
|
2054
|
+
# async def ensure(self, combo, wait=1, timeout=None):
|
|
2055
|
+
# if combo in self.combos_run:
|
|
2056
|
+
# return # good to go
|
|
2057
|
+
#
|
|
2058
|
+
# timeelapsed = 0
|
|
2059
|
+
#
|
|
2060
|
+
# # wait loop
|
|
2061
|
+
# while combo not in self.combos_run:
|
|
2062
|
+
# self.struct.info(f"waiting on {combo}")
|
|
2063
|
+
# if timeout is not None and timeelapsed > timeout:
|
|
2064
|
+
# raise TimeoutError(f"combo {combo} not found within timeout")
|
|
2065
|
+
# await asyncio.sleep(wait)
|
|
2066
|
+
# timeelapsed += wait
|
|
2067
|
+
#
|
|
2068
|
+
# return # good to go
|
|
2069
|
+
#
|
|
2070
|
+
# def run_failure_analysis(self, SF=1.0, run_full=False, **kw):
|
|
2071
|
+
# """
|
|
2072
|
+
# Failure Determination:
|
|
2073
|
+
# Beam Stress Estiates are used to determine if a 2D FEA beam/combo analysis should be run. The maximum beam vonmises stress is compared the the beam material allowable stress / saftey factor.
|
|
2074
|
+
#
|
|
2075
|
+
# Override:
|
|
2076
|
+
# Depending on number of load cases, this analysis could take a while. Its encouraged to use a similar pattern of check and exit to reduce load combo size in an effort to fail early. Override this function if nessicary.
|
|
2077
|
+
#
|
|
2078
|
+
# Case / Beam Ordering:
|
|
2079
|
+
# runs the gravity case first and checks for failure to exit early,
|
|
2080
|
+
# then all other combos are run
|
|
2081
|
+
# :returns: a dictionary with 2D fea failures and esimated failures as well for each beam
|
|
2082
|
+
# """
|
|
2083
|
+
# ctx = ray.get_runtime_context()
|
|
2084
|
+
# actor_handle = ctx.current_actor
|
|
2085
|
+
# try:
|
|
2086
|
+
# cases = []
|
|
2087
|
+
# ref = {"struct_ref": self.struct_ref, "actor_ref": actor_handle}
|
|
2088
|
+
# # Run the rest of the load cases
|
|
2089
|
+
# for combo in self.struct.failure_load_combos(
|
|
2090
|
+
# self.report, run_full, remote_sync=actor_handle, **kw
|
|
2091
|
+
# ):
|
|
2092
|
+
# d = parallel_combo_run.remote(
|
|
2093
|
+
# combo, ref, run_full=run_full, SF=SF
|
|
2094
|
+
# )
|
|
2095
|
+
# cases.append(d)
|
|
2096
|
+
#
|
|
2097
|
+
# for coro in asyncio.as_completed(cases):
|
|
2098
|
+
# if self.failed and not run_full:
|
|
2099
|
+
# self.struct.warning(f"structure failed!")
|
|
2100
|
+
# return self.report
|
|
2101
|
+
#
|
|
2102
|
+
# except KeyboardInterrupt:
|
|
2103
|
+
# return self.report
|
|
2104
|
+
#
|
|
2105
|
+
# except Exception as e:
|
|
2106
|
+
# self.struct.error(e, "issue in failur analysis")
|
|
2107
|
+
#
|
|
2108
|
+
# return self.report # tada!
|
|
2109
|
+
|
|
2110
|
+
# Remote Sync Util
|
|
2111
|
+
def remote_combo_failure_analysis(
|
|
2112
|
+
ref_structure, combo, run_full: bool = False, SF: float = 1.0
|
|
2113
|
+
):
|
|
2114
|
+
"""runs a single load combo and adds 2d section failures"""
|
|
2115
|
+
|
|
2116
|
+
sync = ref_structure["actor_ref"]
|
|
2117
|
+
struct_ref = ref_structure["struct_ref"]
|
|
2118
|
+
|
|
2119
|
+
log = StructureLog()
|
|
2120
|
+
|
|
2121
|
+
log.info(f"determine combo {combo} failures...")
|
|
2122
|
+
|
|
2123
|
+
out = {}
|
|
2124
|
+
d = sync.estimated_failures_report.remote(combos=combo)
|
|
2125
|
+
failures = ray.get(d)
|
|
2126
|
+
|
|
2127
|
+
out["estimate"] = failures
|
|
2128
|
+
out["actual"] = check_fail = {}
|
|
2129
|
+
# perform 2d analysis if any estimated
|
|
2130
|
+
if check_est_failures(failures):
|
|
2131
|
+
log.info(f"testing estimated failures...")
|
|
2132
|
+
|
|
2133
|
+
dfail = remote_failure_sections(sync, failures, run_full, SF)
|
|
2134
|
+
out["actual"] = dfail
|
|
2135
|
+
|
|
2136
|
+
if dfail and not run_full:
|
|
2137
|
+
log.info(f"found actual failure...")
|
|
2138
|
+
return out
|
|
2139
|
+
else:
|
|
2140
|
+
log.info(f"no estimated failures found")
|
|
2141
|
+
|
|
2142
|
+
return out
|
|
2143
|
+
|
|
2144
|
+
def remote_failure_sections(
|
|
2145
|
+
sync,
|
|
2146
|
+
failures,
|
|
2147
|
+
run_full: bool = False,
|
|
2148
|
+
SF: float = 1.0,
|
|
2149
|
+
fail_fast=True,
|
|
2150
|
+
group_size=12,
|
|
2151
|
+
):
|
|
2152
|
+
"""takes estimated failures and runs 2D section FEA on those cases"""
|
|
2153
|
+
log = StructureLog()
|
|
2154
|
+
|
|
2155
|
+
secton_results = {}
|
|
2156
|
+
skip_beams = set()
|
|
2157
|
+
cur = []
|
|
2158
|
+
|
|
2159
|
+
for (beamnm, c, x), dat in sort_max_estimated_failures(failures):
|
|
2160
|
+
log.info(
|
|
2161
|
+
f'run remote beam sections: {beamnm},{c} @ {x} {dat["fail_frac"]*100.}%'
|
|
2162
|
+
)
|
|
2163
|
+
if beamnm in skip_beams:
|
|
2164
|
+
continue
|
|
2165
|
+
|
|
2166
|
+
# prep for output
|
|
2167
|
+
if beamnm not in secton_results:
|
|
2168
|
+
secton_results[beamnm] = {}
|
|
2169
|
+
|
|
2170
|
+
# get from sync
|
|
2171
|
+
ray.get(sync.ensure.remote(c))
|
|
2172
|
+
d = sync.get_beam_forces.remote(beamnm, c, x)
|
|
2173
|
+
r = sync.get_beam_info.remote(beamnm)
|
|
2174
|
+
forces, beam_ref = ray.get([d, r])
|
|
2175
|
+
beam_ref = r["ref"]
|
|
2176
|
+
allowable = r["allowable"]
|
|
2177
|
+
|
|
2178
|
+
cur.append(
|
|
2179
|
+
remote_section.remote(beam_ref, beamnm, forces, allowable, c, x, SF=SF)
|
|
2180
|
+
)
|
|
2181
|
+
|
|
2182
|
+
# run the damn thing
|
|
2183
|
+
if len(cur) >= group_size:
|
|
2184
|
+
log.info(f"waiting on {group_size}")
|
|
2185
|
+
ray.wait(cur)
|
|
2186
|
+
|
|
2187
|
+
for res in ray.get(cur):
|
|
2188
|
+
fail = res["fails"]
|
|
2189
|
+
beamnm = res["beam"]
|
|
2190
|
+
combo = res["combo"]
|
|
2191
|
+
x = res["x"]
|
|
2192
|
+
secton_results[res["beam"]][(combo, x)] = res
|
|
2193
|
+
|
|
2194
|
+
if fail:
|
|
2195
|
+
log.warning(f"beam {beamnm} failed @ {x*100:3.0f}%| {c}")
|
|
2196
|
+
if fail_fast:
|
|
2197
|
+
return secton_results
|
|
2198
|
+
if not run_full:
|
|
2199
|
+
skip_beams.add(beamnm)
|
|
2200
|
+
# else:
|
|
2201
|
+
# log.info(f'beam {beamnm} ok @ {x*100:3.0f}%| {c}')
|
|
2202
|
+
|
|
2203
|
+
cur = []
|
|
2204
|
+
log.info(f"done waiting, continue...")
|
|
2205
|
+
|
|
2206
|
+
# finish them!
|
|
2207
|
+
if cur:
|
|
2208
|
+
log.info(f"waiting on {len(cur)}")
|
|
2209
|
+
ray.wait(cur)
|
|
2210
|
+
log.info(f"done, continue...")
|
|
2211
|
+
|
|
2212
|
+
for res in ray.get(cur):
|
|
2213
|
+
fail = res["fails"]
|
|
2214
|
+
beamnm = res["beam"]
|
|
2215
|
+
combo = res["combo"]
|
|
2216
|
+
x = res["x"]
|
|
2217
|
+
secton_results[res["beam"]][(combo, x)] = res
|
|
2218
|
+
|
|
2219
|
+
if fail:
|
|
2220
|
+
log.warning(f"beam {beamnm} failed @ {x*100:3.0f}%| {c}")
|
|
2221
|
+
if fail_fast:
|
|
2222
|
+
return secton_results
|
|
2223
|
+
if not run_full:
|
|
2224
|
+
skip_beams.add(beamnm)
|
|
2225
|
+
# else:
|
|
2226
|
+
# log.info(f'beam {beamnm} ok @ {x*100:3.0f}%| {c}')
|
|
2227
|
+
|
|
2228
|
+
return secton_results
|