Skip to content

Commit 0c89f80

Browse files
Merge pull request #462 from robbievanleeuwen/fix/composite-plastic
Fix plastic calculation for composite sections
2 parents cee66ac + b4d991e commit 0c89f80

File tree

3 files changed

+229
-65
lines changed

3 files changed

+229
-65
lines changed

src/sectionproperties/analysis/plastic_section.py

Lines changed: 61 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,14 @@ def __init__(
3535
Args:
3636
geometry: Section geometry object
3737
"""
38+
# move geometry to geometric centroid
3839
self.geometry = geometry.align_center()
3940
self.geometry.compile_geometry()
4041

4142
# initialize variables to be defined later within calculate_plastic_force
42-
self._c_top = [0.0, 0.0]
43-
self._c_bot = [0.0, 0.0]
44-
self._f_top = 0.0
43+
self._f_list: list[float] = []
44+
self._cx_list: list[float] = []
45+
self._cy_list: list[float] = []
4546

4647
def calculate_plastic_properties(
4748
self,
@@ -89,7 +90,15 @@ def calculate_plastic_properties(
8990

9091
self.check_convergence(root_result=r, axis="x-axis")
9192
section.section_props.y_pc = y_pc
92-
section.section_props.sxx = self._f_top * abs(self._c_top[1] - self._c_bot[1])
93+
94+
# calculate plastic section moduli (plastic moment if composite)
95+
section.section_props.sxx = 0.0
96+
97+
# loop through each geometry force & y-centroid
98+
for f, c in zip(self._f_list, self._cy_list):
99+
# calculate distance from y-centroid to plastic centroid
100+
d_y = abs(c - y_pc)
101+
section.section_props.sxx += f * d_y
93102

94103
if verbose:
95104
self.print_verbose(d=y_pc, root_result=r, axis="x-axis")
@@ -107,7 +116,15 @@ def calculate_plastic_properties(
107116

108117
self.check_convergence(root_result=r, axis="y-axis")
109118
section.section_props.x_pc = x_pc
110-
section.section_props.syy = self._f_top * abs(self._c_top[0] - self._c_bot[0])
119+
120+
# calculate plastic section moduli (plastic moment if composite)
121+
section.section_props.syy = 0.0
122+
123+
# loop through each geometry force & x-centroid
124+
for f, c in zip(self._f_list, self._cx_list):
125+
# calculate distance from x-centroid to plastic centroid
126+
d_x = abs(c - x_pc)
127+
section.section_props.syy += f * d_x
111128

112129
if verbose:
113130
self.print_verbose(d=x_pc, root_result=r, axis="y-axis")
@@ -137,17 +154,20 @@ def calculate_plastic_properties(
137154
verbose=verbose,
138155
)
139156

140-
# calculate the centroids in the principal coordinate system
141-
c_top_p = fea.principal_coordinate(
142-
phi=section.section_props.phi, x=self._c_top[0], y=self._c_top[1]
143-
)
144-
c_bot_p = fea.principal_coordinate(
145-
phi=section.section_props.phi, x=self._c_bot[0], y=self._c_bot[1]
146-
)
147-
148157
self.check_convergence(root_result=r, axis="11-axis")
149158
section.section_props.y22_pc = y22_pc
150-
section.section_props.s11 = self._f_top * abs(c_top_p[1] - c_bot_p[1])
159+
160+
# calculate plastic section moduli (plastic moment if composite)
161+
section.section_props.s11 = 0.0
162+
163+
# loop through each geometry force & xy-centroid
164+
for f, cx, cy in zip(self._f_list, self._cx_list, self._cy_list):
165+
# convert centroid to principal coordinates
166+
cen = fea.principal_coordinate(phi=section.section_props.phi, x=cx, y=cy)
167+
168+
# calculate distance from 22-centroid to plastic centroid
169+
d22 = abs(cen[1] - y22_pc)
170+
section.section_props.s11 += f * d22
151171

152172
if verbose:
153173
self.print_verbose(d=y22_pc, root_result=r, axis="11-axis")
@@ -163,17 +183,20 @@ def calculate_plastic_properties(
163183
verbose=verbose,
164184
)
165185

166-
# calculate the centroids in the principal coordinate system
167-
c_top_p = fea.principal_coordinate(
168-
phi=section.section_props.phi, x=self._c_top[0], y=self._c_top[1]
169-
)
170-
c_bot_p = fea.principal_coordinate(
171-
phi=section.section_props.phi, x=self._c_bot[0], y=self._c_bot[1]
172-
)
173-
174186
self.check_convergence(root_result=r, axis="22-axis")
175187
section.section_props.x11_pc = x11_pc
176-
section.section_props.s22 = self._f_top * abs(c_top_p[0] - c_bot_p[0])
188+
189+
# calculate plastic section moduli (plastic moment if composite)
190+
section.section_props.s22 = 0.0
191+
192+
# loop through each geometry force & xy-centroid
193+
for f, cx, cy in zip(self._f_list, self._cx_list, self._cy_list):
194+
# convert centroid to principal coordinates
195+
cen = fea.principal_coordinate(phi=section.section_props.phi, x=cx, y=cy)
196+
197+
# calculate distance from 11-centroid to plastic centroid
198+
d11 = abs(cen[0] - x11_pc)
199+
section.section_props.s22 += f * d11
177200

178201
if verbose:
179202
self.print_verbose(d=x11_pc, root_result=r, axis="22-axis")
@@ -396,13 +419,13 @@ def calculate_plastic_force(
396419
p: Point on the axis line
397420
398421
Returns:
399-
Force in the above and below the axis line (``f_top``, ``f_bot``)
422+
Force in the geometries above and below the axis line (``f_top``, ``f_bot``)
400423
"""
401424
# initialise variables
402425
f_top, f_bot = 0.0, 0.0
403-
a_top, a_bot = 0.0, 0.0
404-
qx_top, qx_bot = 0.0, 0.0
405-
qy_top, qy_bot = 0.0, 0.0
426+
self._f_list = []
427+
self._cx_list = []
428+
self._cy_list = []
406429

407430
# split geometry above and below the line
408431
top_geoms, bot_geoms = self.geometry.split_section(point_i=p, vector=u)
@@ -415,12 +438,14 @@ def calculate_plastic_force(
415438
area_top = top_geom.calculate_area()
416439
cx, cy = top_geom.calculate_centroid()
417440

418-
# sum properties
419-
a_top += area_top
420-
qx_top += cy * area_top
421-
qy_top += cx * area_top
441+
# sum force
422442
f_top += f_y * area_top
423443

444+
# add force and centroids to list
445+
self._f_list.append(f_y * area_top)
446+
self._cx_list.append(cx)
447+
self._cy_list.append(cy)
448+
424449
if bot_geoms:
425450
# loop through all bottom geometries
426451
for bot_geom in bot_geoms:
@@ -429,23 +454,12 @@ def calculate_plastic_force(
429454
area_bot = bot_geom.calculate_area()
430455
cx, cy = bot_geom.calculate_centroid()
431456

432-
# sum properties
433-
a_bot += area_bot
434-
qx_bot += cy * area_bot
435-
qy_bot += cx * area_bot
457+
# sum force
436458
f_bot += f_y * area_bot
437459

438-
# calculate centroid of force action
439-
try:
440-
self._c_top = [qy_top / a_top, qx_top / a_top]
441-
self._f_top = f_top
442-
except ZeroDivisionError:
443-
self._c_top = [0.0, 0.0]
444-
self._f_top = 0.0
445-
446-
try:
447-
self._c_bot = [qy_bot / a_bot, qx_bot / a_bot]
448-
except ZeroDivisionError:
449-
self._c_bot = [0.0, 0.0]
460+
# add force and centroids to list
461+
self._f_list.append(f_y * area_bot)
462+
self._cx_list.append(cx)
463+
self._cy_list.append(cy)
450464

451465
return f_top, f_bot

tests/analysis/test_plastic.py

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def test_plastic_centroid():
9393
# create a Section object
9494
section = Section(geometry=geometry)
9595

96-
# perform a geometric, warping and plastic analysis
96+
# perform a geometric and plastic analysis
9797
section.calculate_geometric_properties()
9898
section.calculate_plastic_properties()
9999

@@ -125,3 +125,152 @@ def test_plastic_centroid():
125125
x_pc, y_pc = nested_sec.get_pc()
126126
assert x_pc == pytest.approx(37.5)
127127
assert y_pc == pytest.approx(50)
128+
129+
130+
def test_plastic_composite_simple():
131+
"""Tests the plastic properties of a simple composite section."""
132+
mat1 = Material(
133+
name="mat1",
134+
elastic_modulus=10,
135+
poissons_ratio=0.3,
136+
density=1,
137+
yield_strength=50,
138+
color="grey",
139+
)
140+
141+
mat2 = Material(
142+
name="mat2",
143+
elastic_modulus=20,
144+
poissons_ratio=0.3,
145+
density=1,
146+
yield_strength=80,
147+
color="black",
148+
)
149+
150+
rect1 = sections.rectangular_section(d=100, b=50, material=mat1)
151+
rect2 = sections.rectangular_section(d=100, b=50, material=mat2).shift_section(
152+
x_offset=50
153+
)
154+
geom = rect1 + rect2
155+
geom.create_mesh(mesh_sizes=100)
156+
157+
sec = Section(geometry=geom)
158+
sec.calculate_geometric_properties()
159+
sec.calculate_plastic_properties()
160+
161+
x_pc, y_pc = sec.get_pc()
162+
assert y_pc == pytest.approx(50)
163+
164+
# calculate x_pc
165+
f_mat1 = 100 * 50 * 50 # force in mat1
166+
x_mat2 = f_mat1 / 80 / 100 # width of mat2 required to equal force in mat1
167+
x_left = (50 - x_mat2) / 2 # width of ma2 to left of pc
168+
assert x_pc == pytest.approx(x_left + 50)
169+
170+
mp_xx, mp_yy = sec.get_mp()
171+
mp_xx_calc = 50 * 100**2 / 4 * (80 + 50)
172+
assert mp_xx == pytest.approx(mp_xx_calc)
173+
174+
# calculate mp_yy
175+
mp_yy_calc = 0
176+
mp_yy_calc += f_mat1 * abs(25 - x_pc) # mat 1 contribution
177+
# left mat 2 contribution
178+
mp_yy_calc += x_left * 100 * 80 * abs(50 + x_left / 2 - x_pc)
179+
# right mat 2 contribution
180+
mp_yy_calc += (50 - x_left) * 100 * 80 * abs(100 - (50 - x_left) / 2 - x_pc)
181+
assert mp_yy == pytest.approx(mp_yy_calc)
182+
183+
# check principal calc with rotation 45 deg
184+
geom.rotate_section(angle=45)
185+
geom.create_mesh(mesh_sizes=100)
186+
sec = Section(geometry=geom)
187+
sec.calculate_geometric_properties()
188+
sec.calculate_plastic_properties()
189+
190+
mp_11, mp_22 = sec.get_mp_p()
191+
assert mp_11 == pytest.approx(mp_xx_calc)
192+
assert mp_22 == pytest.approx(mp_yy_calc)
193+
194+
195+
def test_plastic_composite_example():
196+
"""Tests the composite example on the sectionproperties docs, refer issue #460.
197+
198+
The steel section is simplified to have no root radii to allow hand calculation
199+
comparison.
200+
"""
201+
# create the steel material
202+
steel = Material(
203+
name="Steel",
204+
elastic_modulus=200e3,
205+
poissons_ratio=0.3,
206+
density=7.85e-6,
207+
yield_strength=500,
208+
color="grey",
209+
)
210+
211+
# create the timber material
212+
timber = Material(
213+
name="Timber",
214+
elastic_modulus=8e3,
215+
poissons_ratio=0.35,
216+
yield_strength=20,
217+
density=0.78e-6,
218+
color="burlywood",
219+
)
220+
221+
# universal steel beam
222+
ub = steel_sections.i_section(
223+
d=304, b=165, t_f=10.2, t_w=6.1, r=0, n_r=2, material=steel
224+
)
225+
226+
# timber floor panel
227+
panel = sections.rectangular_section(d=100, b=600, material=timber)
228+
panel = panel.align_center(align_to=ub).align_to(other=ub, on="top")
229+
230+
# combine geometry
231+
geom = ub + panel
232+
233+
geom.create_mesh(mesh_sizes=[50, 500])
234+
sec = Section(geometry=geom)
235+
sec.calculate_geometric_properties()
236+
sec.calculate_plastic_properties()
237+
238+
# get calculated values
239+
x_pc, y_pc = sec.get_pc()
240+
mp_xx, mp_yy = sec.get_mp()
241+
242+
# hand calcs
243+
# 1) x-axis bending
244+
# calculate y_pc -> assume centroid in top flange
245+
f_timber = 600 * 100 * 20
246+
f_web = (304 - 2 * 10.2) * 6.1 * 500
247+
f_flange = 165 * 10.2 * 500
248+
249+
# let y = distance from bottom of top flange to y_pc
250+
# A) bot = 165 * y * 500
251+
# B) top = 165 * (10.2 - y) * 500
252+
# C) (A) - (B) = f_timber - f_web - f_flange = f_diff
253+
# 165 * 500 (2y - 10.2) = f_diff
254+
# y = (f_diff / (165 * 500) + 10.2) / 2
255+
f_diff = f_timber - f_web - f_flange
256+
y = (f_diff / (165 * 500) + 10.2) / 2
257+
assert y_pc == pytest.approx(304 - 10.2 + y)
258+
259+
# calculate mp_xx
260+
mp_xx_calc = 0
261+
mp_xx_calc += f_timber * abs(304 + 50 - y_pc) # timber contribution
262+
mp_xx_calc += (10.2 - y) * 165 * 500 * abs((10.2 - y) / 2) # top flange p1
263+
mp_xx_calc += y * 165 * 500 * abs(y / 2) # top flange p2
264+
mp_xx_calc += f_web * abs(10.2 + (304 - 2 * 10.2) / 2 - y_pc) # web contribution
265+
mp_xx_calc += f_flange * abs(10.2 / 2 - y_pc) # bot flange contribution
266+
assert mp_xx == pytest.approx(mp_xx_calc)
267+
268+
# 2) y-axis bending
269+
assert x_pc == pytest.approx(165 / 2)
270+
271+
# calculate mp_yy
272+
mp_yy_calc = 0
273+
mp_yy_calc += 2 * (10.2 * 165**2 / 4) * 500 # 2 flanges
274+
mp_yy_calc += (304 - 10.2 * 2) * 6.1**2 / 4 * 500 # web
275+
mp_yy_calc += 100 * 600**2 / 4 * 20 # timber
276+
assert mp_yy == pytest.approx(mp_yy_calc)

tests/validation/test_custom_validation.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
# constants
1414
tol = 1e-6
15+
plastic_tol = 1e-5
1516
warp_tol = 1e-3
1617

1718

@@ -122,7 +123,7 @@ def test_custom_section_warping(custom_section):
122123
def test_custom_section_plastic(custom_section):
123124
"""Test custom section plastic properties.
124125
125-
Warping properties calculated from sectionproperties v3.0.2 with a refined mesh
126+
Plastic properties calculated from sectionproperties v3.0.2 with a refined mesh
126127
[mesh_sizes=0.5].
127128
"""
128129
sec = custom_section
@@ -131,19 +132,19 @@ def test_custom_section_plastic(custom_section):
131132
x_pc, y_pc = sec.get_pc()
132133
x11_pc, y22_pc = sec.get_pc_p()
133134

134-
check.almost_equal(x_pc, 4.977273e01, rel=tol)
135-
check.almost_equal(y_pc, 9.172040e01, rel=tol)
136-
check.almost_equal(x11_pc, 5.133714e01, rel=tol)
137-
check.almost_equal(y22_pc, 9.158984e01, rel=tol)
138-
check.almost_equal(sec.section_props.sxx, 1.531971e05, rel=tol)
139-
check.almost_equal(sec.section_props.syy, 1.014943e05, rel=tol)
140-
check.almost_equal(sec.section_props.s11, 1.533463e05, rel=tol)
141-
check.almost_equal(sec.section_props.s22, 1.015010e05, rel=tol)
142-
check.almost_equal(sec.section_props.sf_xx_plus, 8.942884e-01, rel=tol)
143-
check.almost_equal(sec.section_props.sf_xx_minus, 1.292703e00, rel=tol)
144-
check.almost_equal(sec.section_props.sf_yy_plus, 1.602519e00, rel=tol)
145-
check.almost_equal(sec.section_props.sf_yy_minus, 1.567298e00, rel=tol)
146-
check.almost_equal(sec.section_props.sf_11_plus, 9.450478e-01, rel=tol)
147-
check.almost_equal(sec.section_props.sf_11_minus, 1.341988e00, rel=tol)
148-
check.almost_equal(sec.section_props.sf_22_plus, 1.677621e00, rel=tol)
149-
check.almost_equal(sec.section_props.sf_22_minus, 1.619711e00, rel=tol)
135+
check.almost_equal(x_pc, 4.977273e01, rel=plastic_tol)
136+
check.almost_equal(y_pc, 9.172040e01, rel=plastic_tol)
137+
check.almost_equal(x11_pc, 5.133714e01, rel=plastic_tol)
138+
check.almost_equal(y22_pc, 9.158984e01, rel=plastic_tol)
139+
check.almost_equal(sec.section_props.sxx, 1.531971e05, rel=plastic_tol)
140+
check.almost_equal(sec.section_props.syy, 1.014943e05, rel=plastic_tol)
141+
check.almost_equal(sec.section_props.s11, 1.533463e05, rel=plastic_tol)
142+
check.almost_equal(sec.section_props.s22, 1.015010e05, rel=plastic_tol)
143+
check.almost_equal(sec.section_props.sf_xx_plus, 8.942884e-01, rel=plastic_tol)
144+
check.almost_equal(sec.section_props.sf_xx_minus, 1.292703e00, rel=plastic_tol)
145+
check.almost_equal(sec.section_props.sf_yy_plus, 1.602519e00, rel=plastic_tol)
146+
check.almost_equal(sec.section_props.sf_yy_minus, 1.567298e00, rel=plastic_tol)
147+
check.almost_equal(sec.section_props.sf_11_plus, 9.450478e-01, rel=plastic_tol)
148+
check.almost_equal(sec.section_props.sf_11_minus, 1.341988e00, rel=plastic_tol)
149+
check.almost_equal(sec.section_props.sf_22_plus, 1.677621e00, rel=plastic_tol)
150+
check.almost_equal(sec.section_props.sf_22_minus, 1.619711e00, rel=plastic_tol)

0 commit comments

Comments
 (0)