Sections

General

In StructuralCodes, a Section is responsible for connecting a geometry object with an API for performing calculations on the geometry.

The section contains a SectionCalculator which contains the API for doing calculations on the geometry, including for example lower level methods for integrating a strain profile to stress resultants, or calculating a strain profile based on stress resultants, or higher level methods like calculating a moment-curvature relation, or an interaction diagram between moments and axial force.

The SectionCalculator uses a SectionIntegrator to integrate the strain response on a geometry.

Tip

See the theory reference for a guide on the sign convention used in StructuralCodes.

Tip

The theory reference provides an overview of the theory behind the section calculator and the section integrators.

The beam section

The BeamSection takes a SurfaceGeometry or a CompoundGeometry as input, and is capable of calculating the response of an arbitrarily shaped geometry with arbitrary reinforcement layout, subject to stresses in the direction of the beam axis.

In the example below, we continue the example with the T-shaped geometry.

Tip

If you are looking for the gross properties of the section, these are available at the .gross_properties property on the Section object ✅

Since the axial force and the moments are all relative to the origin, we start by translating the geometry such that the centroid is alligned with the origin.

Notice how we can use .calculate_bending_strength() and .calculate_limit_axial_load() to calculate the bending strength and the limit axial loads in tension and compression.

Furthermore, we have the following methods:

.integrate_strain_profile()

Calculate the stress resultants for a given strain profile.

.calculate_strain_profile()

Calculate the strain profile for given stress resultants.

.calculate_moment_curvature()

Calculate the moment-curvature relation.

.calculate_nm_interaction_domain()

Calculate the interaction domain between axial load and bending moment.

.calculate_nmm_interaction_domain()

Calculate the interaction domain between axial load and biaxial bending.

.calculate_mm_interaction_domain()

Calculate the interaction domain between biaxial bending for a given axial load.

See the BeamSectionCalculator for a complete list.

Note

Notice that a call to .calculate_nm_interaction_domain() returns the interaction domain for a negative bending moment, i.e. a bending moment that gives compression at the top of the cross section according to the sign convention. To obtain the interaction domain for the positive bending moment, the neutral axis for the calculation should be rotated an angle \(\theta = \pi\), i.e. the method should be called with the keyword argument theta = np.pi or theta = math.pi. Alternatively, to return the complete domain, the method could be called with the keyword argument complete_domain = True.

Syntax
Create a section and perform calculations.
from structuralcodes.sections import BeamSection

# Create a section
section_not_translated = BeamSection(geometry=t_geom)

# Use the centroid of the section, given in the gross properties, to re-allign
# the geometry with the origin
t_geom = t_geom.translate(dy=-section_not_translated.gross_properties.cz)
section = BeamSection(geometry=t_geom)

# Calculate the bending strength
bending_strength = section.section_calculator.calculate_bending_strength()

# Calculate the limit axial loads
limit_axial_loads = section.section_calculator.calculate_limit_axial_load()

# Calculate the interaction domain between axial force and bending moment
nm = section.section_calculator.calculate_nm_interaction_domain()

# Calculate the moment-curvature relation
moment_curvature = section.section_calculator.calculate_moment_curvature()

Notice how for example .calculate_moment_curvature() returns a custom dataclass of type MomentCurvatureResults. If we inspect this class further, we find among its attributes chi_y and m_y. These are the curvature and the moment about the selected global axis of the section. We also find the curvature and moment about the axis orthogonal to the global axis chi_z and m_z, and the axial strain at the level of the global axis eps_axial. All these attributes are stored as arrays, ready for visualization or further processing.

Tip

Use your favourite plotting library to visualize the results from the BeamSectionCalculator. The code below shows how to plot the moment-curvature relation in the figure below with Matplotlib. Notice how we are plotting the negative values of the curvatures and moments due to the sign convention.

Syntax
Visualize the moment-curvature relation.
import matplotlib.pyplot as plt

# Visualize moment-curvature relation
fig_momcurv, ax_momcurv = plt.subplots()
ax_momcurv.plot(
    -moment_curvature.chi_y * 1e3, -moment_curvature.m_y * 1e-6, '-k'
)
ax_momcurv.grid()
ax_momcurv.set_xlabel(r'$\chi_{\mathrm{y}}$ [1/m]')
ax_momcurv.set_ylabel(r'$M_{\mathrm{y}}$ [kNm]')
ax_momcurv.set_xlim(xmin=0)
ax_momcurv.set_ylim(ymin=0)
../../_images/moment_curvature.png

The moment-curvature relation computed with the code above.

Inspecting further with results objects

The results objects contain methods for inspecting and processing further the detailed results on the section after a calculation is performed.

For instance, calling the method .calculate_bending_strength() returns an object of class UltimateBendingMomentResults.

The object contains the following methods:

.create_detailed_result()

Create an object of class SectionDetailedResultState with the datastructure storing strain and stress fields across the section.

.get_point_strain()

Get the strain at a point.

.get_point_stress()

Get the stress at a point.

In the example below, we continue the example with the T-shaped geometry and we create the detailed_result.

Syntax
Create the detailed result datastructure.
# Calculate the bending strength
bending_strength = section.section_calculator.calculate_bending_strength()
# Create the detailed_results data structure
bending_strength.create_detailed_result(num_points=10000)

If we inspect this result object, we find among its attributes detailed_result that has two useful datastructures: one for surface geometries and one for point geometries. The datastructure for surface geometries contains all points samples from the geometries. These data structures can be easily input for creating a DataFrame object permitting further processing. For instance in the example below, we use pandas plotting abilities to create a visualization of strain and stress fields.

Syntax
Create a plot of strain and stress fields for bending strength.
# Visualize bending strength strain field
df = pd.DataFrame(bending_strength.detailed_result.surface_data)
fig_strain, ax_strain = plt.subplots()
norm = mcolors.CenteredNorm(vcenter=0)
abs_max = df['strain'].abs().max()
df.plot.scatter(
    x='y', y='z', s=5, c='strain', ax=ax_strain, cmap='PiYG', norm=norm
)
fig_strain.tight_layout()

fig_strain.savefig('strain_field.png', dpi=300)

# Visualize bending strength stress field
df_p = pd.DataFrame(bending_strength.detailed_result.point_data)
fig_stress, ax_stress = plt.subplots()
# plot as background all concrete fibers in grey
df.plot.scatter(x='y', y='z', s=5, color='lightgray', zorder=1, ax=ax_stress)
# Plot compressed concrete fibers
# Rename just to have a nicer label. Alternative is plotting manually with
# matplotlib and controlling the colorbar
df.rename(columns={'stress': 'stress_c'}).query('stress_c < 0.0').plot.scatter(
    x='y', y='z', s=5, c='stress_c', cmap='Oranges_r', zorder=2, ax=ax_stress
)
# Plot reinforcement fibers
# Rename just to have a nicer label. Alternative is plotting manually with
# matplotlib and controlling the colorbar
df_p.rename(columns={'stress': 'stress_s'}).plot.scatter(
    x='y',
    y='z',
    s=5,
    c='stress_s',
    cmap='Blues',
    zorder=3,
    ax=ax_stress,
    vmin=0,
)

fig_stress.tight_layout()
fig_stress.savefig('stress_field.png', dpi=300)
../../_images/strain_field.png

The strain field plotted with the code above.

../../_images/stress_field.png

The stress field plotted with the code above.

If we only want to know the strain and/or stress at a specific point we can directly use methods .get_point_strain() or .get_point_stress() without the need of creating the detailed result. For instance with the code below we query the stress at a point for any geometry whose group_label matches the pattern "reinforcement". Note that a pattern can be given with wildcards (like '*' or '?'); for more information refer to .get_point_stress() and .group_filter().

Syntax
Get the stress at the coordinates of one reinforcement
# Get the stress at a specific point in the section
coord_reinf = (
    section.geometry.point_geometries[0].x,
    section.geometry.point_geometries[0].y,
)
stress = bending_strength.get_point_stress(
    *coord_reinf, group_label='reinforcement'
)
print(f'Stress at point {coord_reinf}: {stress:.2f} MPa')

When dealing with analysis that do not involve a single section state, like for instance a Moment-Curvature analysis, the detailed_result object is created for the current_step. The current_step is initialized at 0. Then methods are available for navigating through steps:

.next_step()

Navigate to the next step.

.previous_step()

Navigate to the previous step.

.set_step()

Navigate to a specific step.

If we are not interested in getting the detailed results but we want to get the stresses and/or strains at specific points the same methods .get_point_stress() and .get_point_strain() can be used. In this case the return will be an array of stresses (or strains) for the point through the moment-curvature analysis. For instance with the code below it is possible to plot the stress for one rebar at a given position for the increasing curvature.

Syntax
Plot the stress at reinforcement position for increasing curvature.
# Plot the stress at rebar for increasing curvature
# Get the stress at a specific point in the section
coord_reinf = (
    section.geometry.point_geometries[0].x,
    section.geometry.point_geometries[0].y,
)
stress = moment_curvature.get_point_stress(
    *coord_reinf, group_label='reinforcement'
)

fig_stress_mc, ax_stress_mc = plt.subplots()
ax_stress_mc.plot(
    -moment_curvature.chi_y,
    stress,
    '-k',
    label=f'Stress at point ({coord_reinf[0]:.0f}, {coord_reinf[1]:.0f}),',
)
ax_stress_mc.set_xlabel(r'$\chi_y$')
ax_stress_mc.set_ylabel('Stress [MPa]')
ax_stress_mc.grid(True)
ax_stress_mc.legend()
ax_stress_mc.set_xlim(xmin=0)
ax_stress_mc.set_ylim(ymin=0)
fig_stress_mc.tight_layout()
fig_stress_mc.savefig('stress_at_rebar.png', dpi=300)
../../_images/stress_at_rebar.png

The stress for increasing curvature plotted with the code above.

Using in a more advanced way matplotlib, it is possible to create an animation (like for instance a gif file) showing the stress field for the steps of the moment-curvature analysis. The code below create an animation with the moment curvature diagram on the left and the cross section stress field on the right.

Syntax
Generate an animation for the moment-curvature section response.
# Create a gif animation of stress field
chi = -moment_curvature.chi_y
mom = -moment_curvature.m_y * 1e-6  # kNm
n_steps = len(chi)

# Precompute axis limits and color limits to avoid jumping during animation
# Section limits from one representative detailed result
moment_curvature.set_step(0)
df0 = pd.DataFrame(moment_curvature.detailed_result.surface_data)
df0_p = pd.DataFrame(moment_curvature.detailed_result.point_data)
ymin = df0['y'].min()
ymax = df0['y'].max()
zmin = df0['z'].min()
zmax = df0['z'].max()
dy = ymax - ymin
dz = zmax - zmin
pad_y = 0.05 * dy if dy > 0 else 1.0
pad_z = 0.05 * dz if dz > 0 else 1.0
# Precompute stress ranges over all steps so the colormap stays fixed
concrete_min = 0.0
steel_max = 0.0

for i in range(n_steps):
    moment_curvature.set_step(i)
    c_min = np.asarray(
        moment_curvature.detailed_result.surface_data['stress']
    ).min()
    concrete_min = min(concrete_min, c_min)
    s_max = np.asarray(
        moment_curvature.detailed_result.point_data['stress']
    ).max()
    steel_max = max(steel_max, s_max)

# Create figure and fixed artists
fig, (ax_mc, ax_sec) = plt.subplots(1, 2, figsize=(14, 6))
# ---- Left: moment-curvature curve
ax_mc.plot(chi, mom, '-k', lw=1.5)
(point_mc,) = ax_mc.plot([], [], 'or', ms=6)

ax_mc.set_xlabel(r'$\chi_y$')
ax_mc.set_ylabel(r'$M_y$ [kNm]')
ax_mc.grid(True)
ax_mc.set_xlim(xmin=0)
ax_mc.set_ylim(ymin=0)

title_mc = ax_mc.set_title('Moment-curvature response')

# ---- Right: section stress field
ax_sec.set_xlabel('y')
ax_sec.set_ylabel('z')
ax_sec.set_aspect('equal')
ax_sec.grid(True)
ax_sec.set_xlim(ymin - pad_y, ymax + pad_y)
ax_sec.set_ylim(zmin - pad_z, zmax + pad_z)

title_sec = ax_sec.set_title('Stress distribution')

# Create empty scatter artists once
ax_sec.scatter(df0['y'], df0['z'], s=4, c='lightgray', zorder=1)
sc_conc = ax_sec.scatter(
    [], [], s=5, c=[], cmap='Oranges_r', zorder=2, vmin=concrete_min, vmax=0.0
)
sc_steel = ax_sec.scatter(
    [], [], s=20, c=[], cmap='Blues', zorder=3, vmin=0.0, vmax=steel_max
)

# Optional colorbars
cbar_conc = fig.colorbar(sc_conc, ax=ax_sec)
cbar_conc.set_label('Concrete stress [MPa]')

cbar_steel = fig.colorbar(sc_steel, ax=ax_sec)
cbar_steel.set_label('Reinforcement stress [MPa]')


# Update function for animation
def update(frame):
    # Update analysis step
    moment_curvature.set_step(frame)
    dr = moment_curvature.detailed_result

    # ---- Left panel: moving point on M-k curve
    point_mc.set_data([chi[frame]], [mom[frame]])
    title_mc.set_text(
        f'Moment-curvature response - step {frame + 1}/{n_steps}'
    )

    # ---- Right panel: section plot
    # Compressed concrete only
    conc_offsets = np.column_stack(
        [np.asarray(dr.surface_data['y']), np.asarray(dr.surface_data['z'])]
    )
    conc_colors = np.asarray(dr.surface_data['stress'])
    conc_offsets = conc_offsets[conc_colors < 0, :]
    conc_colors = conc_colors[conc_colors < 0]

    sc_conc.set_offsets(conc_offsets)
    sc_conc.set_array(conc_colors)

    # Reinforcement fibers
    steel_offsets = np.column_stack(
        [np.asarray(dr.point_data['y']), np.asarray(dr.point_data['z'])]
    )
    steel_colors = np.asarray(dr.point_data['stress'])

    sc_steel.set_offsets(steel_offsets)
    sc_steel.set_array(steel_colors)

    title_sec.set_text(
        f'Stress distribution\n$\\chi_y$={chi[frame]:.4e},  $M_y$={mom[frame]:.2f} kNm'
    )

    return point_mc, sc_conc, sc_steel, title_mc, title_sec


# Build and save animation
fig.tight_layout()

anim = FuncAnimation(
    fig,
    update,
    frames=n_steps,
    interval=150,  # milliseconds between frames
    blit=False,  # safer with scatter + colorbars
    repeat=True,
)

anim.save(
    'moment_curvature_stress_animation.gif',
    writer=PillowWriter(fps=8),
    dpi=150,
)

../../_images/moment_curvature_stress_animation.gif

The animation of the stress field with increasing curvature created with the code above.