Skip to content

Commit b917aa4

Browse files
Implement yield moment, update theory, add to display_results(), add more tests
1 parent c3b0ef2 commit b917aa4

File tree

4 files changed

+283
-7
lines changed

4 files changed

+283
-7
lines changed

docs/user_guide/theory.rst

+10-1
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,16 @@ extreme (min. and max.) coordinates of the cross-section in the x and y-directio
423423
Yield Moments
424424
~~~~~~~~~~~~~
425425

426-
TODO
426+
The yield moment is defined as the lowest bending moment that causes any point within
427+
cross-section to reach the yield strength. Note that this implementation is purely
428+
linear-elastic i.e. uses the linear-elastic modulus and bi-directional yield strength
429+
only.
430+
431+
``sectionproperties`` applies a unit bending moment, about each axis separately, and
432+
determines the yield index for each point within the mesh. The yield index is defined as
433+
the stress divided by the material yield strength. Through this method, a critical yield
434+
index is determined (i.e. point which will yield first under bending) and the yield
435+
moment calculated as the inverse of the critical yield index.
427436

428437
.. _label-theory-plastic-section-moduli:
429438

src/sectionproperties/analysis/section.py

+67-1
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,73 @@ def calculate_geom(progress: Progress | None = None) -> None:
273273
self.section_props.my_11 = 0.0
274274
self.section_props.my_22 = 0.0
275275

276-
# TODO: calculate yield moments
276+
# calculate yield moments:
277+
# 1) loop through each material group and through each element in the group
278+
# 2) for each point, calculate the bending stress from a unit bending moment
279+
# in each direction (mxx, myy, m11, m22)
280+
# 3) from this, calculate the yield index for each point
281+
# 4) get the largest yield index and scale the bending moment such that the
282+
# yield index is 1
283+
284+
# initialise the yield indexes
285+
yield_index = {
286+
"mxx": 0.0,
287+
"myy": 0.0,
288+
"m11": 0.0,
289+
"m22": 0.0,
290+
}
291+
292+
# get useful section properties
293+
cx = self.section_props.cx
294+
cy = self.section_props.cy
295+
phi = self.section_props.phi
296+
ixx = self.section_props.ixx_c
297+
iyy = self.section_props.iyy_c
298+
ixy = self.section_props.ixy_c
299+
i11 = self.section_props.i11_c
300+
i22 = self.section_props.i22_c
301+
302+
if ixx is None or iyy is None or ixy is None or i11 is None or i22 is None:
303+
msg = "Section properties failed to save."
304+
raise RuntimeError(msg)
305+
306+
# loop through each material group
307+
for group in self.material_groups:
308+
em = group.material.elastic_modulus
309+
fy = group.material.yield_strength
310+
311+
# loop through each element in the material group
312+
for el in group.elements:
313+
# loop through each node in the element
314+
for coord in el.coords.transpose():
315+
# calculate coordinates wrt centroidal & principal axes
316+
x = coord[0] - cx
317+
y = coord[1] - cy
318+
x11, y22 = fea.principal_coordinate(phi=phi, x=x, y=y)
319+
320+
# calculate bending stresses due to unit moments
321+
sig_mxx = em * (
322+
-ixy / (ixx * iyy - ixy**2) * x
323+
+ iyy / (ixx * iyy - ixy**2) * y
324+
)
325+
sig_myy = em * (
326+
-ixx / (ixx * iyy - ixy**2) * x
327+
+ ixy / (ixx * iyy - ixy**2) * y
328+
)
329+
sig_m11 = em / i11 * y22
330+
sig_m22 = -em / i22 * x11
331+
332+
# update yield indexes
333+
yield_index["mxx"] = max(yield_index["mxx"], abs(sig_mxx / fy))
334+
yield_index["myy"] = max(yield_index["myy"], abs(sig_myy / fy))
335+
yield_index["m11"] = max(yield_index["m11"], abs(sig_m11 / fy))
336+
yield_index["m22"] = max(yield_index["m22"], abs(sig_m22 / fy))
337+
338+
# calculate yield moments
339+
self.section_props.my_xx = 1.0 / yield_index["mxx"]
340+
self.section_props.my_yy = 1.0 / yield_index["myy"]
341+
self.section_props.my_11 = 1.0 / yield_index["m11"]
342+
self.section_props.my_22 = 1.0 / yield_index["m22"]
277343

278344
if progress and task is not None:
279345
msg = "[bold green]:white_check_mark: Geometric analysis complete"

src/sectionproperties/post/post.py

+18
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,15 @@ def print_results(
670670
except RuntimeError:
671671
pass
672672

673+
# print cross-section my
674+
if is_composite:
675+
try:
676+
my_xx, my_yy = section.get_my()
677+
table.add_row("my_xx", f"{my_xx:>{fmt}}")
678+
table.add_row("my_yy-", f"{my_yy:>{fmt}}")
679+
except RuntimeError:
680+
pass
681+
673682
# print cross-section rc
674683
try:
675684
rx, ry = section.get_rc()
@@ -721,6 +730,15 @@ def print_results(
721730
except RuntimeError:
722731
pass
723732

733+
# print cross-section my_p
734+
if is_composite:
735+
try:
736+
my_11, my_22 = section.get_my_p()
737+
table.add_row("my_11", f"{my_11:>{fmt}}")
738+
table.add_row("my_22-", f"{my_22:>{fmt}}")
739+
except RuntimeError:
740+
pass
741+
724742
# print cross-section rp
725743
try:
726744
r11, r22 = section.get_rp()

tests/analysis/test_yield_moment.py

+188-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
"""Tests for the yield moment calculation."""
22

3+
import numpy as np
34
import pytest
45

56
from sectionproperties.analysis import Section
7+
from sectionproperties.post.stress_post import StressPost
68
from sectionproperties.pre import Material
7-
from sectionproperties.pre.library import rectangular_section
9+
from sectionproperties.pre.library import i_section, rectangular_section
810

911
STEEL = Material(
1012
name="Steel",
@@ -18,7 +20,7 @@
1820

1921
def test_get_without_analysis():
2022
"""Test for raising an error if a geometric analysis has not been performed."""
21-
geom = rectangular_section(d=50, b=50)
23+
geom = rectangular_section(d=50, b=50, material=STEEL)
2224
geom.create_mesh(mesh_sizes=0, coarse=True)
2325
sec = Section(geometry=geom)
2426

@@ -52,14 +54,195 @@ def test_rectangle():
5254

5355
my_xx, my_yy = sec.get_my()
5456
my_11, my_22 = sec.get_my()
57+
my_xx_calc = 50 * 100 * 100 / 6 * 500
58+
my_yy_calc = 100 * 50 * 50 / 6 * 500
59+
60+
# compare with theoretical values
61+
assert my_xx == pytest.approx(my_xx_calc)
62+
assert my_yy == pytest.approx(my_yy_calc)
63+
assert my_11 == pytest.approx(my_xx_calc)
64+
assert my_22 == pytest.approx(my_yy_calc)
65+
66+
# compare with stress analysis
67+
stress = sec.calculate_stress(mxx=my_xx, myy=my_yy, m11=my_11, m22=my_22)
68+
assert check_yield_index(stress, "sig_zz_mxx") == pytest.approx(1.0)
69+
assert check_yield_index(stress, "sig_zz_myy") == pytest.approx(1.0)
70+
assert check_yield_index(stress, "sig_zz_m11") == pytest.approx(1.0)
71+
assert check_yield_index(stress, "sig_zz_m22") == pytest.approx(1.0)
72+
73+
74+
def test_rectangle_rotated():
75+
"""Test the yield moment of a simple rotated rectangle."""
76+
geom = rectangular_section(d=100, b=50, material=STEEL)
77+
geom.rotate_section(angle=45.0)
78+
geom.create_mesh(mesh_sizes=0, coarse=True)
79+
sec = Section(geometry=geom)
80+
sec.calculate_geometric_properties()
81+
82+
my_11, my_22 = sec.get_my()
83+
my_xx_calc = 50 * 100 * 100 / 6 * 500
84+
my_yy_calc = 100 * 50 * 50 / 6 * 500
85+
86+
# compare with theoretical values
87+
assert my_11 == pytest.approx(my_xx_calc)
88+
assert my_22 == pytest.approx(my_yy_calc)
89+
90+
# compare with stress analysis
91+
stress = sec.calculate_stress(m11=my_11, m22=my_22)
92+
assert check_yield_index(stress, "sig_zz_m11") == pytest.approx(1.0)
93+
assert check_yield_index(stress, "sig_zz_m22") == pytest.approx(1.0)
94+
95+
96+
def test_isection():
97+
"""Test the yield moment of an isection."""
98+
geom = i_section(d=200, b=100, t_f=10, t_w=5, r=12, n_r=8, material=STEEL)
99+
geom.create_mesh(mesh_sizes=0, coarse=True)
100+
sec = Section(geometry=geom)
101+
sec.calculate_geometric_properties()
102+
103+
my_xx, my_yy = sec.get_my()
104+
my_11, my_22 = sec.get_my()
105+
ezxx, _, ezyy, _ = sec.get_ez()
106+
my_xx_calc = ezxx / 200e3 * 500
107+
my_yy_calc = ezyy / 200e3 * 500
55108

56-
my_xx_calc = 50 * 100 * 100 / 4 * 500
57-
my_yy_calc = 100 * 50 * 50 / 4 * 500
109+
# compare with theoretical values
110+
assert my_xx == pytest.approx(my_xx_calc)
111+
assert my_yy == pytest.approx(my_yy_calc)
112+
assert my_11 == pytest.approx(my_xx_calc)
113+
assert my_22 == pytest.approx(my_yy_calc)
114+
115+
# compare with stress analysis
116+
stress = sec.calculate_stress(mxx=my_xx, myy=my_yy, m11=my_11, m22=my_22)
117+
assert check_yield_index(stress, "sig_zz_mxx") == pytest.approx(1.0)
118+
assert check_yield_index(stress, "sig_zz_myy") == pytest.approx(1.0)
119+
assert check_yield_index(stress, "sig_zz_m11") == pytest.approx(1.0)
120+
assert check_yield_index(stress, "sig_zz_m22") == pytest.approx(1.0)
121+
122+
123+
def test_rectangle_composite():
124+
"""Test the yield moment of a composite rectangular section."""
125+
mat1 = Material("a", 2, 0, 2, 1, "b")
126+
mat2 = Material("b", 1, 0, 1, 1, "r")
58127

128+
rect1 = rectangular_section(d=20, b=20, material=mat1)
129+
rect2 = rectangular_section(d=20, b=20, material=mat2).align_to(rect1, "top")
130+
rect3 = rectangular_section(d=20, b=20, material=mat1).align_to(rect2, "top")
131+
geom = rect1 + rect2 + rect3
132+
geom.create_mesh(mesh_sizes=0, coarse=True)
133+
sec = Section(geom)
134+
sec.calculate_geometric_properties()
135+
136+
my_xx, my_yy = sec.get_my()
137+
my_11, my_22 = sec.get_my()
138+
eixx_calc = (20 * 20**3 / 12) + 2 * 2 * ((20 * 20**3 / 12) + (20 * 20 * 20**2))
139+
eiyy_calc = 5 * (20 * 20**3 / 12)
140+
# myield = f * Ieff / y
141+
my_xx_calc = 2 * (eixx_calc / 2) / 30
142+
my_yy_calc = 1 * eiyy_calc / 10
143+
144+
# compare with theoretical values
59145
assert my_xx == pytest.approx(my_xx_calc)
60146
assert my_yy == pytest.approx(my_yy_calc)
61147
assert my_11 == pytest.approx(my_xx_calc)
62148
assert my_22 == pytest.approx(my_yy_calc)
63149

150+
# compare with stress analysis
151+
stress = sec.calculate_stress(mxx=my_xx, myy=my_yy, m11=my_11, m22=my_22)
152+
assert check_yield_index(stress, "sig_zz_mxx") == pytest.approx(1.0)
153+
assert check_yield_index(stress, "sig_zz_myy") == pytest.approx(1.0)
154+
assert check_yield_index(stress, "sig_zz_m11") == pytest.approx(1.0)
155+
assert check_yield_index(stress, "sig_zz_m22") == pytest.approx(1.0)
156+
157+
158+
def test_composite_example():
159+
"""Tests the composite example from the docs, compares to stress analysis."""
160+
timber = Material(
161+
name="Timber",
162+
elastic_modulus=8e3,
163+
poissons_ratio=0.35,
164+
yield_strength=20,
165+
density=0.78e-6,
166+
color="burlywood",
167+
)
168+
169+
ub = i_section(d=304, b=165, t_f=10.2, t_w=6.1, r=11.4, n_r=8, material=STEEL)
170+
panel = rectangular_section(d=100, b=600, material=timber)
171+
panel = panel.align_center(align_to=ub).align_to(other=ub, on="top")
172+
geom = ub + panel
173+
geom.create_mesh(mesh_sizes=[10, 500])
174+
sec = Section(geometry=geom)
175+
sec.calculate_geometric_properties()
176+
177+
# compare with stress analysis
178+
my_xx, my_yy = sec.get_my()
179+
my_11, my_22 = sec.get_my()
180+
stress = sec.calculate_stress(mxx=my_xx, myy=my_yy, m11=my_11, m22=my_22)
181+
assert check_yield_index(stress, "sig_zz_mxx") == pytest.approx(1.0)
182+
assert check_yield_index(stress, "sig_zz_myy") == pytest.approx(1.0)
183+
assert check_yield_index(stress, "sig_zz_m11") == pytest.approx(1.0)
184+
assert check_yield_index(stress, "sig_zz_m22") == pytest.approx(1.0)
185+
186+
187+
def test_yield_internal():
188+
"""Test where the internal material yields first."""
189+
mat1 = Material(
190+
name="Material_1",
191+
elastic_modulus=0.75,
192+
poissons_ratio=0,
193+
yield_strength=1,
194+
density=1,
195+
color="gold",
196+
)
197+
mat2 = Material(
198+
name="Material_2",
199+
elastic_modulus=1,
200+
poissons_ratio=0,
201+
yield_strength=1,
202+
density=1,
203+
color="blue",
204+
)
205+
mat3 = Material(
206+
name="Material 3",
207+
elastic_modulus=3,
208+
poissons_ratio=0,
209+
yield_strength=1,
210+
density=1,
211+
color="red",
212+
)
213+
214+
sq1 = rectangular_section(d=100, b=100, material=mat1).align_center()
215+
sq2 = rectangular_section(d=75, b=75, material=mat2).align_center()
216+
sq3 = rectangular_section(d=50, b=50, material=mat3).align_center()
217+
hole = rectangular_section(d=25, b=25).align_center()
218+
compound = (sq1 - sq2) + (sq2 - sq3) + (sq3 - hole)
219+
compound.create_mesh(10)
220+
sec = Section(compound)
221+
sec.calculate_geometric_properties()
222+
223+
# compare with stress analysis
224+
my_xx, my_yy = sec.get_my()
225+
my_11, my_22 = sec.get_my()
226+
stress = sec.calculate_stress(mxx=my_xx, myy=my_yy, m11=my_11, m22=my_22)
227+
assert check_yield_index(stress, "sig_zz_mxx") == pytest.approx(1.0)
228+
assert check_yield_index(stress, "sig_zz_myy") == pytest.approx(1.0)
229+
assert check_yield_index(stress, "sig_zz_m11") == pytest.approx(1.0)
230+
assert check_yield_index(stress, "sig_zz_m22") == pytest.approx(1.0)
231+
232+
233+
def check_yield_index(stress: StressPost, key: str) -> float:
234+
"""Returns the largest yield index.
235+
236+
Given the output from StressPost object and a dict key representing the type of
237+
stress, returns the largest yield index.
238+
"""
239+
yield_index = 0.0
240+
241+
for idx, mat_dict in enumerate(stress.get_stress()):
242+
fy = stress.material_groups[idx].material.yield_strength
243+
sigs = mat_dict[key]
244+
sig_max = max(np.absolute(sigs))
245+
246+
yield_index = max(yield_index, sig_max / fy)
64247

65-
# TODO: add more tests
248+
return yield_index

0 commit comments

Comments
 (0)