Source code for civilpy.structural.shear_flow

"""Transverse shear stress distribution tau = VQ/(I*b) over a built-up
section of stacked rectangles, with the classic distribution plot.

Examples
--------
>>> sec = ShearSection([Plate(width=12, height=2, y_bottom=0),
...                     Plate(width=1, height=20, y_bottom=2),
...                     Plate(width=12, height=2, y_bottom=22)])
>>> round(sec.y_bar, 2)
12.0
>>> tau_max = sec.tau(50, sec.y_bar)   # web stress at the NA
>>> tau_web_top = sec.tau(50, 21.99)
>>> tau_max > tau_web_top
True
"""

#  CivilPy
#  Copyright (C) 2019-2026 Dane Parks
#  SPDX-License-Identifier: MIT

from collections import namedtuple

import matplotlib.pyplot as plt
import numpy as np


[docs] class Plate(namedtuple("Plate", "width, height, y_bottom")): """A rectangle in the cross-section: ``width`` x ``height`` with its bottom edge at ``y_bottom`` (in)."""
[docs] class ShearSection: """Stacked-rectangle section for VQ/(I*b) shear stress.""" def __init__(self, plates: list[Plate]): self.plates = [Plate(*p) for p in plates] self.area = sum(p.width * p.height for p in self.plates) self.y_bar = sum( p.width * p.height * (p.y_bottom + p.height / 2.0) for p in self.plates ) / self.area self.i_x = sum( p.width * p.height**3 / 12.0 + p.width * p.height * (p.y_bottom + p.height / 2.0 - self.y_bar) ** 2 for p in self.plates )
[docs] def width_at(self, y: float) -> float: """Total section width crossing elevation ``y``.""" return sum(p.width for p in self.plates if p.y_bottom < y < p.y_bottom + p.height)
[docs] def q_at(self, y: float) -> float: """First moment of the area above ``y`` about the neutral axis.""" q = 0.0 for p in self.plates: top = p.y_bottom + p.height lo = max(p.y_bottom, y) if lo >= top: continue a = p.width * (top - lo) q += a * ((top + lo) / 2.0 - self.y_bar) return q
[docs] def tau(self, v: float, y: float) -> float: """Shear stress at elevation ``y`` for shear force ``v`` (ksi for kips and inches).""" b = self.width_at(y) return v * self.q_at(y) / (self.i_x * b) if b > 0.0 else 0.0
[docs] def plot(self, v: float, ax=None, n: int = 400): """Plot tau(y) beside the section outline; flange/web jumps from the width change show as the textbook discontinuities.""" if ax is None: fig, axes = plt.subplots(1, 2, figsize=(9, 5), sharey=True) ax_sec, ax_tau = axes else: ax_sec, ax_tau = None, ax y_lo = min(p.y_bottom for p in self.plates) y_hi = max(p.y_bottom + p.height for p in self.plates) ys = np.linspace(y_lo + 1e-9, y_hi - 1e-9, n) taus = np.array([self.tau(v, y) for y in ys]) if ax_sec is not None: for p in self.plates: ax_sec.add_patch(plt.Rectangle( (-p.width / 2.0, p.y_bottom), p.width, p.height, facecolor="0.85", edgecolor="k")) ax_sec.axhline(self.y_bar, color="r", ls="--", lw=0.9) ax_sec.annotate("N.A.", (0, self.y_bar), textcoords="offset points", xytext=(4, 3), color="r") ax_sec.set_xlim(-max(p.width for p in self.plates) * 0.75, max(p.width for p in self.plates) * 0.75) ax_sec.set_aspect("equal", adjustable="datalim") ax_sec.set_title("Section") ax_sec.set_ylabel("y (in)") ax_tau.plot(taus, ys, "b", lw=1.5) ax_tau.fill_betweenx(ys, taus, 0.0, color="b", alpha=0.15) i_peak = int(np.argmax(taus)) ax_tau.annotate(rf"$\tau_{{max}}$ = {taus[i_peak]:.4g} ksi", (taus[i_peak], ys[i_peak]), textcoords="offset points", xytext=(6, 0)) ax_tau.axhline(self.y_bar, color="r", ls="--", lw=0.9) ax_tau.set_xlabel(r"$\tau = VQ/Ib$ (ksi)") ax_tau.set_title(f"Shear stress, V = {v:g} kips") ax_tau.grid(True, alpha=0.3) return ax_tau.get_figure()