engforge 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- engforge/__init__.py +26 -0
- engforge/_testing_components.py +864 -0
- engforge/analysis.py +257 -0
- engforge/attr_dynamics.py +222 -0
- engforge/attr_plotting.py +811 -0
- engforge/attr_signals.py +116 -0
- engforge/attr_slots.py +211 -0
- engforge/attr_solver.py +443 -0
- engforge/attributes.py +326 -0
- engforge/common.py +76 -0
- engforge/component_collections.py +276 -0
- engforge/components.py +55 -0
- engforge/configuration.py +673 -0
- engforge/dataframe.py +185 -0
- engforge/datastores/__init__.py +116 -0
- engforge/datastores/data.py +519 -0
- engforge/datastores/gdocs.py +2650 -0
- engforge/datastores/reporting.py +935 -0
- engforge/datastores/secrets.py +549 -0
- engforge/dynamics.py +662 -0
- engforge/eng/__init__.py +0 -0
- engforge/eng/costs.py +1459 -0
- engforge/eng/fluid_material.py +279 -0
- engforge/eng/geometry.py +1060 -0
- engforge/eng/pipes.py +1211 -0
- engforge/eng/prediction.py +366 -0
- engforge/eng/solid_materials.py +445 -0
- engforge/eng/structure.py +2228 -0
- engforge/eng/structure_beams.py +821 -0
- engforge/eng/thermodynamics.py +271 -0
- engforge/engforge_attributes.py +380 -0
- engforge/env_var.py +214 -0
- engforge/locations.py +18 -0
- engforge/logging.py +290 -0
- engforge/patterns.py +449 -0
- engforge/problem_context.py +2251 -0
- engforge/properties.py +501 -0
- engforge/reporting.py +595 -0
- engforge/solveable.py +964 -0
- engforge/solver.py +316 -0
- engforge/solver_utils.py +726 -0
- engforge/system.py +143 -0
- engforge/system_reference.py +299 -0
- engforge/tabulation.py +288 -0
- engforge/test/__init__.py +2 -0
- engforge/test/_pre_test_structures.py +221 -0
- engforge/test/report_testing.py +158 -0
- engforge/test/test_airfilter.py +91 -0
- engforge/test/test_analysis.py +3 -0
- engforge/test/test_comp_iter.py +196 -0
- engforge/test/test_composition.py +139 -0
- engforge/test/test_costs.py +338 -0
- engforge/test/test_dynamics.py +109 -0
- engforge/test/test_dynamics_spaces.py +39 -0
- engforge/test/test_four_bar.py +97 -0
- engforge/test/test_modules.py +72 -0
- engforge/test/test_performance.py +93 -0
- engforge/test/test_pipes.py +49 -0
- engforge/test/test_problem.py +411 -0
- engforge/test/test_problem_deepscoping.py +68 -0
- engforge/test/test_slider_crank.py +51 -0
- engforge/test/test_solver.py +343 -0
- engforge/test/test_tabulation.py +233 -0
- engforge/typing.py +92 -0
- engforge-0.3.0.dist-info/METADATA +392 -0
- engforge-0.3.0.dist-info/RECORD +69 -0
- engforge-0.3.0.dist-info/WHEEL +5 -0
- engforge-0.3.0.dist-info/licenses/LICENSE +7 -0
- engforge-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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)
|