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,821 @@
1
+ from collections.abc import Iterable
2
+ import attr
3
+ import numpy
4
+ import functools
5
+ import shapely
6
+ import pandas
7
+ from shapely.geometry import Polygon, Point
8
+
9
+ from engforge.tabulation import (
10
+ TABLE_TYPES,
11
+ NUMERIC_VALIDATOR,
12
+ system_property,
13
+ )
14
+ from engforge.configuration import forge, Configuration
15
+ from engforge.components import Component
16
+
17
+ # from engforge.analysis import Analysis
18
+ from engforge.properties import system_property
19
+ from engforge.system import System
20
+ from engforge.eng.solid_materials import *
21
+ from engforge.common import *
22
+ import engforge.eng.geometry as enggeo
23
+ from engforge.eng.costs import CostModel, cost_property
24
+
25
+ import sectionproperties
26
+ import sectionproperties.pre.geometry as geometry
27
+ import sectionproperties.pre.library.primitive_sections as sections
28
+ import sectionproperties.analysis.section as cross_section
29
+ import Pynite as pynite
30
+
31
+ # from Pynite import Visualization
32
+ import copy
33
+
34
+ nonetype = type(None)
35
+
36
+
37
+ def rotation_matrix_from_vectors(vec1, vec2):
38
+ """Find the rotation matrix that aligns vec1 to vec2
39
+ :param vec1: A 3d "source" vector
40
+ :param vec2: A 3d "destination" vector
41
+ :return mat: A transform matrix (3x3) which when applied to vec1, aligns it with vec2.
42
+ """
43
+ a, b = (vec1 / numpy.linalg.norm(vec1)).reshape(3), (
44
+ vec2 / numpy.linalg.norm(vec2)
45
+ ).reshape(3)
46
+ v = numpy.cross(a, b)
47
+ if any(v): # if not all zeros then
48
+ c = numpy.dot(a, b)
49
+ s = numpy.linalg.norm(v)
50
+ kmat = numpy.array([[0, -v[2], v[1]], [v[2], 0, -v[0]], [-v[1], v[0], 0]])
51
+ return numpy.eye(3) + kmat + kmat.dot(kmat) * ((1 - c) / (s**2))
52
+
53
+ else:
54
+ return numpy.eye(3) # cross of all zeros only occurs on identical directions
55
+
56
+
57
+ @forge
58
+ class Beam(Component):
59
+ """Beam is a wrapper for emergent useful properties of the structure"""
60
+
61
+ # parent structure, will be in its _beams
62
+ structure: "Structure" = attr.ib()
63
+
64
+ name: str = attr.ib()
65
+ material: "SolidMaterial" = attr.ib(
66
+ validator=attr.validators.instance_of(SolidMaterial),
67
+ )
68
+ section: "Profile2D" = attr.ib(
69
+ validator=attr.validators.instance_of(
70
+ (
71
+ geometry.Geometry,
72
+ enggeo.Profile2D,
73
+ type(None),
74
+ )
75
+ )
76
+ )
77
+
78
+ # Section Overrides
79
+ in_Iy: float = attr.ib(
80
+ default=None,
81
+ validator=attr.validators.instance_of((int, float, nonetype)),
82
+ )
83
+ in_Ix: float = attr.ib(
84
+ default=None,
85
+ validator=attr.validators.instance_of((int, float, nonetype)),
86
+ )
87
+ in_J: float = attr.ib(
88
+ default=None,
89
+ validator=attr.validators.instance_of((int, float, nonetype)),
90
+ )
91
+ in_A: float = attr.ib(
92
+ default=None,
93
+ validator=attr.validators.instance_of((int, float, nonetype)),
94
+ )
95
+ # by default assume symmetric
96
+ in_Ixy: float = attr.ib(
97
+ default=0.0,
98
+ validator=attr.validators.instance_of((int, float, nonetype)),
99
+ )
100
+
101
+ min_mesh_size: float = attrs.field(default=0.01)
102
+ analysis_intervals: int = attrs.field(default=3)
103
+
104
+ _L = None
105
+ _ITensor = None
106
+
107
+ min_stress_xy = None # set to true or false
108
+
109
+ def __on_init__(self):
110
+ self.debug("initalizing...")
111
+ # update material
112
+ if isinstance(self.section, enggeo.ShapelySection):
113
+ if self.section.material is None:
114
+ raise Exception("No Section Material")
115
+ else:
116
+ self.material = self.section.material
117
+ self.update_section(self.section)
118
+
119
+ def update_section(self, section):
120
+ self.section = section
121
+
122
+ self.debug(f"determining {section} properties...")
123
+ if isinstance(self.section, geometry.Geometry):
124
+ self.debug(f"converting to shapelysection {section}")
125
+ if not self.section.material:
126
+ material = self.material
127
+ else:
128
+ material = self.section.material
129
+ self.section = enggeo.ShapelySection(
130
+ shape=self.section, material=self.material
131
+ )
132
+
133
+ if isinstance(self.section, enggeo.ShapelySection):
134
+ self.debug(f"determining profile {section} properties...")
135
+ self.in_Ix = self.section.Ixx
136
+ self.in_Iy = self.section.Iyy
137
+ self.in_J = self.section.J
138
+ self.in_A = self.section.A
139
+ self.in_Ixy = self.section.Ixy
140
+
141
+ elif isinstance(self.section, enggeo.Profile2D):
142
+ self.warning(f"use shapely section instead {section} properties...")
143
+ # raise Exception("use shapely section instead")
144
+ self.in_Ix = self.section.Ixx
145
+ self.in_Iy = self.section.Iyy
146
+ self.in_J = self.section.J
147
+ self.in_A = self.section.A
148
+ else:
149
+ raise Exception(f"unhanelded input {self.section}")
150
+
151
+ self.debug(f"checking input values")
152
+ assert all(
153
+ [val is not None for val in (self.in_Iy, self.in_Ix, self.in_J, self.in_A)]
154
+ )
155
+
156
+ # Assemble tensor
157
+ T = [[self.in_Ix, self.in_Ixy], [self.in_Ixy, self.in_Iy]]
158
+ self._ITensor = numpy.array(T)
159
+
160
+ @system_property
161
+ def current_combo(self) -> str:
162
+ return self.structure.current_combo
163
+
164
+ @system_property
165
+ def L(self) -> float:
166
+ return self.length
167
+
168
+ @system_property
169
+ def length(self) -> float:
170
+ return self.member.L()
171
+
172
+ @property
173
+ def member(self):
174
+ return self.structure.members[self.name]
175
+
176
+ @property
177
+ def n1(self):
178
+ return self.member.i_node
179
+
180
+ @property
181
+ def n2(self):
182
+ return self.member.j_node
183
+
184
+ @property
185
+ def P1(self):
186
+ return numpy.array([self.n1.X, self.n1.Y, self.n1.Z])
187
+
188
+ @property
189
+ def P2(self):
190
+ return numpy.array([self.n2.X, self.n2.Y, self.n2.Z])
191
+
192
+ @system_property
193
+ def E(self) -> float:
194
+ return self.material.E
195
+
196
+ @system_property
197
+ def G(self) -> float:
198
+ return self.material.G
199
+
200
+ @property
201
+ def ITensor(self):
202
+ return self._ITensor
203
+
204
+ @system_property
205
+ def Iy(self) -> float:
206
+ return self.in_Iy
207
+
208
+ @system_property
209
+ def Ix(self) -> float:
210
+ return self.in_Ix
211
+
212
+ @system_property
213
+ def J(self) -> float:
214
+ return self.in_J
215
+
216
+ @system_property
217
+ def A(self) -> float:
218
+ return self.in_A
219
+
220
+ @property
221
+ def Ao(self):
222
+ """outside area, over ride for hallow sections"""
223
+ if isinstance(self.section, enggeo.Profile2D):
224
+ return self.section.Ao
225
+ return self.A
226
+
227
+ @system_property
228
+ def Ixy(self) -> float:
229
+ return self.ITensor[0, 1]
230
+
231
+ @system_property
232
+ def Imx(self) -> float:
233
+ return self.material.density * self.Ix
234
+
235
+ @system_property
236
+ def Imy(self) -> float:
237
+ return self.material.density * self.Iy
238
+
239
+ @system_property
240
+ def Imxy(self) -> float:
241
+ return self.material.density * self.Ixy
242
+
243
+ @system_property
244
+ def Jm(self) -> float:
245
+ return self.material.density * self.J
246
+
247
+ @system_property
248
+ def Imz(self) -> float:
249
+ return self.mass * self.L**2.0 / 12.0
250
+
251
+ @system_property
252
+ def Iz(self) -> float:
253
+ """Outside area inertia on z"""
254
+ return self.A * self.L**2.0 / 12.0
255
+
256
+ @system_property
257
+ def Izo(self) -> float:
258
+ """Outside area inertia on z"""
259
+ return self.Ao * self.L**2.0 / 12.0
260
+
261
+ @property
262
+ def LOCAL_INERTIA(self):
263
+ """the mass inertia tensor in local frame"""
264
+ return numpy.array(
265
+ [
266
+ [self.Imz + self.Imx, self.Imxy, 0.0],
267
+ [self.Imxy, self.Imz + self.Imy, 0.0],
268
+ [0.0, 0.0, self.Jm],
269
+ ]
270
+ )
271
+
272
+ @property
273
+ def INERTIA(self):
274
+ """the mass inertia tensor in global structure frame"""
275
+ # TODO: include rotation from beam uv vector frame? is it ok already?
276
+ return self.RotationMatrix.dot(self.LOCAL_INERTIA)
277
+
278
+ @property
279
+ def GLOBAL_INERTIA(self):
280
+ """Returns the rotated inertia matrix with structure relative parallal axis contribution"""
281
+ return self.INERTIA + self.CG_RELATIVE_INERTIA
282
+
283
+ @property
284
+ def CG_RELATIVE_INERTIA(self):
285
+ """the mass inertia tensor in global frame"""
286
+ dcg = self.cog - self.structure.cog
287
+ return self.mass * numpy.eye(3) * dcg * dcg.T
288
+
289
+ @system_property
290
+ def Vol(self) -> float:
291
+ return self.A * self.L
292
+
293
+ @system_property
294
+ def Vol_outside(self) -> float:
295
+ return self.Ao * self.L
296
+
297
+ @system_property
298
+ def section_mass(self) -> float:
299
+ return self.material.density * self.A
300
+
301
+ @system_property
302
+ def mass(self) -> float:
303
+ return self.material.density * self.Vol
304
+
305
+ # @system_property(category="mfg,material,beams")
306
+ @system_property
307
+ def cost(self) -> float:
308
+ return self.mass * self.material.cost_per_kg
309
+
310
+ @property
311
+ def centroid2d(self):
312
+ return self.section._sec.get_c()
313
+
314
+ @instance_cached
315
+ def centroid3d(self):
316
+ return self.L_vec / 2.0 + self.P1
317
+
318
+ @property
319
+ def n_vec(self):
320
+ return self.L_vec / self.L
321
+
322
+ @property
323
+ def L_vec(self):
324
+ return self.P2 - self.P1
325
+
326
+ @property
327
+ def cog(self):
328
+ return self.centroid3d
329
+
330
+ # Geometry Tabulation
331
+ # p1
332
+ @system_property
333
+ def X1(self) -> float:
334
+ return self.P1[0]
335
+
336
+ @system_property
337
+ def Y1(self) -> float:
338
+ return self.P1[1]
339
+
340
+ @system_property
341
+ def Z1(self) -> float:
342
+ return self.P1[2]
343
+
344
+ # p2
345
+ @system_property
346
+ def X2(self) -> float:
347
+ return self.P2[0]
348
+
349
+ @system_property
350
+ def Y2(self) -> float:
351
+ return self.P2[1]
352
+
353
+ @system_property
354
+ def Z2(self) -> float:
355
+ return self.P2[2]
356
+
357
+ # cg
358
+ @system_property
359
+ def Xcg(self) -> float:
360
+ return self.centroid3d[0]
361
+
362
+ @system_property
363
+ def Ycg(self) -> float:
364
+ return self.centroid3d[1]
365
+
366
+ @system_property
367
+ def Zcg(self) -> float:
368
+ return self.centroid3d[2]
369
+
370
+ @property
371
+ def RotationMatrix(self):
372
+ n_o = [
373
+ 1,
374
+ 0,
375
+ 0,
376
+ ]
377
+ # n_vec is along Z, so we must tranlate from the along axis which is z
378
+ return rotation_matrix_from_vectors(n_o, self.n_vec)
379
+
380
+ @property
381
+ def ReverseRotationMatrix(self):
382
+ # FIXME: Ensure that this is the correct orientation
383
+ return self.RotationMatrix.T
384
+
385
+ def section_results(self):
386
+ return self.section._sec.display_results()
387
+
388
+ def show_mesh(self):
389
+ return self.section._sec.plot_mesh()
390
+
391
+ def estimate_stress(self, force_calc=True, **forces):
392
+ """uses the best available method to determine max stress in the beam, for ShapelySections this is done through a learning process, for other sections it is done through a simple calculation aimed at providing a conservative estimate"""
393
+ if isinstance(self.section, enggeo.ShapelySection):
394
+ return self.section.estimate_stress(**forces, force_calc=force_calc)
395
+ else:
396
+ return self._fallback_estimate_stress(**forces)
397
+
398
+ def _fallback_estimate_stress(self, n, vx, vy, mxx, myy, mzz, SM=5, **extra):
399
+ """sum the absolute value of each stress component. This isn't accurate but each value here should represent the worst sections, and take the 1-norm to max for each type of stress"""
400
+
401
+ if self.section.x_bounds is None:
402
+ self.warning(f"bad section bounds")
403
+ self.section.calculate_bounds()
404
+
405
+ m_y = max([abs(v) for v in self.section.y_bounds])
406
+ m_x = max([abs(v) for v in self.section.x_bounds])
407
+
408
+ sigma_n = n / self.in_A
409
+
410
+ sigma_bx = mxx * m_y / self.in_Ix
411
+ sigma_by = myy * m_x / self.in_Iy
412
+ sigma_tw = mzz * max(m_y, m_x) / self.in_J
413
+
414
+ # A2 = self.in_A/2.
415
+ # experimental
416
+ # sigma_vx = vx * (A2*m_y/2) / (self.in_Iy*m_x)
417
+ # sigma_vy = vy * (A2*m_x/2) / (self.in_Ix*m_y)
418
+
419
+ # assume circle (worst case)
420
+ sigma_vx = vx * 0.75 / self.in_A
421
+ sigma_vy = vy * 0.75 / self.in_A
422
+
423
+ max_bend = abs(sigma_bx) + abs(sigma_by)
424
+ max_shear = abs(sigma_vx) + abs(sigma_vy)
425
+
426
+ return (abs(sigma_n) + max_bend + abs(sigma_tw) + max_shear) * SM
427
+
428
+ def get_forces_at(self, x, combo=None, lower=True, skip_principle=True):
429
+ """outputs pynite results in section_properties.calculate_stress() input"""
430
+ if combo is None:
431
+ combo = self.structure.current_combo
432
+
433
+ x = x * self.L # frac of L
434
+ principles = ["m11", "m22", "m33"]
435
+
436
+ inp = dict(
437
+ N=self.member.axial(x, combo),
438
+ Vx=self.member.shear("Fz", x, combo),
439
+ Vy=self.member.shear("Fy", x, combo),
440
+ Mxx=self.member.moment("Mz", x, combo),
441
+ Myy=self.member.moment("My", x, combo),
442
+ M11=0,
443
+ M22=0,
444
+ Mzz=self.member.torque(x, combo),
445
+ )
446
+ inp = {
447
+ k.lower(): v
448
+ for k, v in inp.items()
449
+ if any([k.lower() not in principles]) or not skip_principle
450
+ }
451
+ return inp
452
+
453
+ def get_stress_at(self, x, combo=None, **kw):
454
+ """takes force input and runs stress calculation as a fraction of material properties"""
455
+ if combo is None:
456
+ combo = self.structure.current_combo
457
+
458
+ inp = self.get_forces_at(x, combo)
459
+
460
+ # preferr real stresses to fail fraction
461
+ if "value" not in kw:
462
+ kw["value"] = True
463
+
464
+ return self.calculate_stress(**inp, **kw)
465
+
466
+ def calculate_stress(self, **forces):
467
+ """takes force input and runs stress calculation as a fraction of material properties, see ShapelySection.calculate_stress() for more details"""
468
+ # preferr real stresses to fail fraction
469
+ if "value" not in forces:
470
+ forces["value"] = True
471
+
472
+ return self.section.calculate_stress(**forces)
473
+
474
+ @property
475
+ def Fg(self):
476
+ """force of gravity"""
477
+ return numpy.array([0, 0, -self.mass * g])
478
+
479
+ # RESULTS:
480
+ @system_property
481
+ def max_stress_estimate(self) -> float:
482
+ """estimates these are proportional to stress but 2D FEA is "truth" since we lack cross section specifics"""
483
+ if not self.structure._any_solved:
484
+ return numpy.nan
485
+
486
+ vals = [
487
+ self.estimate_stress(**self.get_forces_at(x), value=True)
488
+ for x in [0, 0.5, 1]
489
+ ]
490
+
491
+ try:
492
+ return max(vals)
493
+
494
+ except Exception as e:
495
+ self.warning(f"failed to get max stress estimate {e}| {vals}")
496
+ return numpy.nan
497
+
498
+ @system_property
499
+ def fail_factor_estimate(self) -> float:
500
+ """the ratio of max estimated stress to the material's allowable stress"""
501
+ if not self.structure._any_solved:
502
+ return numpy.nan
503
+ return self.max_stress_estimate / self.material.allowable_stress
504
+
505
+ # axial
506
+ @system_property
507
+ def min_axial(self) -> float:
508
+ if not self.structure._any_solved:
509
+ return numpy.nan
510
+ return self.member.min_axial(self.structure.current_combo)
511
+
512
+ @system_property
513
+ def max_axial(self) -> float:
514
+ if not self.structure._any_solved:
515
+ return numpy.nan
516
+ return self.member.max_axial(self.structure.current_combo)
517
+
518
+ # deflection
519
+ @system_property
520
+ def min_deflection_x(self) -> float:
521
+ if not self.structure._any_solved:
522
+ return numpy.nan
523
+ return self.member.min_deflection("dx", self.structure.current_combo)
524
+
525
+ @system_property
526
+ def max_deflection_x(self) -> float:
527
+ if not self.structure._any_solved:
528
+ return numpy.nan
529
+ return self.member.max_deflection("dx", self.structure.current_combo)
530
+
531
+ @system_property
532
+ def min_deflection_y(self) -> float:
533
+ if not self.structure._any_solved:
534
+ return numpy.nan
535
+ return self.member.min_deflection("dy", self.structure.current_combo)
536
+
537
+ @system_property
538
+ def max_deflection_y(self) -> float:
539
+ if not self.structure._any_solved:
540
+ return numpy.nan
541
+ return self.member.max_deflection("dy", self.structure.current_combo)
542
+
543
+ # torsion
544
+ @system_property
545
+ def min_torsion(self) -> float:
546
+ if not self.structure._any_solved:
547
+ return numpy.nan
548
+ return self.member.min_torque(self.structure.current_combo)
549
+
550
+ @system_property
551
+ def max_torsion(self) -> float:
552
+ if not self.structure._any_solved:
553
+ return numpy.nan
554
+ return self.member.max_torque(self.structure.current_combo)
555
+
556
+ # shear
557
+ @system_property
558
+ def min_shear_z(self) -> float:
559
+ if not self.structure._any_solved:
560
+ return numpy.nan
561
+ return self.member.min_shear("Fz", self.structure.current_combo)
562
+
563
+ @system_property
564
+ def max_shear_z(self) -> float:
565
+ if not self.structure._any_solved:
566
+ return numpy.nan
567
+ return self.member.max_shear("Fz", self.structure.current_combo)
568
+
569
+ @system_property
570
+ def min_shear_y(self) -> float:
571
+ if not self.structure._any_solved:
572
+ return numpy.nan
573
+ return self.member.min_shear("Fy", self.structure.current_combo)
574
+
575
+ @system_property
576
+ def max_shear_y(self) -> float:
577
+ if not self.structure._any_solved:
578
+ return numpy.nan
579
+ return self.member.max_shear("Fy", self.structure.current_combo)
580
+
581
+ # moment
582
+ @system_property
583
+ def min_moment_z(self) -> float:
584
+ if not self.structure._any_solved:
585
+ return numpy.nan
586
+ return self.member.min_moment("Mz", self.structure.current_combo)
587
+
588
+ @system_property
589
+ def max_moment_z(self) -> float:
590
+ if not self.structure._any_solved:
591
+ return numpy.nan
592
+ return self.member.max_moment("Mz", self.structure.current_combo)
593
+
594
+ @system_property
595
+ def min_moment_y(self) -> float:
596
+ if not self.structure._any_solved:
597
+ return numpy.nan
598
+ return self.member.min_moment("My", self.structure.current_combo)
599
+
600
+ @system_property
601
+ def max_moment_y(self) -> float:
602
+ if not self.structure._any_solved:
603
+ return numpy.nan
604
+ return self.member.max_moment("My", self.structure.current_combo)
605
+
606
+ # Load Application
607
+ def get_valid_force_choices(only_local=False, only_global=False, use_moment=True):
608
+ if only_local or only_global:
609
+ assert only_global != only_local, "choose local or global"
610
+
611
+ floc = set(["Fx", "Fy", "Fz"])
612
+ fglb = set(["FX", "FY", "FZ"])
613
+ mloc = set(["Mx", "My", "Mz"])
614
+ mglb = set(["MX", "MY", "MZ"])
615
+
616
+ if only_global:
617
+ if use_moment:
618
+ return list(set.union(*(fglb, mglb)))
619
+ return list(fglb)
620
+
621
+ elif only_local:
622
+ if use_moment:
623
+ return list(set.union(*(floc, mloc)))
624
+ return list(floc)
625
+
626
+ else:
627
+ if use_moment:
628
+ return list(set.union(*(floc, mloc, fglb, mglb)))
629
+ return list(set.union(*(floc, fglb)))
630
+
631
+ # FORCE APPLICATION (TODO: update for global input 0.0.78)
632
+ def apply_pt_load(self, x_frac, case=None, **kwargs):
633
+ """add a force in a global orientation"""
634
+ if case is None:
635
+ case = self.structure.default_case
636
+
637
+ # adjust x for relative input
638
+ x = x_frac * self.L
639
+
640
+ valid = self.get_valid_force_choices(use_moment=True, only_global=True)
641
+ fin = [(v, kwargs[v]) for v in valid if v in kwargs]
642
+ for Fkey, Fval in fin:
643
+ if Fval:
644
+ self.debug(f"adding {Fkey}={Fval}")
645
+ self.structure.frame.add_member_pt_load(
646
+ self.member.name, Fkey, Fval, x, case=case
647
+ )
648
+
649
+ def apply_distributed_load(self, start_factor=1, end_factor=1, case=None, **kwargs):
650
+ """add forces in global vector"""
651
+ if case is None:
652
+ case = self.structure.default_case
653
+ valid = self.get_valid_force_choices(use_moment=False, only_global=True)
654
+ fin = [(v, kwargs[v]) for v in valid if v in kwargs]
655
+ for Fkey, Fval in fin:
656
+ if Fval:
657
+ self.debug(f"adding dist {Fkey}={Fval}")
658
+ self.structure.frame.add_member_dist_load(
659
+ self.member.name,
660
+ Fkey,
661
+ Fval * start_factor,
662
+ Fval * end_factor,
663
+ case=case,
664
+ )
665
+
666
+ def apply_local_pt_load(self, x, case=None, **kwargs):
667
+ """add a force in a global orientation"""
668
+ if case is None:
669
+ case = self.structure.default_case
670
+ valid = self.get_valid_force_choices(only_local=True, use_moment=True)
671
+ fin = [(v, kwargs[v]) for v in valid if v in kwargs]
672
+ for Fkey, Fval in fin:
673
+ if Fval:
674
+ self.debug(f"adding {Fkey}={Fval}")
675
+ self.structure.frame.add_member_pt_load(
676
+ self.member.name, Fkey, Fval, x, case=case
677
+ )
678
+
679
+ def apply_local_distributed_load(
680
+ self, start_factor=1, end_factor=1, case=None, **kwargs
681
+ ):
682
+ """add forces in global vector"""
683
+ if case is None:
684
+ case = self.structure.default_case
685
+ valid = self.get_valid_force_choices(only_local=True, use_moment=False)
686
+ fin = [(v, kwargs[v]) for v in valid if v in kwargs]
687
+ for Fkey, Fval in fin:
688
+ if Fval:
689
+ self.debug(f"adding dist {Fkey}={Fval}")
690
+ self.structure.frame.add_member_dist_load(
691
+ self.member.name,
692
+ Fkey,
693
+ Fval * start_factor,
694
+ Fval * end_factor,
695
+ case=case,
696
+ )
697
+
698
+ def apply_gravity_force_distribution(self, sv=1, ev=1, z_dir="FZ", z_mag=-1):
699
+ # TODO: ensure that integral of sv, ev is 1, and all positive
700
+ self.debug(f"applying gravity distribution to {self.name}")
701
+ for case in self.structure.gravity_cases:
702
+ total_weight = self.mass * self.structure.gravity_mag
703
+ d = {z_dir: z_mag * total_weight}
704
+ self.apply_distributed_load(case=self.structure.gravity_name, **d)
705
+
706
+ def apply_gravity_force(self, x_frac=0.5, z_dir="FZ", z_mag=-1):
707
+ self.debug(f"applying gravity to {self.name}")
708
+ for case in self.structure.gravity_cases:
709
+ total_weight = self.mass * self.structure.gravity_mag
710
+ d = {z_dir: z_mag * total_weight}
711
+ self.apply_pt_load(x_frac, case=self.structure.gravity_name, **d)
712
+
713
+ def __dir__(self) -> Iterable[str]:
714
+ d = set(super().__dir__())
715
+ return list(d.union(dir(Beam)))
716
+
717
+ def max_von_mises(self) -> float:
718
+ """The worst of the worst cases, after adjusting the beem orientation for best loading"""
719
+ # TODO: make faster system property
720
+ return numpy.nanmax([self.max_von_mises_by_case()])
721
+
722
+ def max_von_mises_by_case(self, combos=None):
723
+ """Gathers max vonmises stress info per case"""
724
+
725
+ cmprv = {}
726
+ for rxy in [True, False]:
727
+ new = []
728
+ out = self.von_mises_stress_l
729
+ for cmbo, vm_stress_vec in out.items():
730
+ if combos and cmbo not in combos:
731
+ continue
732
+ new.append(numpy.nanmax(vm_stress_vec))
733
+ cmprv[rxy] = numpy.array(new)
734
+
735
+ if cmprv[True] and cmprv[False]:
736
+ vt = numpy.nanmax(cmprv[True])
737
+ vf = numpy.nanmax(cmprv[False])
738
+
739
+ # We choose the case with the
740
+ if vf < vt:
741
+ self.min_stress_xy = False
742
+ return vf
743
+
744
+ self.min_stress_xy = True
745
+ return vt
746
+
747
+ # TODO: Breakout other stress vectors
748
+ @solver_cached
749
+ def von_mises_stress_l(self):
750
+ """Max von-mises stress"""
751
+ out = {}
752
+ sect_stresses = self.section_stresses()
753
+ for combo in self.structure.frame.LoadCombos:
754
+ rows = []
755
+ for i in numpy.linspace(0, 1, self.analysis_intervals):
756
+ max_vm = sect_stresses[combo][i]
757
+ rows.append(max_vm)
758
+ out[combo] = numpy.array(rows)
759
+ return out
760
+
761
+ def section_stresses(self, **kwargs):
762
+ # FIXME: enable: assert self.structure.solved, f'must be solved first!'
763
+ combos = {}
764
+ for combo in self.structure.frame.LoadCombos:
765
+ combos[combo] = spans = {}
766
+ for i in numpy.linspace(0, 1, self.analysis_intervals):
767
+ self.info(f"evaluating stresses for {combo} @ {i} w {kwargs}")
768
+ sol = self.get_stress_at(i, combo, **kwargs)
769
+ spans[i] = sol
770
+ return combos
771
+
772
+ # #TODO: remove or refactor
773
+ # @solver_cached
774
+ # def stress_info(self):
775
+ # """Max profile stress info along beam for each type"""
776
+ # rows = []
777
+ # for combo in self.structure.frame.LoadCombos:
778
+ # for i in numpy.linspace(0, 1, self.analysis_intervals):
779
+ # mat_stresses = self.section_stresses[combo][i].get_stress()
780
+ # oout = {"x": i, "combo": combo}
781
+ # for stresses in mat_stresses:
782
+ # max_vals = {
783
+ # sn
784
+ # + "_max_"
785
+ # + stresses["Material"]: numpy.nanmax(stress)
786
+ # for sn, stress in stresses.items()
787
+ # if isinstance(stress, numpy.ndarray)
788
+ # }
789
+ # min_vals = {
790
+ # sn
791
+ # + "_min_"
792
+ # + stresses["Material"]: numpy.nanmin(stress)
793
+ # for sn, stress in stresses.items()
794
+ # if isinstance(stress, numpy.ndarray)
795
+ # }
796
+ # avg_vals = {
797
+ # sn
798
+ # + "_avg_"
799
+ # + stresses["Material"]: numpy.nanmean(stress)
800
+ # for sn, stress in stresses.items()
801
+ # if isinstance(stress, numpy.ndarray)
802
+ # }
803
+ # # Make some simple to determine dataframe failure prediction
804
+ # factor_of_saftey = (
805
+ # self.material.yield_strength
806
+ # / numpy.nanmax(stresses["sig_vm"])
807
+ # )
808
+ # fail_frac = (
809
+ # numpy.nanmax(stresses["sig_vm"])
810
+ # / self.material.allowable_stress
811
+ # )
812
+ # fsnm = stresses["Material"] + "_saftey_factor"
813
+ # fsff = stresses["Material"] + "_fail_frac"
814
+ # allowable = {fsnm: factor_of_saftey, fsff: fail_frac}
815
+ # oout.update(allowable)
816
+ # oout.update(max_vals)
817
+ # oout.update(min_vals)
818
+ # oout.update(avg_vals)
819
+ # rows.append(oout)
820
+ #
821
+ # return pandas.DataFrame(rows)