engforge 0.3.0__py3-none-any.whl

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