# CivilPy
# Copyright (C) 2019-2026 Dane Parks
#
# SPDX-License-Identifier: MIT
# See the LICENSE file in the project root for full license text.
"""Substructure geometry placement: the executed design becomes the model.
:func:`substructure_from_layout` closes the loop the substructure design
notebook opens. The superstructure hands its factored reactions to
:func:`~civilpy.structural.stm_topology.design.optimize_pier_cap` (pier and
abutment caps), :class:`~civilpy.structural.pier.MultiColumnBent` (columns),
and :class:`~civilpy.structural.abutment.RetainingWall` (wingwalls); this
module reads the dimensions **out of those design objects** — never free
parameters — and places them under the bridge in the layout's coordinate
frame, mirroring how :class:`~civilpy.structural.bridge_layout.BridgeInput`
drives the superstructure.
Placement conventions (all feet, the hub frame: X = stations along the
centerline, Y transverse with girder 1 at y = 0, Z = 0 at top of deck at
the crown):
* Each support line runs along the skew: plan direction
``u = (sin(skew), cos(skew))``, so a cap's local coordinate ``s`` is the
distance *along the cap* with ``s = 0`` at girder 1 — the same frame the
``load_xs`` / ``column_xs`` fed to ``optimize_pier_cap`` are measured in.
* The cap top is a level plane set one minimum seat below the lowest
bearing-stack bottom on that support line; each girder then gets a
**beam seat** block making up its own stack height, so the seats step
across the width following the deck cross slope.
* The cap is centered on the girder group: its length comes from the
design (``PierCapDesign.span`` — girders plus the sweep's edge
distance), so the start offset is recovered as
``(width_along_cap - span) / 2`` without re-entering the edge parameter.
Everything here is a plain geometry record; :mod:`civilpy.structural
.rhino_bim` turns it into tagged emit objects on the ``Substructure::*``
layers.
"""
from __future__ import annotations
import math
from dataclasses import dataclass, replace
from civilpy.structural.substructure import SubstructureUnit, substructure_units
from typing import Literal
Point = tuple[float, float, float]
#: Bearing-stack height (in) under a girder bottom flange — load plate plus
#: elastomeric pad, matching the ``rhino_bim`` hardware defaults
#: (1.5 in plate + 5 x 0.6 in plies).
DEFAULT_BEARING_STACK_IN = 4.5
SEAT_MIN_IN = 3.0 #: minimum beam-seat (pedestal) height
SEAT_SIDE_IN = 27.0 #: seat plan side: 21 in load plate + 3 in edges
PILE_EMBED_IN = 12.0 #: pile head embedment into a capped-pile cap
# ── geometry records ──────────────────────────────────────────────────────
[docs]
@dataclass(frozen=True)
class BeamSeat:
"""One stepped bearing seat: a square pedestal from the cap top up to
the bottom of that girder's bearing stack."""
girder_line: int
center: Point # plan center at the SEAT TOP (= pad bottom)
side_in: float
height_in: float
[docs]
@dataclass(frozen=True)
class CapBeam:
"""A cap beam along a (possibly skewed) support line. ``origin`` is
the cap-top centerline point at ``s = s0``; ``axis`` the unit vector
along the cap. ``tie_bar_*`` carry the governing STM tie's bar
schedule for the rebar emit and ``tie_z_frac`` its height in the cap
(0 = soffit, 1 = top — a hammerhead's governing tie is the top
chord); None when no design was attached.
``soffit_profile`` makes the cap non-prismatic: ``(s_rel, depth_ft)``
breakpoints from the cap start, linearly interpolated — the top stays
level and the soffit steps/tapers (hammerhead cantilevers). ``None``
is the constant-depth cap."""
origin: Point
axis: Point
length_ft: float
width_ft: float
depth_ft: float
tie_bar_size: int | None = None
tie_bar_count: int | None = None
tie_z_frac: float | None = None
soffit_profile: tuple[tuple[float, float], ...] | None = None
@property
def volume_cy(self) -> float:
if self.soffit_profile is None:
return self.length_ft * self.width_ft * self.depth_ft / 27.0
area = 0.0
pts = self.soffit_profile
for (s0, d0), (s1, d1) in zip(pts, pts[1:]):
area += (d0 + d1) / 2.0 * (s1 - s0)
return area * self.width_ft / 27.0
[docs]
@dataclass(frozen=True)
class ColumnGeometry:
"""One pier column, cap soffit to footing top. Circular when
``diameter_in`` is set, else rectangular ``b_in x h_in`` (``b`` along
the cap axis)."""
center: tuple[float, float] # plan (x, y)
z_top: float
z_bot: float
diameter_in: float | None = None
b_in: float | None = None
h_in: float | None = None
bars_area_in2: float = 0.0 # longitudinal steel from the design
@property
def height_ft(self) -> float:
return self.z_top - self.z_bot
@property
def volume_cy(self) -> float:
if self.diameter_in is not None:
area_sf = math.pi * (self.diameter_in / 12.0) ** 2 / 4.0
else:
area_sf = (self.b_in / 12.0) * (self.h_in / 12.0)
return area_sf * self.height_ft / 27.0
[docs]
@dataclass(frozen=True)
class PileGeometry:
"""One driven HP pile. ``head`` is the butt at the embedment plane
inside the cap; the pay length runs below the cutoff."""
head: Point
shape: str
length_ft: float
[docs]
@dataclass(frozen=True)
class WallPanel:
"""A rectangular wall run (backwall or wingwall stem/footing).
``origin`` is the bottom-centerline start point; the panel extends
``length_ft`` along ``axis``, ``thickness_ft`` centered on the line,
``height_ft`` up."""
origin: Point
axis: Point
length_ft: float
thickness_ft: float
height_ft: float
@property
def volume_cy(self) -> float:
return self.length_ft * self.thickness_ft * self.height_ft / 27.0
[docs]
@dataclass(frozen=True)
class PierGeometry:
"""One pier: a multi-column bent carries ``columns`` (+ optional
``footings``); a capped-pile bent carries ``piles`` instead."""
unit: SubstructureUnit
cap: CapBeam
seats: tuple[BeamSeat, ...]
columns: tuple[ColumnGeometry, ...] = ()
footings: tuple[FootingGeometry, ...] = ()
piles: tuple[PileGeometry, ...] = ()
[docs]
@dataclass(frozen=True)
class AbutmentGeometry:
"""One abutment. ``kind`` is ``"seat"`` (bearings on a stepped-seat
cap), ``"semi-integral"`` (seat cap plus an end diaphragm that moves
with the superstructure), or ``"integral"`` (a full-height end
diaphragm on a single pile row — no bearings, so ``seats`` is
empty)."""
unit: SubstructureUnit
cap: CapBeam
seats: tuple[BeamSeat, ...]
piles: tuple[PileGeometry, ...]
backwall: WallPanel | None = None
wingwalls: tuple[WallPanel, ...] = ()
#: Abutment configuration this geometry represents.
kind: Literal["seat", "semi-integral", "integral"] = "seat"
diaphragm: WallPanel | None = None
[docs]
@dataclass(frozen=True)
class SubstructureLayout:
"""Every substructure unit of one bridge, placed under its layout."""
layout: object # BridgeLayout
abutments: tuple[AbutmentGeometry, ...]
piers: tuple[PierGeometry, ...]
@property
def units(self) -> tuple:
return tuple(sorted((*self.abutments, *self.piers),
key=lambda g: g.unit.index))
# ── caller-supplied specs (only what no design object carries) ────────────
[docs]
@dataclass(frozen=True)
class AbutmentSpec:
"""Capped-pile abutment parameters that live outside the cap design:
the pile layout the cap STM was solved on (``pile_xs_ft`` in the same
girder-1-origin frame as its ``column_xs``), the driven length from
the geotech recommendation, and the wingwall design.
``wingwall`` is the executed :class:`~civilpy.structural.abutment
.RetainingWall` whose stem/footing dimensions the wingwall panels are
read from; ``wingwall_length_ft`` its run along the roadway."""
pile_xs_ft: tuple[float, ...]
#: AISC HP label from :mod:`civilpy.structural.steel`, e.g.
#: ``"HP10X42"``.
pile_shape: str = "HP10X42"
pile_length_ft: float = 40.0
backwall_thickness_in: float = 18.0
wingwall: object | None = None # RetainingWall
wingwall_length_ft: float = 0.0
# ── placement ─────────────────────────────────────────────────────────────
def _support_frame(layout, station_ft: float):
"""Plan frame of a support line: point-at-s and the unit axis."""
skew = math.radians(layout.inputs.skew_deg)
u = (math.sin(skew), math.cos(skew), 0.0)
def at(s: float, z: float) -> Point:
return (station_ft + s * u[0], s * u[1], z)
return at, u
def _seat_plane(layout, station_index: int, *, bearing_stack_in: float,
seat_min_in: float, seat_side_in: float):
"""Cap-top elevation and the stepped seats for one support line."""
pads = [(bp.line_no, bp.location) for bp in layout.bearings
if bp.station_index == station_index]
if not pads:
raise ValueError(f"no bearings at support index {station_index}")
stack_ft = bearing_stack_in / 12.0
bottoms = {line: (loc[0], loc[1], loc[2] - stack_ft)
for line, loc in pads}
cap_top = min(z for _, _, z in bottoms.values()) - seat_min_in / 12.0
seats = tuple(BeamSeat(girder_line=line, center=pt, side_in=seat_side_in,
height_in=(pt[2] - cap_top) * 12.0)
for line, pt in sorted(bottoms.items()))
return cap_top, seats
def _governing_tie(cap_design):
"""Bar schedule of the highest-force tie in a solved cap design, plus
its height fraction in the cap (from the STM node elevations) so the
rebar emit knows whether the main steel is a bottom or top chord."""
report = cap_design.report
if report is None or not report.ties:
return None, None, None
t = max(report.ties, key=lambda t: t.force)
frac = None
model = getattr(cap_design, "model", None)
member = getattr(t, "member", None)
if model is not None and member is not None:
try:
ys = [model.nodes[node][1] for node in member]
frac = (sum(ys) / len(ys)) / cap_design.optimal.depth
except (KeyError, TypeError, AttributeError, ZeroDivisionError):
frac = None
return t.bar_size, t.bar_count, frac
def _cap_from_design(layout, station_ft: float, cap_top: float, cap_design,
soffit_profile=None):
"""Center the designed cap on the girder group along the support
line and hang its depth from the seat plane."""
if cap_design.optimal is None:
raise ValueError("cap design has no feasible depth; nothing to place")
inp = layout.inputs
cos_skew = math.cos(math.radians(inp.skew_deg))
width_along_cap = (inp.girder_count - 1) * inp.girder_spacing_ft / cos_skew
s0 = (width_along_cap - cap_design.span) / 2.0
at, u = _support_frame(layout, station_ft)
bar_size, bar_count, frac = _governing_tie(cap_design)
return CapBeam(origin=at(s0, cap_top), axis=u,
length_ft=cap_design.span,
width_ft=cap_design.thickness,
depth_ft=cap_design.optimal.depth,
tie_bar_size=bar_size, tie_bar_count=bar_count,
tie_z_frac=frac, soffit_profile=soffit_profile), s0
def _piles_along_cap(at, z_cap_bot: float, xs, shape: str, length_ft: float,
embed_in: float) -> tuple[PileGeometry, ...]:
"""Driven piles at ``xs`` (girder-frame ft along the cap), heads
embedded ``embed_in`` into the cap."""
return tuple(
PileGeometry(head=at(s, z_cap_bot + embed_in / 12.0),
shape=shape, length_ft=length_ft)
for s in xs)
[docs]
def pier_geometry(layout, unit: SubstructureUnit, cap_design, bent, *,
footing: FootingSpec | None = None,
bearing_stack_in: float = DEFAULT_BEARING_STACK_IN,
seat_min_in: float = SEAT_MIN_IN,
seat_side_in: float = SEAT_SIDE_IN) -> PierGeometry:
"""Place one pier from its executed designs: the cap from
``cap_design`` (:class:`~civilpy.structural.stm_topology.design
.PierCapDesign`), the columns from ``bent``
(:class:`~civilpy.structural.pier.MultiColumnBent`, whose
``cap.column_positions`` are inches from the left end of the cap)."""
cap_top, seats = _seat_plane(layout, unit.index,
bearing_stack_in=bearing_stack_in,
seat_min_in=seat_min_in,
seat_side_in=seat_side_in)
cap, s0 = _cap_from_design(layout, unit.station_ft, cap_top, cap_design)
at, _ = _support_frame(layout, unit.station_ft)
z_cap_bot = cap_top - cap.depth_ft
columns, footings = [], []
for pos_in, col in zip(bent.cap.column_positions, bent.columns):
s = s0 + pos_in / 12.0
x, y, _ = at(s, z_cap_bot)
z_bot = z_cap_bot - col.height / 12.0
columns.append(ColumnGeometry(
center=(x, y), z_top=z_cap_bot, z_bot=z_bot,
diameter_in=col.diameter, b_in=col.b, h_in=col.h,
bars_area_in2=sum(l.area for l in col.layers)))
if footing is not None:
footings.append(FootingGeometry(
center=(x, y), z_top=z_bot, length_ft=footing.length_ft,
width_ft=footing.width_ft,
thickness_ft=footing.thickness_ft, axis=cap.axis))
return PierGeometry(unit=unit, cap=cap, seats=seats,
columns=tuple(columns), footings=tuple(footings))
[docs]
def pile_bent_geometry(layout, unit: SubstructureUnit, cap_design,
pile_xs_ft, *, pile_shape: str = "HP12X53",
pile_length_ft: float = 40.0,
bearing_stack_in: float = DEFAULT_BEARING_STACK_IN,
seat_min_in: float = SEAT_MIN_IN,
seat_side_in: float = SEAT_SIDE_IN,
pile_embed_in: float = PILE_EMBED_IN
) -> PierGeometry:
"""Place one capped-pile pier (pile bent): the cap from ``cap_design``
(an :func:`optimize_pier_cap` run with the piles as supports, same as
the abutment cap) directly on driven piles at ``pile_xs_ft`` — the
CPP-1-08 pattern generalized off the continuous-slab sheet, whose
``HP12X53`` default the pile shape keeps
(:mod:`civilpy.structural.odot.capped_pile_pier` carries the SCD's
own limits for the standard-drawing case)."""
cap_top, seats = _seat_plane(layout, unit.index,
bearing_stack_in=bearing_stack_in,
seat_min_in=seat_min_in,
seat_side_in=seat_side_in)
cap, s0 = _cap_from_design(layout, unit.station_ft, cap_top, cap_design)
at, _ = _support_frame(layout, unit.station_ft)
piles = _piles_along_cap(at, cap_top - cap.depth_ft, pile_xs_ft,
pile_shape, pile_length_ft, pile_embed_in)
return PierGeometry(unit=unit, cap=cap, seats=seats, piles=piles)
[docs]
def hammerhead_geometry(layout, unit: SubstructureUnit, cap_design,
column, *, tip_depth_ft: float | None = None,
footing: FootingSpec | None = None,
bearing_stack_in: float = DEFAULT_BEARING_STACK_IN,
seat_min_in: float = SEAT_MIN_IN,
seat_side_in: float = SEAT_SIDE_IN
) -> PierGeometry:
"""Place one hammerhead pier: the cap from ``cap_design`` (an
:func:`optimize_pier_cap` run with a **single** column support — the
cantilever D-region is exactly what the STM checks, and its governing
tie lands in the top chord, which the rebar emit follows), the stem
from ``column`` (a :class:`~civilpy.structural.pier.PierColumn` for
section/height/steel).
``tip_depth_ft`` tapers the soffit linearly from the full design
depth at the column faces to this depth at the cantilever tips
(``None`` keeps the cap prismatic). The STM was solved on the full-
depth rectangle, so the tie schedule carries over; the taper is the
conventional weight/formwork refinement outside the nodal zones."""
cap_top, seats = _seat_plane(layout, unit.index,
bearing_stack_in=bearing_stack_in,
seat_min_in=seat_min_in,
seat_side_in=seat_side_in)
inp = layout.inputs
cos_skew = math.cos(math.radians(inp.skew_deg))
s_col = (inp.girder_count - 1) * inp.girder_spacing_ft / cos_skew / 2.0
profile = None
col_b_ft = (column.diameter if column.diameter is not None
else column.b) / 12.0
if tip_depth_ft is not None:
depth = cap_design.optimal.depth
if not 0.0 < tip_depth_ft <= depth:
raise ValueError("tip_depth_ft must be in (0, cap depth]")
# breakpoints in cap-start coordinates; the builder recenters,
# so the column sits at span/2
half = cap_design.span / 2.0
profile = ((0.0, tip_depth_ft),
(half - col_b_ft / 2.0, depth),
(half + col_b_ft / 2.0, depth),
(cap_design.span, tip_depth_ft))
cap, s0 = _cap_from_design(layout, unit.station_ft, cap_top, cap_design,
soffit_profile=profile)
at, _ = _support_frame(layout, unit.station_ft)
x, y, _ = at(s_col, 0.0)
z_col_top = cap_top - cap.depth_ft
z_col_bot = z_col_top - column.height / 12.0
col = ColumnGeometry(center=(x, y), z_top=z_col_top, z_bot=z_col_bot,
diameter_in=column.diameter, b_in=column.b,
h_in=column.h,
bars_area_in2=sum(l.area for l in column.layers))
footings = ()
if footing is not None:
footings = (FootingGeometry(
center=(x, y), z_top=z_col_bot, length_ft=footing.length_ft,
width_ft=footing.width_ft, thickness_ft=footing.thickness_ft,
axis=cap.axis),)
return PierGeometry(unit=unit, cap=cap, seats=seats, columns=(col,),
footings=footings)
[docs]
def abutment_geometry(layout, unit: SubstructureUnit, cap_design,
spec: AbutmentSpec, *,
bearing_stack_in: float = DEFAULT_BEARING_STACK_IN,
seat_min_in: float = SEAT_MIN_IN,
seat_side_in: float = SEAT_SIDE_IN,
pile_embed_in: float = PILE_EMBED_IN
) -> AbutmentGeometry:
"""Place one capped-pile abutment: the cap from its ``cap_design``
(an :func:`optimize_pier_cap` run with the piles as supports), the
piles from ``spec``, the backwall from the cap top to the low deck
edge, and wingwall stem+footing panels from the executed
:class:`~civilpy.structural.abutment.RetainingWall`."""
cap_top, seats = _seat_plane(layout, unit.index,
bearing_stack_in=bearing_stack_in,
seat_min_in=seat_min_in,
seat_side_in=seat_side_in)
cap, s0 = _cap_from_design(layout, unit.station_ft, cap_top, cap_design)
at, u = _support_frame(layout, unit.station_ft)
z_cap_bot = cap_top - cap.depth_ft
piles = _piles_along_cap(at, z_cap_bot, spec.pile_xs_ft,
spec.pile_shape, spec.pile_length_ft,
pile_embed_in)
# backwall on the approach side of the cap, up to the low deck edge
# (crown-following top is a later refinement)
inp = layout.inputs
back = -1.0 if unit.index == 0 else 1.0 # away from the spans
y_edges = (-inp.overhang_ft,
(inp.girder_count - 1) * inp.girder_spacing_ft
+ inp.overhang_ft)
z_bw_top = min(layout.deck_top_z(y) for y in y_edges)
bw_t = spec.backwall_thickness_in / 12.0
bw_shift = back * (cap.width_ft - bw_t) / 2.0
ox, oy, _ = cap.origin
backwall = WallPanel(
origin=(ox + bw_shift, oy, cap_top), axis=u,
length_ft=cap.length_ft, thickness_ft=bw_t,
height_ft=z_bw_top - cap_top)
wingwalls: list[WallPanel] = []
if spec.wingwall is not None and spec.wingwall_length_ft > 0.0:
wall = spec.wingwall
w_axis = (back, 0.0, 0.0) # along the roadway
z_stem_top = z_bw_top
z_stem_bot = z_stem_top - wall.stem_height
for s_end in (s0, s0 + cap.length_ft):
x, y, _ = at(s_end, 0.0)
wingwalls.append(WallPanel( # stem
origin=(x, y, z_stem_bot), axis=w_axis,
length_ft=spec.wingwall_length_ft,
thickness_ft=wall.stem_thickness,
height_ft=wall.stem_height))
wingwalls.append(WallPanel( # footing
origin=(x, y, z_stem_bot - wall.footing_thickness),
axis=w_axis, length_ft=spec.wingwall_length_ft,
thickness_ft=wall.base_width,
height_ft=wall.footing_thickness))
return AbutmentGeometry(unit=unit, cap=cap, seats=seats, piles=piles,
backwall=backwall, wingwalls=tuple(wingwalls))
def _deck_top_high(layout) -> float:
"""Highest top-of-deck elevation across the width (the crown when it
falls inside the deck) — the end-diaphragm top."""
inp = layout.inputs
y_lo = -inp.overhang_ft
y_hi = (inp.girder_count - 1) * inp.girder_spacing_ft + inp.overhang_ft
ys = [y_lo, y_hi]
if y_lo < layout.crown_y_ft < y_hi:
ys.append(layout.crown_y_ft)
return max(layout.deck_top_z(y) for y in ys)
[docs]
def semi_integral_abutment_geometry(layout, unit: SubstructureUnit,
cap_design, spec: AbutmentSpec, *,
diaphragm_thickness_in: float = 30.0,
bearing_stack_in: float =
DEFAULT_BEARING_STACK_IN,
seat_min_in: float = SEAT_MIN_IN,
seat_side_in: float = SEAT_SIDE_IN,
pile_embed_in: float = PILE_EMBED_IN
) -> AbutmentGeometry:
"""Place one semi-integral abutment: the seat abutment (cap on piles,
stepped seats, bearings) with the backwall replaced by an **end
diaphragm** that encases the girder ends and moves with the
superstructure. The diaphragm runs the cap length, offset *inward*
(toward the span) so its back face sits over the cap's back edge,
from the bearing plane up to the high deck edge (drawn level; the
crown-following top is the same refinement flagged for backwalls)."""
geom = abutment_geometry(layout, unit, cap_design, spec,
bearing_stack_in=bearing_stack_in,
seat_min_in=seat_min_in,
seat_side_in=seat_side_in,
pile_embed_in=pile_embed_in)
cap = geom.cap
inward = 1.0 if unit.index == 0 else -1.0
t = diaphragm_thickness_in / 12.0
z_base = cap.origin[2] + seat_min_in / 12.0 # lowest pad bottom
ox, oy, _ = cap.origin
at, u = _support_frame(layout, unit.station_ft)
shift = inward * (t / 2.0 - cap.width_ft / 2.0)
diaphragm = WallPanel(
origin=(ox + shift, oy, z_base), axis=u,
length_ft=cap.length_ft, thickness_ft=t,
height_ft=_deck_top_high(layout) - z_base)
return replace(geom, kind="semi-integral", backwall=None,
diaphragm=diaphragm)
# ── per-unit type specs (mix substructure types on one bridge) ────────────
[docs]
@dataclass(frozen=True)
class BentPierSpec:
"""Multi-column bent: cap from ``cap_design``, columns from ``bent``
(see :func:`pier_geometry`)."""
cap_design: object
bent: object
footing: FootingSpec | None = None
[docs]
def build(self, layout, unit, **frame_kw) -> PierGeometry:
return pier_geometry(layout, unit, self.cap_design, self.bent,
footing=self.footing, **frame_kw)
[docs]
@dataclass(frozen=True)
class PileBentSpec:
"""Capped-pile pier (see :func:`pile_bent_geometry`)."""
cap_design: object
pile_xs_ft: tuple[float, ...]
#: AISC HP label from :mod:`civilpy.structural.steel`
#: (CPP-1-08 default ``"HP12X53"``).
pile_shape: str = "HP12X53"
pile_length_ft: float = 40.0
[docs]
def build(self, layout, unit, **frame_kw) -> PierGeometry:
return pile_bent_geometry(layout, unit, self.cap_design,
self.pile_xs_ft,
pile_shape=self.pile_shape,
pile_length_ft=self.pile_length_ft,
**frame_kw)
[docs]
@dataclass(frozen=True)
class HammerheadSpec:
"""Single-column hammerhead pier (see :func:`hammerhead_geometry`).
``column`` is the executed :class:`~civilpy.structural.pier
.PierColumn`."""
cap_design: object
column: object
tip_depth_ft: float | None = None
footing: FootingSpec | None = None
[docs]
def build(self, layout, unit, **frame_kw) -> PierGeometry:
return hammerhead_geometry(layout, unit, self.cap_design,
self.column,
tip_depth_ft=self.tip_depth_ft,
footing=self.footing, **frame_kw)
[docs]
@dataclass(frozen=True)
class SeatAbutmentSpec:
"""Conventional seat abutment: the Phase-4 :class:`AbutmentSpec`
plus its cap design, buildable per unit."""
cap_design: object
spec: AbutmentSpec
[docs]
def build(self, layout, unit, **frame_kw) -> AbutmentGeometry:
return abutment_geometry(layout, unit, self.cap_design, self.spec,
**frame_kw)
[docs]
@dataclass(frozen=True)
class IntegralAbutmentSpec:
"""Integral abutment: a full-height end diaphragm cast around the
girder ends on a **single row** of piles — no bearings. The
diaphragm depth is derived from the layout (high deck edge down to
``embed_below_girder_ft`` under the girder bottom flange), not a free
parameter; piles embed ``pile_embed_in`` (2 ft typical) into it."""
pile_xs_ft: tuple[float, ...]
#: AISC HP label from :mod:`civilpy.structural.steel`, e.g.
#: ``"HP10X42"``.
pile_shape: str = "HP10X42"
pile_length_ft: float = 40.0
diaphragm_thickness_in: float = 36.0
embed_below_girder_ft: float = 1.0
end_extension_ft: float = 2.0
pile_embed_in: float = 24.0
wingwall: object | None = None # RetainingWall
wingwall_length_ft: float = 0.0
[docs]
def build(self, layout, unit, **_frame_kw) -> AbutmentGeometry:
return integral_abutment_geometry(layout, unit, self)
[docs]
def integral_abutment_geometry(layout, unit: SubstructureUnit,
spec: IntegralAbutmentSpec
) -> AbutmentGeometry:
"""Place one integral abutment (see :class:`IntegralAbutmentSpec`).
The superstructure emit must skip the bearing stack at this support
line (``girder_bridge_emit(..., integral_supports=...)``); the
``gdr.*`` support points stay for the analysis reader."""
inp = layout.inputs
at, u = _support_frame(layout, unit.station_ft)
cos_skew = math.cos(math.radians(inp.skew_deg))
width_along_cap = (inp.girder_count - 1) * inp.girder_spacing_ft / cos_skew
s0 = -spec.end_extension_ft
length = width_along_cap + 2.0 * spec.end_extension_ft
z_gb = min(bp.location[2] for bp in layout.bearings
if bp.station_index == unit.index) # girder bottom flange
z_bot = z_gb - spec.embed_below_girder_ft
z_top = _deck_top_high(layout)
t = spec.diaphragm_thickness_in / 12.0
cap = CapBeam(origin=at(s0, z_top), axis=u, length_ft=length,
width_ft=t, depth_ft=z_top - z_bot)
diaphragm = WallPanel(origin=at(s0, z_bot), axis=u, length_ft=length,
thickness_ft=t, height_ft=z_top - z_bot)
piles = _piles_along_cap(at, z_bot, spec.pile_xs_ft, spec.pile_shape,
spec.pile_length_ft, spec.pile_embed_in)
wingwalls: list[WallPanel] = []
if spec.wingwall is not None and spec.wingwall_length_ft > 0.0:
wall = spec.wingwall
back = -1.0 if unit.index == 0 else 1.0
w_axis = (back, 0.0, 0.0)
z_stem_bot = z_top - wall.stem_height
for s_end in (s0, s0 + length):
x, y, _ = at(s_end, 0.0)
wingwalls.append(WallPanel(
origin=(x, y, z_stem_bot), axis=w_axis,
length_ft=spec.wingwall_length_ft,
thickness_ft=wall.stem_thickness,
height_ft=wall.stem_height))
wingwalls.append(WallPanel(
origin=(x, y, z_stem_bot - wall.footing_thickness),
axis=w_axis, length_ft=spec.wingwall_length_ft,
thickness_ft=wall.base_width,
height_ft=wall.footing_thickness))
return AbutmentGeometry(unit=unit, cap=cap, seats=(), piles=piles,
backwall=None, wingwalls=tuple(wingwalls),
kind="integral", diaphragm=diaphragm)
[docs]
@dataclass(frozen=True)
class SemiIntegralAbutmentSpec:
"""Seat abutment plus the superstructure-borne end diaphragm (see
:func:`semi_integral_abutment_geometry`)."""
cap_design: object
spec: AbutmentSpec
diaphragm_thickness_in: float = 30.0
[docs]
def build(self, layout, unit, **frame_kw) -> AbutmentGeometry:
return semi_integral_abutment_geometry(
layout, unit, self.cap_design, self.spec,
diaphragm_thickness_in=self.diaphragm_thickness_in, **frame_kw)
[docs]
def assemble_substructure(layout, assignments: dict, *,
bearing_stack_in: float = DEFAULT_BEARING_STACK_IN,
seat_min_in: float = SEAT_MIN_IN,
seat_side_in: float = SEAT_SIDE_IN
) -> SubstructureLayout:
"""Place a substructure that mixes unit types.
``assignments`` maps a support-line index (0 at the start abutment)
to its typed spec (:class:`BentPierSpec`, :class:`PileBentSpec`,
:class:`SeatAbutmentSpec`, ...); the string keys ``"pier"`` and
``"abutment"`` supply defaults for unassigned units of that role."""
frame_kw = dict(bearing_stack_in=bearing_stack_in,
seat_min_in=seat_min_in, seat_side_in=seat_side_in)
abutments, piers = [], []
for unit in substructure_units(layout):
role = ("abutment" if unit.name.startswith("Abutment") else "pier")
spec = assignments.get(unit.index, assignments.get(role))
if spec is None:
raise ValueError(f"no spec assigned for {unit.name} "
f"(index {unit.index})")
geom = spec.build(layout, unit, **frame_kw)
(abutments if isinstance(geom, AbutmentGeometry)
else piers).append(geom)
return SubstructureLayout(layout=layout, abutments=tuple(abutments),
piers=tuple(piers))
[docs]
def substructure_from_layout(layout, *, pier_cap, pier_bent,
abutment_cap, abutment: AbutmentSpec,
footing: FootingSpec | None = None,
bearing_stack_in: float =
DEFAULT_BEARING_STACK_IN,
seat_min_in: float = SEAT_MIN_IN,
seat_side_in: float = SEAT_SIDE_IN
) -> SubstructureLayout:
"""Place the full substructure under ``layout`` from the executed
designs: every pier gets ``pier_cap`` + ``pier_bent`` and every
abutment gets ``abutment_cap`` + ``abutment`` (one design reused
across identical units, the way the notebook designs them — pass the
per-unit builders directly for units that differ)."""
abutments, piers = [], []
for unit in substructure_units(layout):
if unit.name.startswith("Abutment"):
abutments.append(abutment_geometry(
layout, unit, abutment_cap, abutment,
bearing_stack_in=bearing_stack_in,
seat_min_in=seat_min_in, seat_side_in=seat_side_in))
else:
piers.append(pier_geometry(
layout, unit, pier_cap, pier_bent, footing=footing,
bearing_stack_in=bearing_stack_in,
seat_min_in=seat_min_in, seat_side_in=seat_side_in))
return SubstructureLayout(layout=layout, abutments=tuple(abutments),
piers=tuple(piers))