civilpy.structural package

Structural engineering — the most developed CivilPy package: member and section tools, AASHTO LRFD / AREMA design checks, strut-and-tie tools, bridge substructure designers, and the hub-and-spoke bridge pipeline — bridge_layout builds the canonical model, midas_models feeds a live MIDAS Civil NX session, and the rhino_bim emit layer produces the tagged BrIM geometry (drawn live in Rhino 8 or baked headlessly to .3dm with emit_to_3dm), with per-object ODOT pay items rolling up into quantities.

Subpackages

Submodules

civilpy.structural.abutment module

Cantilever retaining walls and stub / full-height abutments.

A RetainingWall carries the geometry of a conventional cantilever wall (toe, stem, heel, footing) and computes the three external-stability checks every wall and spread-footing abutment must satisfy – overturning, sliding, and bearing – driving the earth pressures from the Rankine and Coulomb coefficients already in civilpy.geotech.lateral_earth and the contact-pressure distribution from civilpy.geotech.shallow_foundation. The structural design of the stem, toe, and heel as reinforced-concrete cantilevers reuses the AASHTO LRFD resistance functions in civilpy.structural.aashto.lrfd.concrete. A wingwall is handled as a retaining wall taken at its average exposed height.

Units: feet, pounds, pcf (pressures psf), everything per running foot of wall; the structural helpers convert to the kip-inch-ksi LRFD convention. Moments and lever arms are measured about the bottom front corner of the toe. Angles in degrees.

class civilpy.structural.abutment.RetainingWall(stem_height: float, stem_thickness: float, toe_length: float, heel_length: float, footing_thickness: float, backfill_gamma: float, backfill_phi: float, concrete_gamma: float = 150.0, surcharge: float = 0.0, backfill_slope: float = 0.0, base_friction_deg: float | None = None, base_cohesion: float = 0.0, embedment: float = 0.0, include_passive: bool = False, include_toe_soil: bool = True, method: Literal['rankine', 'coulomb'] = 'rankine', wall_friction_deg: float = 0.0)[source]

Bases: object

Cantilever retaining wall geometry and soil parameters.

stem_height is the stem height above the footing, stem_thickness its (rectangular) thickness; toe_length and heel_length are the footing projections in front of and behind the stem; footing_thickness the base slab depth. backfill_gamma/backfill_phi describe the retained soil; surcharge a uniform vertical surcharge (psf) over the heel; backfill_slope the surface slope behind the wall (deg). base_friction_deg the soil-footing interface friction angle and base_cohesion its adhesion (psf). When include_passive the passive wedge over an embedment (ft) of soil on the toe is credited to sliding.

backfill_gamma: float
backfill_phi: float
backfill_slope: float = 0.0
base_cohesion: float = 0.0
base_friction_deg: float | None = None
property base_width: float
concrete_gamma: float = 150.0
embedment: float = 0.0
footing_thickness: float
heel_design(q_avg: float, a_s: float, f_c: float = 4.0, f_y: float = 60.0, cover_in: float = 3.0, bar_dia_in: float = 1.0, load_factor: float = 1.35) dict[str, CheckResult][source]

Heel cantilever: net downward pressure (soil + surcharge + self-weight, less the upward bearing q_avg psf averaged under the heel) produces tension in the top. a_s is top steel per foot; q_avg is the contact pressure under the heel from stability().

heel_length: float
include_passive: bool = False
include_toe_soil: bool = True
property ka: float
property kp: float
method: Literal['rankine', 'coulomb'] = 'rankine'

"rankine" or "coulomb".

Type:

Lateral earth-pressure theory

stability(fs_sliding: float = 1.5, fs_overturning: float = 2.0) StabilityResult[source]

Overturning, sliding, and bearing stability per foot of wall.

stem_design(a_s: float, f_c: float = 4.0, f_y: float = 60.0, cover_in: float = 2.0, bar_dia_in: float = 1.0, eh: float = 1.5) dict[str, CheckResult][source]

Flexure and shear at the base of the stem, reusing the LRFD RC functions. a_s is the vertical reinforcement in a 12-in strip (in^2/ft); the stem height drives the cantilever moment.

stem_height: float
stem_thickness: float
surcharge: float = 0.0
toe_design(q_avg: float, a_s: float, f_c: float = 4.0, f_y: float = 60.0, cover_in: float = 3.0, bar_dia_in: float = 1.0, load_factor: float = 1.35) dict[str, CheckResult][source]

Toe cantilever: the upward bearing pressure q_avg (psf, net of the toe self-weight) produces tension in the bottom. a_s is bottom steel per foot.

toe_length: float
property total_height: float

Height of the active-pressure plane through the heel, including the soil wedge above a sloping backfill.

wall_friction_deg: float = 0.0
classmethod wingwall(high_height: float, low_height: float, **kwargs) RetainingWall[source]

Build a wingwall as a retaining wall taken at the average of its high and low exposed stem heights (a common simplification for the tapering wing of an abutment). Extra keyword arguments are passed through to RetainingWall.

class civilpy.structural.abutment.StabilityResult(sum_v: float, sum_h: float, resisting_moment: float, overturning_moment: float, eccentricity: float, base_width: float, contact: object, passive: float = 0.0, fs_required_sliding: float = 1.5, fs_required_overturning: float = 2.0, details: dict = <factory>)[source]

Bases: object

External-stability summary for a retaining wall / abutment, per foot of wall.

base_width: float
bearing_ok(allowable: float) bool[source]

True if the peak contact pressure is within allowable (psf).

contact: object
details: dict
eccentricity: float
property fs_overturning: float
fs_required_overturning: float = 2.0
fs_required_sliding: float = 1.5
property fs_sliding: float
property ok_overturning: bool
property ok_sliding: bool
overturning_moment: float
passive: float = 0.0
property q_max: float
property q_min: float
resisting_moment: float
property resultant_in_middle_third: bool
sum_h: float
sum_v: float

civilpy.structural.beam_bending module

Beam analysis module for shear force, bending moment, and deflection diagrams.

Sign conventions follow AISC Steel Construction Manual (Table 3-23):
  • Loads: positive = downward (gravity direction)

  • Shear V: positive = left portion of beam has upward resultant (left-face-up)

  • Moment M: positive = sagging (concave up, bottom-fiber tension)

  • Reactions: positive = upward

  • Deflection: negative = downward (shown below zero line)

All units are US customary: lengths in feet (ft), forces in kips, moments in kip·ft. Deflection calculations require E in ksi and I in in⁴; results are in inches.

Overhanging beams (AISC Table 3-23 Cases 24–28) are fully supported. The pinned and rolling supports may be placed anywhere within the beam span — they do not need to coincide with the beam ends. When a support is interior, the beam simply overhangs that support on one or both sides.

Examples

Simply supported beam (supports at ends):

>>> my_beam = Beam(9)
>>> my_beam.pinned_support = 0
>>> my_beam.rolling_support = 9
>>> my_beam.add_loads([PointLoadV(20, 4.5)])
>>> F_Ax, F_Ay, F_By = my_beam.get_reaction_forces()
>>> round(F_Ay, 4), round(F_By, 4)
(10.0, 10.0)

Overhanging beam — AISC Case 26 (P at free overhang end): Beam 0–12 ft, pin at x=0, roller at x=8, 10-kip load at x=12. Expected: R_A = −5 kips (down), R_B = 15 kips (up).

>>> ob = Beam(12)
>>> ob.pinned_support = 0
>>> ob.rolling_support = 8
>>> ob.add_loads([PointLoadV(10, 12)])
>>> F_Ax, F_Ay, F_By = ob.get_reaction_forces()
>>> round(F_Ay, 4), round(F_By, 4)
(-5.0, 15.0)
class civilpy.structural.beam_bending.Beam(span: float = 10)[source]

Bases: object

Represents a one-dimensional beam that can take axial and tangential loads.

Through the method add_loads, a Beam object can accept a list of:

  • PointLoad objects, and/or

  • DistributedLoad objects.

Notes

  • Supports only statically determinate beams with exactly one pinned and one roller support.

  • Supports may be placed at any x-coordinate within the beam span. When a support is interior to the span the beam overhangs that support on one or both sides (AISC Table 3-23 Cases 24–28).

  • Units: lengths in ft, forces in kips, moments in kip·ft. Deflection requires E in ksi and I in in⁴; result is in inches.

__init__(span: float = 10)[source]

Initializes a Beam object of a given length.

Parameters:

span (float or int) – Length of the beam span. Must be positive, and the pinned and rolling supports can only be placed within this span. The default value is 10.

add_loads(loads: list)[source]

Apply an arbitrary list of (point- or distributed) loads to the beam.

Parameters:

loads (list) – An iterable containing DistributedLoad or PointLoad objects to be applied to the Beam object. Note that the load application point (or segment) must be within the Beam span.

Raises:

TypeError – If any of the provided loads is not of a supported type.

get_bending_moment()[source]

Alias for get_moment_function to maintain compatibility with older notebook cells.

get_deflection_function(E_ksi, I_in4)[source]

Returns the symbolic deflection function δ(x) in inches.

Parameters:
  • E_ksi (float) – Modulus of elasticity in ksi (e.g. 29000 for steel).

  • I_in4 (float) – Moment of inertia in in⁴. Can be a Pint quantity — magnitude is used.

get_envelope(loads: list, step_size: float = 0.5)[source]

Calculates the shear and moment envelope for a moving load or set of loads.

Parameters:
  • loads (list) – A list of PointLoadV objects representing the moving load configuration. The coordinates of these loads should be relative to the first load (at 0).

  • step_size (float) – The distance to move the load at each step.

Returns:

  • x_vec (ndarray) – The x-coordinates along the beam.

  • max_shear (ndarray) – Maximum shear at each x.

  • min_shear (ndarray) – Minimum shear at each x.

  • max_moment (ndarray) – Maximum moment at each x.

  • min_moment (ndarray) – Minimum moment at each x.

get_moment_function()[source]
get_reaction_forces()[source]

Calculates the reaction forces at the supports, given the applied loads.

The first and second values correspond to the horizontal and vertical forces of the pinned support. The third one is the vertical force at the rolling support.

Returns:

(F_Ax, F_Ay, F_By) reaction force components for pinned (x,y) and rolling (y) supports respectively.

Return type:

tuple

property length

Length of the beam. Must be positive.

Type:

float or int

property pinned_support

x-coordinate of the beam’s pinned support. Must be within the beam span.

Type:

float or int

plot()[source]

Generates a single figure with 4 plots.

The plots correspond to:
  • a schematic of the loaded beam

  • normal force diagram

  • shear force diagram

  • bending moment diagram

Returns:

figure with the 4 subplots.

Return type:

matplotlib.figure.Figure

plot_all()[source]

Generates a figure with 4 subplots: beam diagram, shear, moment, deflection.

Parameters are obtained from the beam’s existing loads. Deflection is omitted (use plot() or call plot_deflection() separately with E and I values). Returns a figure with shear and moment only alongside the beam diagram.

plot_beam_diagram(ax=None)[source]

Returns a schematic of the beam and all the loads applied on it.

plot_bending_moment(ax=None)[source]

Returns a plot of the bending moment as a function of the x-coordinate.

plot_deflection(E_ksi, I_in4, ax=None)[source]

Plots the deflection diagram in inches.

Parameters:
  • E_ksi (float) – Modulus of elasticity in ksi.

  • I_in4 (float or Pint quantity) – Moment of inertia in in⁴.

plot_envelope(x_vec, max_shear, min_shear, max_moment, min_moment, ax=None)[source]

Plots the shear and moment envelopes.

plot_normal_force(ax=None)[source]

Returns a plot of the normal force as a function of the x-coordinate.

plot_shear_force(ax=None)[source]

Returns a plot of the shear force as a function of the x-coordinate.

property rolling_support

x-coordinate of the beam’s rolling support. Must be within the beam span.

Type:

float or int

class civilpy.structural.beam_bending.CantileverBeam(span: float = 10)[source]

Bases: Beam

A cantilever beam: fixed at x=0, free at x=span.

Loads and coordinates use the same conventions as Beam (kips, ft).

Example

>>> cb = CantileverBeam(10)
>>> cb.add_loads([PointLoadV(-5, 10)])   # 5 kip downward at free end
>>> V, M = cb.get_reaction_forces()
>>> round(float(V), 4), round(float(M), 4)
(5.0, 50.0)
get_deflection_function(E_ksi, I_in4)[source]

Returns the symbolic deflection function δ(x) in inches.

Boundary conditions: δ(0) = 0, δ’(0) = 0 (fixed end).

get_reaction_forces()[source]

Calculates the reaction forces (V_A, M_A) for a cantilever beam.

Returns:

(V_A, M_A) Vertical reaction force (positive upward) and moment (positive counter-clockwise)

at the fixed support (left end).

Return type:

tuple

plot_beam_diagram(ax=None)[source]

Returns a schematic of the cantilever beam.

class civilpy.structural.beam_bending.ContinuousBeam(span: float = 10, intermediate_supports: list = None)[source]

Bases: Beam

A continuous beam with an arbitrary number of intermediate roller supports.

Parameters:
  • span (float) – Length of the beam.

  • intermediate_supports (list of float) – x-coordinates of intermediate roller supports.

__init__(span: float = 10, intermediate_supports: list = None)[source]

Initializes a continuous beam.

Parameters:
  • span (float) – Length of the beam. Defaults to 10.

  • intermediate_supports (list of float, optional) – x-coordinates of intermediate roller supports.

get_reaction_forces()[source]

Returns (F_Ax, F_Ay, F_By) where F_Ay and F_By are the base supports. Intermediate reactions can be retrieved from self._redundant_reactions.

class civilpy.structural.beam_bending.DistributedLoadH(expr, span)[source]

Bases: DistributedLoadH

Distributed horizontal load (kips/ft) over a span interval (ft).

Examples

>>> wind_load = DistributedLoadH("0.5", (0, 20))  # 0.5 kips/ft over full span
class civilpy.structural.beam_bending.DistributedLoadV(expr, span)[source]

Bases: DistributedLoadV

Distributed vertical load (kips/ft) over a span interval (ft). Positive = downward.

Examples

>>> snow_load = DistributedLoadV("2*x+1", (0, 10))  # downward, linearly varying, 0 to 10 ft
class civilpy.structural.beam_bending.FixedFixedBeam(span: float = 10)[source]

Bases: Beam

A fixed-fixed beam: fixed at x=0, fixed at x=span.

Boundary conditions: - x = 0: slope = 0, deflection = 0 - x = span: slope = 0, deflection = 0

get_deflection_function(E_ksi, I_in4)[source]

Returns the symbolic deflection function δ(x) in inches.

Parameters:
  • E_ksi (float) – Modulus of elasticity in ksi (e.g. 29000 for steel).

  • I_in4 (float) – Moment of inertia in in⁴. Can be a Pint quantity — magnitude is used.

get_reaction_forces()[source]

Calculates the reaction forces at the supports, given the applied loads.

The first and second values correspond to the horizontal and vertical forces of the pinned support. The third one is the vertical force at the rolling support.

Returns:

(F_Ax, F_Ay, F_By) reaction force components for pinned (x,y) and rolling (y) supports respectively.

Return type:

tuple

class civilpy.structural.beam_bending.PointLoadH(force, coord)[source]

Bases: PointLoadH

Horizontal point load: (force in kips, coord in ft). Positive = rightward.

Examples

>>> external_force = PointLoadH(10, 9)  # 10 kips to the right at x=9 ft
>>> external_force
PointLoadH(force=10, coord=9)
class civilpy.structural.beam_bending.PointLoadV(force, coord)[source]

Bases: PointLoadV

Vertical point load: (force in kips, coord in ft). Positive = downward.

Examples

>>> external_force = PointLoadV(30, 3)  # 30 kips downwards at x=3 ft
>>> external_force
PointLoadV(force=30, coord=3)
class civilpy.structural.beam_bending.PointTorque(torque, coord)[source]

Bases: PointTorque

Point clockwise torque: (torque in kip·ft, coord in ft).

Examples

>>> applied_moment = PointTorque(30, 4)  # 30 kip·ft clockwise at x=4 ft
class civilpy.structural.beam_bending.ProppedCantileverBeam(span: float = 10)[source]

Bases: Beam

A propped cantilever beam: fixed at x=0, roller at x=span.

Boundary conditions: - x = 0: slope = 0, deflection = 0 - x = span: deflection = 0

Examples

>>> pcb = ProppedCantileverBeam(10)
>>> pcb.add_loads([PointLoadV(5, 5)])  # 5 kip downward at midspan
>>> R_A, V_A, M_A, R_B = pcb.get_reaction_forces()
get_deflection_function(E_ksi, I_in4)[source]

Returns the symbolic deflection function δ(x) in inches. Boundary conditions: δ(0) = 0, δ’(0) = 0.

get_reaction_forces()[source]

Calculates the reaction forces (F_Ax, V_A, M_A, R_B) for a propped cantilever.

Returns:

(F_Ax, V_A, M_A, R_B)
  • F_Ax: Horizontal reaction at fixed end.

  • V_A: Vertical reaction at fixed end (positive upward).

  • M_A: Moment at fixed end (positive counter-clockwise).

  • R_B: Vertical reaction at roller (positive upward).

Return type:

tuple

civilpy.structural.beam_bending.plot_beam_full(beam, E=29000, I=None, title='')[source]

2×2 grid: beam diagram, shear, moment, deflection (if I provided).

Parameters:
  • beam (Beam or CantileverBeam) – A beam object with loads already applied.

  • E (float) – Modulus of elasticity in ksi. Default 29000 (steel).

  • I (float or Pint quantity, optional) – Moment of inertia in in⁴. When omitted the deflection panel is hidden.

  • title (str, optional) – Figure suptitle.

civilpy.structural.bim module

BrIM attribute schema: the per-component BIM record the Rhino “source of truth” model carries as user text, and that MIDAS / quantity estimating read back.

Every component gets a typed attribute set (not the blanket gdr.*):

  • shared bim.type (girder / deck / parapet / bearing / load_plate / haunch / shear_stud / rebar / diaphragm) and a unique bim.id;

  • where the part is a standard detail, bim.scd + bim.scd_year – the highest-value BIM keys, since the SCD implies most of the rest;

  • a pay item (pay.*) so quantities roll straight into an estimate;

  • a material block (mat.*): spec/grade/treatment for steel, f’c/class for concrete, coating/size for reinforcing.

Builders return a flat {key: str} dict ready to write to Rhino user text (or IFC property sets). Values are strings so they survive the .3dm round-trip.

class civilpy.structural.bim.CostEstimate(rows: dict[str, dict], total: float, unpriced: tuple[str, ...] = ())[source]

Bases: object

A priced pay-item rollup. rows extends the quantity records ({"desc", "unit", "qty", "objects"}) with unit_price and cost; items the price book doesn’t know are listed in unpriced (their cost is None) and excluded from total.

rows: dict[str, dict]
total: float
unpriced: tuple[str, ...] = ()
civilpy.structural.bim.DEFAULT_UNIT_PRICES: dict[str, float] = {'507E10000': 75.0, '509E00100': 1.4, '509E00200': 1.6, '509E00300': 3.0, '511E12100': 950.0, '511E40000': 850.0, '512E10000': 1200.0, '513E10220': 2.25, '513E20000': 10.0, '515E10000': 22000.0, '515E20000': 30000.0, '515E30000': 1500.0, '516E10000': 2000.0, '526E10000': 120.0}

Planning-level unit prices ($ per pay-item unit) for the seed catalog — round numbers in the range of recent ODOT bid tabulations, meant to rank alternatives and sanity-check a budget, not to replace a district estimate. Override any entry (or add items) through the prices argument of cost_estimate().

civilpy.structural.bim.PAY_ITEMS: dict[str, PayItem] = {'507E10000': PayItem(code='507E10000', description='Steel piles HP, furnished and driven [CONFIRM]', unit='ft', category='507', level=1), '509E00100': PayItem(code='509E00100', description='Reinforcing steel, black [CONFIRM]', unit='lb', category='509', level=1), '509E00200': PayItem(code='509E00200', description='Epoxy coated reinforcing steel [CONFIRM]', unit='lb', category='509', level=1), '509E00300': PayItem(code='509E00300', description='GFRP deformed bars [CONFIRM]', unit='lb', category='509', level=1), '511E12100': PayItem(code='511E12100', description='Class QC2 concrete, superstructure (deck) [CONFIRM]', unit='cy', category='511', level=1), '511E40000': PayItem(code='511E40000', description='Class QC1 concrete, substructure [CONFIRM]', unit='cy', category='511', level=1), '512E10000': PayItem(code='512E10000', description='Concrete, parapet/railing [CONFIRM]', unit='cy', category='512', level=1), '513E10220': PayItem(code='513E10220', description='Structural steel members, Level 1', unit='lb', category='513', level=1), '513E20000': PayItem(code='513E20000', description='Shear connectors (welded studs)', unit='ea', category='513', level=1), '515E10000': PayItem(code='515E10000', description='Prestressed concrete box beam member [CONFIRM]', unit='ea', category='515', level=1), '515E20000': PayItem(code='515E20000', description='Prestressed concrete I-beam member [CONFIRM]', unit='ea', category='515', level=1), '515E30000': PayItem(code='515E30000', description='Intermediate diaphragms [CONFIRM]', unit='ea', category='515', level=1), '516E10000': PayItem(code='516E10000', description='Elastomeric bearing [CONFIRM]', unit='ea', category='516', level=1), '526E10000': PayItem(code='526E10000', description='Reinforced concrete approach slabs [CONFIRM]', unit='sy', category='526', level=1)}

Seed catalog. The two studs/steel items are confirmed from Dane; the concrete and reinforcing items are typical ODOT numbers flagged CONFIRM until verified against the current ODOT Construction & Material Specs item master.

class civilpy.structural.bim.PayItem(code: str, description: str, unit: str, category: str, level: int = 1)[source]

Bases: object

An ODOT pay item. level is the estimating detail level; unit the quantity unit (lb, ea, cy, ft, sf).

category: str
code: str
description: str
level: int = 1
unit: str
civilpy.structural.bim.REBAR_PAY_ITEM = {'black': '509E00100', 'epoxy': '509E00200', 'gfrp': '509E00300'}

Reinforcing pay item by coating (see PAY_ITEMS).

civilpy.structural.bim.SUBSTRUCTURE_CONCRETE_TYPES = ('pier_cap', 'abutment_cap', 'beam_seat', 'column', 'footing', 'backwall', 'wingwall', 'foreslope_wall', 'cutoff_wall')

Substructure concrete component types (one Rhino layer each).

civilpy.structural.bim.approach_slab_tags(bid: str, scd: str = 'AS-1-15', *, scd_year: str | int | None = None, length_ft: float, width_ft: float, thickness_in: float, skew_deg: float = 0.0, fc_psi: float = 4500.0, area_sy: float | None = None) dict[source]

Approach slab concrete. ITEM 526 measures the plan area (sy) and includes the slab reinforcing (anchor bars into the abutment are the exception — they measure under the ITEM 509 reinforcing items).

civilpy.structural.bim.bearing_tags(bid: str, *, fixity: str, kind: str = 'elastomeric', plies: int | None = None, ply_thickness_in: float | None = None, total_thickness_in: float | None = None) dict[source]
civilpy.structural.bim.box_beam_tags(bid: str, *, box: str, depth_in: float, beam_type: str, part: str, span_ft: float, scd: str | None = 'PSBD-1-25', scd_year: str | int | None = 2025, fc_psi: float = 6000.0, n_strands: int | None = None, concrete_cy: float | None = None, count: int | None = None) dict[source]

Prestressed box-beam member. The member is drawn as several part prisms (top/bottom flange, webs) sharing a beam id prefix; exactly one part per beam carries count so the 515 member item counts each beam once (strands, tie rods, and precast diaphragms are included in the member).

civilpy.structural.bim.concrete_mat(fc_psi: float, cls: str = 'QC2') dict[source]

Concrete material block.

civilpy.structural.bim.cost_estimate(quantities: dict[str, dict], prices: dict[str, float] | None = None) CostEstimate[source]

Price a quantity rollup (the {item: {"desc", "unit", "qty", ...}} dict that pay_item_quantities / read_bim_quantities produce).

Unit prices come from DEFAULT_UNIT_PRICES updated with prices; see the caveat there — these are planning numbers.

civilpy.structural.bim.cross_frame_tags(bid: str, *, frame_type: str, member_shape: str, grade: str = '50', weight_lb: float | None = None) dict[source]

Intermediate cross-frame / steel diaphragm bay (build plan §3a). Its members are fabricated structural steel measured by weight into the 513 item like the girders.

civilpy.structural.bim.deck_tags(bid: str, *, thickness_in: float, slope_pct: float, crown_offset_ft: float, fc_psi: float = 4500.0, cls: str = 'QC2', volume_cy: float | None = None) dict[source]
civilpy.structural.bim.diaphragm_tags(bid: str, *, thickness_in: float, fc_psi: float = 4500.0, volume_cy: float | None = None, pay: bool = True, item: str = '511E12100', count: float | None = None) dict[source]

Concrete end/intermediate diaphragm. Integral and semi-integral end diaphragms are cast with (and move with) the superstructure, so their concrete measures into the superstructure item like the haunches; a box beam’s precast diaphragms are included in the member (pay=False). A PS I-beam bridge’s intermediate diaphragms have their own 515 each-measured item (PSID-1-13 sheet 10) — pass item="515E30000" with count instead of volume_cy.

civilpy.structural.bim.field_splice_tags(bid: str, *, bolt_count: int, grade: str = '50', weight_lb: float | None = None) dict[source]

Bolted field splice (§3a): the splice plates are structural steel by weight (513); the high-strength bolts are incidental, carried as a count.

civilpy.structural.bim.girder_tags(bid: str, shape: str, *, grade: str = '50W', spec: str = 'ASTM A709', treatment: str = 'none', weight_lb: float | None = None) dict[source]
civilpy.structural.bim.haunch_tags(bid: str, *, depth_in: float, width_in: float, fc_psi: float = 4500.0, volume_cy: float | None = None) dict[source]

Haunch concrete is conventionally measured with the superstructure (deck) concrete item, so a volume_cy rolls into 511E12100.

civilpy.structural.bim.load_plate_tags(bid: str, *, thickness_in: float, grade: str = '50', spec: str = 'ASTM A709', weight_lb: float | None = None) dict[source]
civilpy.structural.bim.parapet_tags(bid: str, scd: str, *, scd_year: str | int | None = None, height_in: float | None = None, fc_psi: float = 4500.0, length_ft: float | None = None, volume_cy: float | None = None) dict[source]
civilpy.structural.bim.pay_item(code: str) PayItem[source]
civilpy.structural.bim.pile_tags(bid: str, *, shape: str, length_ft: float, grade: str = '50', spec: str = 'ASTM A572') dict[source]

Driven steel HP pile: pay quantity is the furnished+driven length (ft) below cutoff.

civilpy.structural.bim.ps_i_beam_tags(bid: str, *, section: str, depth_in: float, span_ft: float, scd: str | None = 'PSID-1-13', scd_year: str | int | None = 2025, fc_psi: float = 5500.0, n_strands: int | None = None, n_debonded: int | None = None, concrete_cy: float | None = None, count: int | None = None) dict[source]

Prestressed I-beam member (PSID-1-13). One prism per beam; the member carries the 515 I-beam item (strands, embedded sole plates, and anchorage-zone steel are included in the member per sheet 10).

civilpy.structural.bim.rebar_mat(size: int, coating: str = 'epoxy') dict[source]

Reinforcing material block. coating = epoxy / GFRP / stainless / black; size the bar number (#), diameter in eighths of an inch. The tabulated weight is steel (C&MS 509); GFRP bars carry the spec reference instead (C&MS 705.28) since their weight is producer-specific.

civilpy.structural.bim.rebar_tags(bid: str, *, size: int, coating: str = 'epoxy', mat: str = 'top', bend: str = 'straight', length_ft: float | None = None, scd: str | None = None) dict[source]
civilpy.structural.bim.shear_stud_tags(bid: str, *, dia_in: float = 0.875, length_in: float = 6.0, count: int | None = None) dict[source]
civilpy.structural.bim.steel_mat(spec: str = 'ASTM A709', grade: str = '50W', treatment: str = 'none') dict[source]

Structural-steel material block. grade is 36/50/50W/70; 50W and 70W are weathering. treatment = none / galvanized / painted.

civilpy.structural.bim.stiffener_tags(bid: str, *, kind: str, thickness_in: float, grade: str = '50', weight_lb: float | None = None) dict[source]

Web/bearing/longitudinal stiffener plate (§3a). kind is transverse / bearing / longitudinal; measured by weight into the 513 structural-steel item.

civilpy.structural.bim.substructure_concrete_tags(btype: str, bid: str, *, fc_psi: float = 4000.0, cls: str = 'QC1', volume_cy: float | None = None, **dims: float) dict[source]

Tag block for a cast-in-place substructure concrete component.

btype is one of SUBSTRUCTURE_CONCRETE_TYPES; extra dims keywords flatten to "<btype>.<key>" string tags (e.g. depth_ft=5.0 on a pier_cap becomes pier_cap.depth_ft). All substructure concrete measures into the one Class QC1 substructure item, the way haunches roll into the deck item.

civilpy.structural.bim.tendon_tags(bid: str, *, strands: int, row_in: float, debonded: int | None = None) dict[source]

One schematic prestressing-strand row (paid with the member). debonded counts the row’s strands debonded at each beam end.

civilpy.structural.bim.tie_rod_tags(bid: str, *, diameter_in: float, station_ft: float) dict[source]

Transverse tie rod (paid with the members).

civilpy.structural.bim_spec module

Storable parametric Spec records — the BIM system’s schema authority.

The queryable-BIM architecture stores every parameter needed to run the civilpy.structural.aashto.lrfd checks from scratch as a JSON document (Postgres JSONB on the application side). This module declares those records once, as plain dataclasses of JSON-safe primitives:

  • every field carries its engineering metadata (unit, bounds, enum options, the checks that consume it) in dataclasses.field(metadata=) — one declaration drives validation, documentation, and the check-coverage test;

  • record_to_dict() / record_from_dict() round-trip a record through a JSON document with no schema declared anywhere else (add a field here and the document simply carries an extra key);

  • JSON is not type-checked on write, so SpecRecord.validate() is the type guarantor — validate before save;

  • the record reconstitutes the executed-design objects the geometry builders in civilpy.structural.substructure_layout consume, so a record read back from storage can regenerate geometry, the tagged .3dm emit, and every check input without the session that authored it.

The first element modeled end-to-end was the single-column hammerhead pier (HammerheadPierRecord); the Phase-6 breadth elements — BentPierRecord, PileBentRecord, SeatAbutmentRecord — follow the same pattern. Each element has a *_CHECK_INPUTS coverage map tying every LRFD check that consumes it to the record fields that feed it — the test suite walks the maps, so a check added without a mapped input path (or a field renamed out from under one) fails loudly instead of rotting silently.

Units follow the hub convention: layout-scale dimensions in feet (_ft), section-scale in inches (_in), strengths in ksi.

civilpy.structural.bim_spec.BENT_PIER_CHECK_INPUTS: dict[str, dict[str, tuple]] = {'4.5.3.2.2b': {'m_2': ('loads', 'larger end moment'), 'p_e': ('derived', ('column.b_in', 'column.h_in', 'column.diameter_in', 'column.height_ft', 'column.fixity', 'column.fc_ksi'), 'Euler load from cracked EI and effective length'), 'p_u': ('loads', 'factored axial')}, '5.10.8.2.1': {'d_b': ('derived', ('pier_cap.tie_bar_size',), 'bar diameter'), 'f_c': ('field', 'pier_cap.fc_ksi'), 'f_y': ('field', 'pier_cap.fy_ksi')}, '5.6.4.2': {'a_g': ('derived', ('column.b_in', 'column.h_in', 'column.diameter_in'), 'gross area'), 'a_st': ('field', 'column.bars_area_in2'), 'f_c': ('field', 'column.fc_ksi'), 'f_y': ('field', 'column.fy_ksi')}, '5.6.4.4': {'a_g': ('derived', ('column.b_in', 'column.h_in', 'column.diameter_in'), 'gross area'), 'a_st': ('field', 'column.bars_area_in2'), 'f_c': ('field', 'column.fc_ksi'), 'f_y': ('field', 'column.fy_ksi'), 'p_u': ('loads', 'factored axial from the cap reactions'), 'spiral': ('field', 'column.spiral')}, '5.6.4.5 check': {'b': ('field', 'column.b_in'), 'diameter': ('field', 'column.diameter_in'), 'f_c': ('field', 'column.fc_ksi'), 'f_y': ('field', 'column.fy_ksi'), 'h': ('field', 'column.h_in'), 'layers': ('derived', ('column.bars_area_in2', 'detailing.cover_in', 'column.h_in', 'column.diameter_in'), 'stem_rebar_layers()'), 'm_u': ('loads', 'factored (magnified) moment'), 'p_u': ('loads', 'factored axial'), 'spiral': ('field', 'column.spiral')}, '5.6.7': {'d_c': ('derived', ('detailing.cover_in', 'pier_cap.tie_bar_size'), 'cover + half bar diameter'), 'f_ss': ('loads', 'service tie-steel stress'), 'h': ('field', 'pier_cap.depth_ft')}, '5.7.2.5': {'a_v': ('derived', ('detailing.stirrup_size',), 'stirrup legs area'), 'b_v': ('field', 'pier_cap.thickness_ft'), 'f_c': ('field', 'pier_cap.fc_ksi'), 's': ('field', 'detailing.stirrup_spacing_in')}, '5.7.2.6': {'b_v': ('field', 'pier_cap.thickness_ft'), 'd_v': ('derived', ('pier_cap.depth_ft', 'detailing.cover_in', 'pier_cap.tie_bar_size'), 'effective shear depth from the section'), 'f_c': ('field', 'pier_cap.fc_ksi'), 'v_u': ('loads', 'factored shear at the section')}, '5.7.3.3': {'a_v': ('derived', ('detailing.stirrup_size',), 'stirrup legs area'), 'b_v': ('field', 'pier_cap.thickness_ft'), 'd_v': ('derived', ('pier_cap.depth_ft', 'detailing.cover_in', 'pier_cap.tie_bar_size'), 'effective shear depth from the section'), 'f_c': ('field', 'pier_cap.fc_ksi'), 'f_y': ('field', 'pier_cap.fy_ksi'), 's': ('field', 'detailing.stirrup_spacing_in'), 'v_u': ('loads', 'factored shear at the section')}, '5.8.2.4': {'a_st': ('derived', ('pier_cap.tie_bar_size', 'pier_cap.tie_bar_count'), 'bar area x count'), 'f_y': ('field', 'pier_cap.fy_ksi'), 'p_u': ('loads', 'governing tie force from the STM solve')}, '5.8.2.5': {'a_cn': ('derived', ('pier_cap.thickness_ft', 'detailing.seat_side_in'), 'node face area: seat side x min(cap width, seat side)'), 'f_c': ('field', 'pier_cap.fc_ksi'), 'p_u': ('loads', 'strut / node face force from the STM solve')}, '5.8.2.6': {'a_s_horizontal': ('derived', ('detailing.skin_bar_size',), 'two-face skin bar area'), 'a_s_vertical': ('derived', ('detailing.stirrup_size',), 'stirrup legs area'), 'b_w': ('field', 'pier_cap.thickness_ft'), 's_h': ('field', 'detailing.stirrup_spacing_in'), 's_v': ('field', 'detailing.skin_bar_spacing_in')}}

the hammerhead map with the stem paths renamed to the bent’s uniform column record (same checks, same physics).

Type:

Multi-column bent

class civilpy.structural.bim_spec.BearingStiffenerRecord(plate_width_in: float, plate_thickness_in: float, pairs: int = 1, fy_ksi: float = 50.0, milled_to_bear: bool = True)[source]

Bases: SpecRecord

Bearing stiffener plate pair at a support (6.10.11.2.3).

fy_ksi: float = 50.0
milled_to_bear: bool = True
pairs: int = 1
plate_thickness_in: float
plate_width_in: float
class civilpy.structural.bim_spec.BentPierRecord(pier_cap: PierCapRecord, column: PierStemRecord, column_xs_ft: tuple[float, ...], footing: FootingRecord | None = None, detailing: CapDetailingRecord | None = None, standard: str | None = None, standard_year: int | None = None, provenance: Provenance | None = None)[source]

Bases: ElementRecord

One multi-column bent pier: the cap envelope + governing tie (the bent cap’s tie is normally the bottom chord — set pier_cap.tie_at_top=False when storing one), a uniform column section/steel, and the column centers along the cap. Per-column height/section variation is a later refinement; the uniform case is the standard-drawing bent.

BIM_TYPE = 'pier'
SUBTYPE = 'bent'
build(layout, unit, **frame_kw)[source]
column: PierStemRecord
column_xs_ft: tuple[float, ...]
detailing: CapDetailingRecord | None = None
footing: FootingRecord | None = None
pier_cap: PierCapRecord
provenance: Provenance | None = None
standard: str | None = None
standard_year: int | None = None
to_bent()[source]

The bent object pier_geometry() consumes: column positions in inches from the cap’s left end + one executed column per position.

class civilpy.structural.bim_spec.BoxBeamBridgeRecord(box: str, span_ft: float, n_beams: int, skew_deg: float = 0.0, fc_psi: float = 6000.0, standard: str | None = None, standard_year: int | None = None, provenance: Provenance | None = None)[source]

Bases: ElementRecord

One adjacent prestressed box-beam bridge on the ODOT standard designs (PSBDD-1-25 tables): the BoxBridgeInput as a storable record. The strand pattern and section come from the design table for (box, span_ft) at emit time.

BIM_TYPE = 'bridge'
SUBTYPE = 'box_beam'
box: str
fc_psi: float = 6000.0
n_beams: int
provenance: Provenance | None = None
skew_deg: float = 0.0
span_ft: float
standard: str | None = None
standard_year: int | None = None
to_input()[source]

The BoxBridgeInput the emit engine consumes.

class civilpy.structural.bim_spec.BridgeLayoutRecord(spans_ft: tuple[float, ...], girder_count: int = 2, girder_spacing_ft: float = 8.0, girder_label: str = 'W36X150', overhang_ft: float = 3.0, railing: str = 'SBR-1-20', grade: str = 'Grade 50', skew_deg: float = 0.0, design_haunch_in: float = 2.0, deck_thickness_in: float | None = None, deck_fc_ksi: float = 4.5, cross_slope_pct: float = 2.0, crown_offset_ft: float | None = None, composite: bool = True, standard: str | None = None, standard_year: int | None = None, provenance: Provenance | None = None)[source]

Bases: ElementRecord

The bridge-level superstructure geometry as a storable record: the BridgeInput a stored bridge reconstitutes to for the .3dm emit. This is the whole-bridge frame (spans, girder count/spacing, overhang, skew, deck) that the per-girder-line SteelGirderRecord details hang off; the batch materializer reads it, calls to_bridge_input(), and runs girder_bridge_emit — so the served model comes from an authored Spec, not a second geometry engine (build plan §2.4/§8). Every field mirrors BridgeInput one-for-one, so the round-trip is faithful.

BIM_TYPE = 'bridge'
SUBTYPE = 'layout'
composite: bool = True
cross_slope_pct: float = 2.0
crown_offset_ft: float | None = None
deck_fc_ksi: float = 4.5
deck_thickness_in: float | None = None
design_haunch_in: float = 2.0
girder_count: int = 2
girder_label: str = 'W36X150'
girder_spacing_ft: float = 8.0
grade: str = 'Grade 50'
overhang_ft: float = 3.0
provenance: Provenance | None = None
railing: str = 'SBR-1-20'
skew_deg: float = 0.0
spans_ft: tuple[float, ...]
standard: str | None = None
standard_year: int | None = None
to_bridge_input()[source]

Reconstitute the BridgeInput the superstructure emit consumes.

class civilpy.structural.bim_spec.CapDetailingRecord(cover_in: float = 3.0, stirrup_size: int = 5, stirrup_spacing_in: float = 12.0, skin_bar_size: int = 5, skin_bar_spacing_in: float = 12.0, seat_side_in: float = 27.0)[source]

Bases: SpecRecord

Cap/stem detailing outside the STM schedule: covers, the shear stirrups the sectional checks run with, the D-region crack-control grid (5.8.2.6), and the bearing-seat plan side the node faces bear on.

cover_in: float = 3.0
seat_side_in: float = 27.0
skin_bar_size: int = 5
skin_bar_spacing_in: float = 12.0
stirrup_size: int = 5
stirrup_spacing_in: float = 12.0
class civilpy.structural.bim_spec.CompositeRecord(stud_diameter_in: float = 0.875, studs_per_row: int = 3, stud_gauge_in: float = 3.0, pitch_in: float = 12.0, effective_width_in: float = 96.0, modular_ratio_n: float = 8.0, haunch_in: float = 2.0, deck_fc_ksi: float = 4.5)[source]

Bases: SpecRecord

The shear-stud connection + composite deck properties – promotes what were emit-time stud arguments into stored fields so a bridge round-trips them. Consuming checks 6.10.10 (fatigue pitch, stud strength).

deck_fc_ksi: float = 4.5
effective_width_in: float = 96.0
haunch_in: float = 2.0
modular_ratio_n: float = 8.0
pitch_in: float = 12.0
stud_diameter_in: float = 0.875
stud_gauge_in: float = 3.0
studs_per_row: int = 3
class civilpy.structural.bim_spec.CrossFrameRecord(frame_type: str = 'K', member_shape: str = 'L4X4X1/2', member_length_ft: float = 6.0, spacing_ft: float = 20.0, stations_ft: tuple[float, ...] | None = None, connection_plate_thickness_in: float = 0.5, fy_ksi: float = 50.0)[source]

Bases: SpecRecord

An intermediate cross-frame / steel diaphragm bay: type, member section, connection plate, and along-span spacing. The members run the ported tension/compression checks (6.8.2.1 / 6.9.4.1.1); the spacing feeds 6.7.4 when it ports and the LTB unbraced length today (6.10.8.2.3 l_b).

bay_stations(length_ft: float) tuple[float, ...][source]

The cross-frame line stations along a girder of length_ft: the stored stations_ft when given (irregular spacing kept verbatim), else a uniform run at spacing_ft.

connection_plate_thickness_in: float = 0.5
frame_type: str = 'K'
fy_ksi: float = 50.0
max_bay_ft(length_ft: float) float[source]

Governing unbraced length l_b = the largest gap between brace points (span ends included).

member_length_ft: float = 6.0
member_shape: str = 'L4X4X1/2'
spacing_ft: float = 20.0
stations_ft: tuple[float, ...] | None = None
class civilpy.structural.bim_spec.ElementRecord[source]

Bases: SpecRecord

Base for whole-element records: the bim.type/subtype identity keys stamped into the flattened document, and the guarded round-trip every element shares.

BIM_TYPE = ''
SUBTYPE = ''
classmethod from_dict(data: dict)[source]
to_dict() dict[source]

The JSONB document: identity keys + flattened fields.

class civilpy.structural.bim_spec.FieldSpliceRecord(station_ft: float, flange_width_left_in: float, flange_width_right_in: float, flange_thickness_in: float, web_depth_in: float, web_thickness_left_in: float, web_thickness_right_in: float, flange_clearance_in: float = 1.0, splice_plate_thickness_in: float = 0.5, bolt_diameter_in: float = 0.875, bolt_rows: int = 2, bolt_cols: int = 3, bolt_fu_ksi: float = 120.0, plate_fy_ksi: float = 50.0, plate_fu_ksi: float = 65.0)[source]

Bases: SpecRecord

A bolted field splice on the girder (6.13.6 family): location, the flange/web splice-plate schedule as the checks size it, and the bolt group. Ties into the NSBA splice designer and the girder->splice gdr.* contract.

bolt_cols: int = 3
bolt_diameter_in: float = 0.875
bolt_fu_ksi: float = 120.0
bolt_rows: int = 2
flange_clearance_in: float = 1.0
flange_thickness_in: float
flange_width_left_in: float
flange_width_right_in: float
plate_fu_ksi: float = 65.0
plate_fy_ksi: float = 50.0
splice_plate_thickness_in: float = 0.5
station_ft: float
web_depth_in: float
web_thickness_left_in: float
web_thickness_right_in: float
class civilpy.structural.bim_spec.FootingRecord(length_ft: float, width_ft: float, thickness_ft: float)[source]

Bases: SpecRecord

Spread-footing plan dims — a geotech deliverable, explicit inputs (mirrors FootingSpec).

length_ft: float
thickness_ft: float
width_ft: float
civilpy.structural.bim_spec.GIRDER_CHECK_INPUTS: dict[str, dict[str, tuple]] = {'6.10.10.1.2': {'d_stud': ('field', 'composite.stud_diameter_in'), 'n_per_row': ('field', 'composite.studs_per_row'), 'pitch': ('field', 'composite.pitch_in'), 'shear_flow': ('loads', 'fatigue horizontal shear flow at the section')}, '6.10.10.2': {'d_stud': ('field', 'composite.stud_diameter_in'), 'n_cycles': ('loads', 'design fatigue cycles N (None = Fatigue I)')}, '6.10.10.3': {'d_stud': ('field', 'composite.stud_diameter_in'), 'flange_width_in': ('derived', ('section.label', 'section.top_flange_width_in'), 'top flange width'), 'gauge_in': ('field', 'composite.stud_gauge_in'), 'n_per_row': ('field', 'composite.studs_per_row')}, '6.10.10.4': {'d_stud': ('field', 'composite.stud_diameter_in'), 'e_c': ('derived', ('composite.deck_fc_ksi',), 'deck modulus from f_c (C5.4.2.4)'), 'f_c': ('field', 'composite.deck_fc_ksi')}, '6.10.11.1.2': {'b_f': ('derived', ('section.label', 'section.top_flange_width_in'), 'widest compression flange in the field section'), 'b_t': ('field', 'transverse_stiffener.plate_width_in'), 'd_web': ('derived', ('section.label', 'section.web_depth_in'), 'web depth'), 't_p': ('field', 'transverse_stiffener.plate_thickness_in')}, '6.10.11.1.3': {'b_t': ('field', 'transverse_stiffener.plate_width_in'), 'd_o': ('field', 'transverse_stiffener.spacing_in'), 'd_web': ('derived', ('section.label', 'section.web_depth_in'), 'web depth'), 'f_ys': ('field', 'transverse_stiffener.fy_ksi'), 'f_yw': ('field', 'section.fyw_ksi'), 'moment_of_inertia': ('derived', ('transverse_stiffener.plate_width_in', 'transverse_stiffener.plate_thickness_in', 'transverse_stiffener.single_sided'), 'I_t about the web face (single) or mid-thickness (pair)'), 't_p': ('field', 'transverse_stiffener.plate_thickness_in'), 't_w': ('derived', ('section.label', 'section.web_thickness_in'), 'web thickness')}, '6.10.11.2.2': {'b_t': ('field', 'bearing_stiffener.plate_width_in'), 'f_ys': ('field', 'bearing_stiffener.fy_ksi'), 't_p': ('field', 'bearing_stiffener.plate_thickness_in')}, '6.10.11.2.3': {'a_pn': ('derived', ('bearing_stiffener.plate_width_in', 'bearing_stiffener.plate_thickness_in', 'bearing_stiffener.pairs'), 'net projecting bearing area'), 'f_ys': ('field', 'bearing_stiffener.fy_ksi')}, '6.10.11.2.4': {'b_t': ('field', 'bearing_stiffener.plate_width_in'), 'd_web': ('derived', ('section.label', 'section.web_depth_in'), 'web depth'), 'f_ys': ('field', 'bearing_stiffener.fy_ksi'), 'p_u': ('loads', 'factored bearing reaction'), 'pairs': ('field', 'bearing_stiffener.pairs'), 't_p': ('field', 'bearing_stiffener.plate_thickness_in'), 't_w': ('derived', ('section.label', 'section.web_thickness_in'), 'web thickness')}, '6.10.11.3': {'d_o': ('derived', ('transverse_stiffener.spacing_in',), 'panel spacing (uniform default if no transverse stiffener)'), 'd_web': ('derived', ('section.label', 'section.web_depth_in'), 'web depth'), 'f_yc': ('field', 'section.fyc_ksi'), 'f_ys': ('field', 'longitudinal_stiffener.fy_ksi'), 'moment_of_inertia': ('derived', ('longitudinal_stiffener.plate_width_in', 'longitudinal_stiffener.plate_thickness_in'), 'I_l of the plate about the web face'), 'proj_width': ('field', 'longitudinal_stiffener.plate_width_in'), 'radius_of_gyration': ('derived', ('longitudinal_stiffener.plate_width_in', 'longitudinal_stiffener.plate_thickness_in'), 'r = sqrt(I_l/A) of the stiffener'), 't_s': ('field', 'longitudinal_stiffener.plate_thickness_in'), 't_w': ('derived', ('section.label', 'section.web_thickness_in'), 'web thickness')}, '6.10.8.1.1': {'f_bu': ('loads', 'factored flange stress'), 'f_l': ('loads', 'flange lateral bending stress'), 'f_nc': ('derived', ('section.label', 'section.top_flange_width_in', 'section.top_flange_thickness_in', 'cross_frame.spacing_ft'), 'Fnc from 6.10.8.2.1')}, '6.10.8.1.3': {'f_bu': ('loads', 'factored flange stress'), 'f_yf': ('field', 'section.fyc_ksi')}, '6.10.8.2.1': {'b_fc': ('derived', ('section.label', 'section.top_flange_width_in'), 'compression-flange width'), 'd_c': ('derived', ('section.label', 'section.web_depth_in'), 'depth of web in compression'), 'f_yc': ('field', 'section.fyc_ksi'), 'f_yw': ('field', 'section.fyw_ksi'), 'l_b': ('derived', ('cross_frame.stations_ft', 'cross_frame.spacing_ft'), 'governing unbraced length (max bay)'), 't_fc': ('derived', ('section.label', 'section.top_flange_thickness_in'), 'compression-flange thickness'), 't_w': ('derived', ('section.label', 'section.web_thickness_in'), 'web thickness')}, '6.10.8.2.2': {'b_fc': ('derived', ('section.label', 'section.top_flange_width_in'), 'compression-flange width from the catalog DB or plates'), 'f_yc': ('field', 'section.fyc_ksi'), 'f_yw': ('field', 'section.fyw_ksi'), 't_fc': ('derived', ('section.label', 'section.top_flange_thickness_in'), 'compression-flange thickness')}, '6.10.8.2.3': {'b_fc': ('derived', ('section.label', 'section.top_flange_width_in'), 'compression-flange width'), 'd_c': ('derived', ('section.label', 'section.web_depth_in'), 'depth of web in compression'), 'f_yc': ('field', 'section.fyc_ksi'), 'f_yw': ('field', 'section.fyw_ksi'), 'l_b': ('derived', ('cross_frame.stations_ft', 'cross_frame.spacing_ft'), 'governing unbraced length = max bay (CrossFrameRecord.max_bay_ft), from the per-bay stations or uniform spacing'), 't_fc': ('derived', ('section.label', 'section.top_flange_thickness_in'), 'compression-flange thickness'), 't_w': ('derived', ('section.label', 'section.web_thickness_in'), 'web thickness')}, '6.10.9': {'d_web': ('derived', ('section.label', 'section.web_depth_in'), 'web depth'), 'f_yw': ('field', 'section.fyw_ksi'), 't_w': ('derived', ('section.label', 'section.web_thickness_in'), 'web thickness')}, '6.13.5.3': {'a_vg': ('derived', ('splices.web_depth_in', 'splices.splice_plate_thickness_in'), 'gross shear area of the web splice plate'), 'a_vn': ('derived', ('splices.web_depth_in', 'splices.splice_plate_thickness_in', 'splices.bolt_diameter_in', 'splices.bolt_rows'), 'net shear area through the bolt line'), 'f_u': ('field', 'splices.plate_fu_ksi'), 'f_y': ('field', 'splices.plate_fy_ksi')}, '6.13.6.1.3b': {'flange_thickness': ('field', 'splices.flange_thickness_in'), 'flange_width_left': ('field', 'splices.flange_width_left_in'), 'flange_width_right': ('field', 'splices.flange_width_right_in'), 'web_thickness_left': ('field', 'splices.web_thickness_left_in'), 'web_thickness_right': ('field', 'splices.web_thickness_right_in')}, '6.13.6.1.3c': {'flange_clearance': ('field', 'splices.flange_clearance_in'), 'web_depth': ('field', 'splices.web_depth_in'), 'web_thickness': ('field', 'splices.web_thickness_left_in'), 'web_thickness_other': ('field', 'splices.web_thickness_right_in')}, '6.13.6.1.4': {'a_f': ('derived', ('splices.flange_width_left_in', 'splices.flange_thickness_in'), 'smaller-side flange area'), 'a_p': ('derived', ('splices.flange_width_left_in', 'splices.splice_plate_thickness_in'), 'splice plate area')}, '6.7.4.2.2': {'brace_stiffness': ('derived', ('cross_frame.member_shape', 'cross_frame.member_length_ft', 'cross_frame.connection_plate_thickness_in'), 'provided cross-frame torsional stiffness'), 'i_eff': ('derived', ('section.label', 'section.top_flange_width_in', 'section.top_flange_thickness_in'), 'effective lateral moment of inertia'), 'l_span': ('derived', ('length_ft',), 'girder span/length'), 'm_r': ('loads', 'required flexural strength Mr at the brace'), 'n_braces': ('derived', ('cross_frame.stations_ft', 'cross_frame.spacing_ft'), 'number of intermediate brace points')}, '6.8.2.1': {'a_g': ('derived', ('cross_frame.member_shape',), 'AISC member gross area'), 'f_y': ('field', 'cross_frame.fy_ksi')}, '6.9.4.1.1': {'a_g': ('derived', ('cross_frame.member_shape',), 'AISC member gross area'), 'f_y': ('field', 'cross_frame.fy_ksi'), 'kl_over_r': ('derived', ('cross_frame.member_shape', 'cross_frame.member_length_ft'), 'K*L/r_y from the section and work length')}, '6.9.4.2.1': {'b': ('derived', ('cross_frame.member_shape',), 'outstanding leg width from the AISC label'), 'f_y': ('field', 'cross_frame.fy_ksi'), 't': ('derived', ('cross_frame.member_shape',), 'leg thickness from the AISC label')}}

The check-coverage map for a steel girder line (build plan §3, applied to §3a). Aggregates every ported LRFD check that consumes some part of the girder and where its inputs live. ("loads", ...) marks a demand from the line-girder / grillage analysis, not a stored field. The coverage test asserts every required parameter of every listed check resolves to a path on SteelGirderRecord. Not-yet-ported checks (6.7.4 cross-frame spacing, 6.10.11.3 longitudinal stiffener) are deliberately absent – their fields are quantity space until the article lands.

class civilpy.structural.bim_spec.GirderSectionRecord(label: str | None = None, web_depth_in: float | None = None, web_thickness_in: float | None = None, top_flange_width_in: float | None = None, top_flange_thickness_in: float | None = None, bot_flange_width_in: float | None = None, bot_flange_thickness_in: float | None = None, fyc_ksi: float = 50.0, fyw_ksi: float = 50.0)[source]

Bases: SpecRecord

The girder cross-section the sectional checks run on: EITHER a cataloged rolled shape (label) OR an explicit built-up plate section. This is the single controlling section (the design’s governing positive-moment / pier section); the whole variable-depth schedule lives in SteelGirderRecord.plates. Give exactly one representation – a label or the full set of plate dims – and resolve() returns the (d, t_w, b_fc, t_fc) the checks need from whichever is present.

bot_flange_thickness_in: float | None = None
bot_flange_width_in: float | None = None
fyc_ksi: float = 50.0
fyw_ksi: float = 50.0
label: str | None = None
resolve() tuple[float, float, float, float][source]

(depth, web_thickness, top_flange_width, top_flange_thickness) in inches – from the AISC catalog when a label is stored, else the explicit plate dims.

top_flange_thickness_in: float | None = None
top_flange_width_in: float | None = None
web_depth_in: float | None = None
web_thickness_in: float | None = None
civilpy.structural.bim_spec.HAMMERHEAD_CHECK_INPUTS: dict[str, dict[str, tuple]] = {'4.5.3.2.2b': {'m_2': ('loads', 'larger end moment'), 'p_e': ('derived', ('pier_stem.b_in', 'pier_stem.h_in', 'pier_stem.diameter_in', 'pier_stem.height_ft', 'pier_stem.fixity', 'pier_stem.fc_ksi'), 'Euler load from cracked EI and effective length'), 'p_u': ('loads', 'factored axial')}, '5.10.8.2.1': {'d_b': ('derived', ('pier_cap.tie_bar_size',), 'bar diameter'), 'f_c': ('field', 'pier_cap.fc_ksi'), 'f_y': ('field', 'pier_cap.fy_ksi')}, '5.6.4.2': {'a_g': ('derived', ('pier_stem.b_in', 'pier_stem.h_in', 'pier_stem.diameter_in'), 'gross area'), 'a_st': ('field', 'pier_stem.bars_area_in2'), 'f_c': ('field', 'pier_stem.fc_ksi'), 'f_y': ('field', 'pier_stem.fy_ksi')}, '5.6.4.4': {'a_g': ('derived', ('pier_stem.b_in', 'pier_stem.h_in', 'pier_stem.diameter_in'), 'gross area'), 'a_st': ('field', 'pier_stem.bars_area_in2'), 'f_c': ('field', 'pier_stem.fc_ksi'), 'f_y': ('field', 'pier_stem.fy_ksi'), 'p_u': ('loads', 'factored axial from the cap reactions'), 'spiral': ('field', 'pier_stem.spiral')}, '5.6.4.5 check': {'b': ('field', 'pier_stem.b_in'), 'diameter': ('field', 'pier_stem.diameter_in'), 'f_c': ('field', 'pier_stem.fc_ksi'), 'f_y': ('field', 'pier_stem.fy_ksi'), 'h': ('field', 'pier_stem.h_in'), 'layers': ('derived', ('pier_stem.bars_area_in2', 'detailing.cover_in', 'pier_stem.h_in', 'pier_stem.diameter_in'), 'stem_rebar_layers()'), 'm_u': ('loads', 'factored (magnified) moment'), 'p_u': ('loads', 'factored axial'), 'spiral': ('field', 'pier_stem.spiral')}, '5.6.7': {'d_c': ('derived', ('detailing.cover_in', 'pier_cap.tie_bar_size'), 'cover + half bar diameter'), 'f_ss': ('loads', 'service tie-steel stress'), 'h': ('field', 'pier_cap.depth_ft')}, '5.7.2.5': {'a_v': ('derived', ('detailing.stirrup_size',), 'stirrup legs area'), 'b_v': ('field', 'pier_cap.thickness_ft'), 'f_c': ('field', 'pier_cap.fc_ksi'), 's': ('field', 'detailing.stirrup_spacing_in')}, '5.7.2.6': {'b_v': ('field', 'pier_cap.thickness_ft'), 'd_v': ('derived', ('pier_cap.depth_ft', 'detailing.cover_in', 'pier_cap.tie_bar_size'), 'effective shear depth from the section'), 'f_c': ('field', 'pier_cap.fc_ksi'), 'v_u': ('loads', 'factored shear at the section')}, '5.7.3.3': {'a_v': ('derived', ('detailing.stirrup_size',), 'stirrup legs area'), 'b_v': ('field', 'pier_cap.thickness_ft'), 'd_v': ('derived', ('pier_cap.depth_ft', 'detailing.cover_in', 'pier_cap.tie_bar_size'), 'effective shear depth from the section'), 'f_c': ('field', 'pier_cap.fc_ksi'), 'f_y': ('field', 'pier_cap.fy_ksi'), 's': ('field', 'detailing.stirrup_spacing_in'), 'v_u': ('loads', 'factored shear at the section')}, '5.8.2.4': {'a_st': ('derived', ('pier_cap.tie_bar_size', 'pier_cap.tie_bar_count'), 'bar area x count'), 'f_y': ('field', 'pier_cap.fy_ksi'), 'p_u': ('loads', 'governing tie force from the STM solve')}, '5.8.2.5': {'a_cn': ('derived', ('pier_cap.thickness_ft', 'detailing.seat_side_in'), 'node face area: seat side x min(cap width, seat side)'), 'f_c': ('field', 'pier_cap.fc_ksi'), 'p_u': ('loads', 'strut / node face force from the STM solve')}, '5.8.2.6': {'a_s_horizontal': ('derived', ('detailing.skin_bar_size',), 'two-face skin bar area'), 'a_s_vertical': ('derived', ('detailing.stirrup_size',), 'stirrup legs area'), 'b_w': ('field', 'pier_cap.thickness_ft'), 's_h': ('field', 'detailing.stirrup_spacing_in'), 's_v': ('field', 'detailing.skin_bar_spacing_in')}}

How each LRFD check consuming a hammerhead resolves its inputs. Values are ("field", "<dotted record path>") for a direct read, ("derived", (<paths>...), "<how>") for a value computed from record fields, and ("loads", "<what>") for demand-side inputs that come from the superstructure reactions / analysis, not the element record. tests/structural/test_bim_spec.py asserts every required parameter of every listed check is mapped and every mapped path exists on the record schema — a gap is a failing test, i.e. the live backlog of missing parameters.

class civilpy.structural.bim_spec.HammerheadPierRecord(pier_cap: PierCapRecord, pier_stem: PierStemRecord, footing: FootingRecord | None = None, detailing: CapDetailingRecord | None = None, standard: str | None = None, standard_year: int | None = None, provenance: Provenance | None = None)[source]

Bases: ElementRecord

One hammerhead pier as a storable parametric record — the Phase-1 vertical slice of the queryable-BIM schema. standard + standard_year key the standards-catalog defaults lookup; build() reconstitutes the geometry-builder spec so a record read back from storage regenerates placement, the tagged .3dm emit, and the check inputs in HAMMERHEAD_CHECK_INPUTS.

BIM_TYPE = 'pier'

identity keys stamped into the flattened document

SUBTYPE = 'hammerhead'
build(layout, unit, **frame_kw)[source]

Place the pier under layout — the same contract as the specs assemble_substructure() consumes, so a stored record slots straight into the emit pipeline.

detailing: CapDetailingRecord | None = None
footing: FootingRecord | None = None
pier_cap: PierCapRecord
pier_stem: PierStemRecord
provenance: Provenance | None = None
standard: str | None = None
standard_year: int | None = None
to_cap_design()[source]

See _cap_design().

to_pier_column()[source]

See _pier_column().

class civilpy.structural.bim_spec.LongitudinalStiffenerRecord(plate_width_in: float, plate_thickness_in: float, location_from_top_flange_in: float, fy_ksi: float = 50.0)[source]

Bases: SpecRecord

Longitudinal web stiffener on a deep web. 6.10.11.3 is not yet in the ported library, so this is geometry/quantity space; its check-coverage entry lands when the article ports (the WingwallRecord fallback).

fy_ksi: float = 50.0
location_from_top_flange_in: float
plate_thickness_in: float
plate_width_in: float
civilpy.structural.bim_spec.PILE_BENT_CHECK_INPUTS: dict[str, dict[str, tuple]] = {'5.10.8.2.1': {'d_b': ('derived', ('pier_cap.tie_bar_size',), 'bar diameter'), 'f_c': ('field', 'pier_cap.fc_ksi'), 'f_y': ('field', 'pier_cap.fy_ksi')}, '5.6.7': {'d_c': ('derived', ('detailing.cover_in', 'pier_cap.tie_bar_size'), 'cover + half bar diameter'), 'f_ss': ('loads', 'service tie-steel stress'), 'h': ('field', 'pier_cap.depth_ft')}, '5.7.2.5': {'a_v': ('derived', ('detailing.stirrup_size',), 'stirrup legs area'), 'b_v': ('field', 'pier_cap.thickness_ft'), 'f_c': ('field', 'pier_cap.fc_ksi'), 's': ('field', 'detailing.stirrup_spacing_in')}, '5.7.2.6': {'b_v': ('field', 'pier_cap.thickness_ft'), 'd_v': ('derived', ('pier_cap.depth_ft', 'detailing.cover_in', 'pier_cap.tie_bar_size'), 'effective shear depth from the section'), 'f_c': ('field', 'pier_cap.fc_ksi'), 'v_u': ('loads', 'factored shear at the section')}, '5.7.3.3': {'a_v': ('derived', ('detailing.stirrup_size',), 'stirrup legs area'), 'b_v': ('field', 'pier_cap.thickness_ft'), 'd_v': ('derived', ('pier_cap.depth_ft', 'detailing.cover_in', 'pier_cap.tie_bar_size'), 'effective shear depth from the section'), 'f_c': ('field', 'pier_cap.fc_ksi'), 'f_y': ('field', 'pier_cap.fy_ksi'), 's': ('field', 'detailing.stirrup_spacing_in'), 'v_u': ('loads', 'factored shear at the section')}, '5.8.2.4': {'a_st': ('derived', ('pier_cap.tie_bar_size', 'pier_cap.tie_bar_count'), 'bar area x count'), 'f_y': ('field', 'pier_cap.fy_ksi'), 'p_u': ('loads', 'governing tie force from the STM solve')}, '5.8.2.5': {'a_cn': ('derived', ('pier_cap.thickness_ft', 'detailing.seat_side_in'), 'node face area: seat side x min(cap width, seat side)'), 'f_c': ('field', 'pier_cap.fc_ksi'), 'p_u': ('loads', 'strut / node face force from the STM solve')}, '5.8.2.6': {'a_s_horizontal': ('derived', ('detailing.skin_bar_size',), 'two-face skin bar area'), 'a_s_vertical': ('derived', ('detailing.stirrup_size',), 'stirrup legs area'), 'b_w': ('field', 'pier_cap.thickness_ft'), 's_h': ('field', 'detailing.stirrup_spacing_in'), 's_v': ('field', 'detailing.skin_bar_spacing_in')}, '6.9.4.1.1': {'a_g': ('derived', ('piles.shape',), 'AISC HP section area'), 'f_y': ('field', 'piles.fy_ksi'), 'kl_over_r': ('derived', ('piles.shape', 'piles.unbraced_length_ft'), 'K*Lu/r_y from the section and exposed length'), 'p_u': ('loads', 'factored pile reaction')}}

the cap family plus the pile compression check.

Type:

Pile bent

class civilpy.structural.bim_spec.PierCapRecord(span_ft: float, depth_ft: float, thickness_ft: float, tie_bar_size: int, tie_bar_count: int, tie_at_top: bool = True, tip_depth_ft: float | None = None, fc_ksi: float = 4.0, fy_ksi: float = 60.0)[source]

Bases: SpecRecord

The executed hammerhead cap design as parameters: overall envelope plus the governing STM tie’s bar schedule (the outputs of optimize_pier_cap() that geometry and checks consume — the sweep itself is not stored).

depth_ft: float
fc_ksi: float = 4.0
fy_ksi: float = 60.0
span_ft: float
thickness_ft: float
tie_at_top: bool = True
tie_bar_count: int
tie_bar_size: int
tip_depth_ft: float | None = None
class civilpy.structural.bim_spec.PierStemRecord(height_ft: float, bars_area_in2: float, b_in: float | None = None, h_in: float | None = None, diameter_in: float | None = None, bar_size: int = 9, fc_ksi: float = 4.0, fy_ksi: float = 60.0, spiral: bool = False, fixity: str = 'fixed-fixed')[source]

Bases: SpecRecord

The hammerhead stem (single column): section, clear height, and the longitudinal steel the executed design carries. Rectangular b_in x h_in (b along the cap) or circular diameter_in.

b_in: float | None = None
bar_size: int = 9
bars_area_in2: float
diameter_in: float | None = None
fc_ksi: float = 4.0
fixity: str = 'fixed-fixed'
fy_ksi: float = 60.0
h_in: float | None = None
height_ft: float
spiral: bool = False
class civilpy.structural.bim_spec.PileBentRecord(pier_cap: PierCapRecord, piles: PileRecord, detailing: CapDetailingRecord | None = None, standard: str | None = None, standard_year: int | None = None, provenance: Provenance | None = None)[source]

Bases: ElementRecord

One capped-pile pier (pile bent): the cap directly on driven HP piles — the CPP-1-08 pattern. The cap design’s supports are the piles, so the governing tie is normally the bottom chord.

BIM_TYPE = 'pier'
SUBTYPE = 'pile_bent'
build(layout, unit, **frame_kw)[source]
detailing: CapDetailingRecord | None = None
pier_cap: PierCapRecord
piles: PileRecord
provenance: Provenance | None = None
standard: str | None = None
standard_year: int | None = None
class civilpy.structural.bim_spec.PileRecord(xs_ft: tuple[float, ...], shape: str = 'HP12X53', length_ft: float = 40.0, fy_ksi: float = 50.0, unbraced_length_ft: float = 0.0)[source]

Bases: SpecRecord

A driven HP-pile group under a cap: positions along the support line (girder-1 frame, same as the cap design’s supports), the AISC section, and the structural-check inputs. Pay length is the driven length from the geotech recommendation.

fy_ksi: float = 50.0
length_ft: float = 40.0
shape: str = 'HP12X53'
unbraced_length_ft: float = 0.0
xs_ft: tuple[float, ...]
class civilpy.structural.bim_spec.PlateSegmentRecord(x_start_ft: float, x_end_ft: float, web_depth_in: float, web_thickness_in: float, top_flange_width_in: float, top_flange_thickness_in: float, bot_flange_width_in: float, bot_flange_thickness_in: float)[source]

Bases: SpecRecord

One station-ranged built-up girder segment: the web + flange plates over [x_start_ft, x_end_ft]. Variable depth rides on web_depth_in varying between segments; flange transitions on the flange plate dims. Geometry/quantity space for the mesh emit – the sectional checks run on the single GirderSectionRecord.

bot_flange_thickness_in: float
bot_flange_width_in: float
top_flange_thickness_in: float
top_flange_width_in: float
web_depth_in: float
web_thickness_in: float
x_end_ft: float
x_start_ft: float
class civilpy.structural.bim_spec.Provenance(source: str = 'manual', field_sources: dict | None = None, doc_id: str | None = None, sheet: str | None = None)[source]

Bases: SpecRecord

Where this element’s parameters came from. source is the element-level tier; field_sources overrides it per field (dotted record path -> tier) for manually-entered values; doc_id / sheet point into the plans document store when source="plans" so a plan revision flags exactly which fields to re-verify.

doc_id: str | None = None
field_sources: dict | None = None
sheet: str | None = None
source: str = 'manual'
civilpy.structural.bim_spec.SCHEMA_VERSION = 1

Bump when a saved document’s meaning changes (not when a field is merely added — additive keys are the point of the JSON substrate).

civilpy.structural.bim_spec.SEAT_ABUTMENT_CHECK_INPUTS: dict[str, dict[str, tuple]] = {'5.10.8.2.1': {'d_b': ('derived', ('cap.tie_bar_size',), 'bar diameter'), 'f_c': ('field', 'cap.fc_ksi'), 'f_y': ('field', 'cap.fy_ksi')}, '5.6.7': {'d_c': ('derived', ('detailing.cover_in', 'cap.tie_bar_size'), 'cover + half bar diameter'), 'f_ss': ('loads', 'service tie-steel stress'), 'h': ('field', 'cap.depth_ft')}, '5.7.2.5': {'a_v': ('derived', ('detailing.stirrup_size',), 'stirrup legs area'), 'b_v': ('field', 'cap.thickness_ft'), 'f_c': ('field', 'cap.fc_ksi'), 's': ('field', 'detailing.stirrup_spacing_in')}, '5.7.2.6': {'b_v': ('field', 'cap.thickness_ft'), 'd_v': ('derived', ('cap.depth_ft', 'detailing.cover_in', 'cap.tie_bar_size'), 'effective shear depth from the section'), 'f_c': ('field', 'cap.fc_ksi'), 'v_u': ('loads', 'factored shear at the section')}, '5.7.3.3': {'a_v': ('derived', ('detailing.stirrup_size',), 'stirrup legs area'), 'b_v': ('field', 'cap.thickness_ft'), 'd_v': ('derived', ('cap.depth_ft', 'detailing.cover_in', 'cap.tie_bar_size'), 'effective shear depth from the section'), 'f_c': ('field', 'cap.fc_ksi'), 'f_y': ('field', 'cap.fy_ksi'), 's': ('field', 'detailing.stirrup_spacing_in'), 'v_u': ('loads', 'factored shear at the section')}, '5.8.2.4': {'a_st': ('derived', ('cap.tie_bar_size', 'cap.tie_bar_count'), 'bar area x count'), 'f_y': ('field', 'cap.fy_ksi'), 'p_u': ('loads', 'governing tie force from the STM solve')}, '5.8.2.5': {'a_cn': ('derived', ('cap.thickness_ft', 'detailing.seat_side_in'), 'node face area: seat side x min(cap width, seat side)'), 'f_c': ('field', 'cap.fc_ksi'), 'p_u': ('loads', 'strut / node face force from the STM solve')}, '5.8.2.6': {'a_s_horizontal': ('derived', ('detailing.skin_bar_size',), 'two-face skin bar area'), 'a_s_vertical': ('derived', ('detailing.stirrup_size',), 'stirrup legs area'), 'b_w': ('field', 'cap.thickness_ft'), 's_h': ('field', 'detailing.stirrup_spacing_in'), 's_v': ('field', 'detailing.skin_bar_spacing_in')}, '6.9.4.1.1': {'a_g': ('derived', ('piles.shape',), 'AISC HP section area'), 'f_y': ('field', 'piles.fy_ksi'), 'kl_over_r': ('derived', ('piles.shape', 'piles.unbraced_length_ft'), 'K*Lu/r_y from the section and exposed length'), 'p_u': ('loads', 'factored pile reaction')}}

cap family (paths under cap.) plus piles. The backwall / wingwall (Section 11) checks are not yet in the ported library — their fields are geometry/quantity space until they land (the §3 fallback), at which point they join this map.

Type:

Seat abutment

civilpy.structural.bim_spec.SOURCES = ('brr', 'plans', 'manual', 'snbi', 'inferred')

Provenance tiers, element-level (decision 3 of the build plan): per-field overrides ride in Provenance.field_sources only for manually-entered values where a re-derivation trigger pays off. snbi = seeded from the inventory columns; inferred = resolved through the era-standards registry (the standard-default path) — both mark records the Tier A campaign populates without a plan in hand.

class civilpy.structural.bim_spec.SeatAbutmentRecord(cap: PierCapRecord, piles: PileRecord, backwall_thickness_in: float = 18.0, wingwall: WingwallRecord | None = None, detailing: CapDetailingRecord | None = None, standard: str | None = None, standard_year: int | None = None, provenance: Provenance | None = None)[source]

Bases: ElementRecord

One conventional capped-pile seat abutment: the cap on driven piles, the backwall up to the low deck edge, and optional wingwalls. The semi-integral and integral variants get their own subtypes when they land (their geometry builders already exist).

BIM_TYPE = 'abutment'
SUBTYPE = 'seat'
backwall_thickness_in: float = 18.0
build(layout, unit, **frame_kw)[source]
cap: PierCapRecord
detailing: CapDetailingRecord | None = None
piles: PileRecord
provenance: Provenance | None = None
standard: str | None = None
standard_year: int | None = None
to_abutment_spec()[source]

The layout-side AbutmentSpec (wingwall reconstituted as the 4-attribute wall the geometry builder reads).

wingwall: WingwallRecord | None = None
class civilpy.structural.bim_spec.SlabBridgeRecord(span_ft: int, width_ft: float, skew_deg: float = 0.0, edge_condition: str = 'over_the_side', standard: str | None = None, standard_year: int | None = None, provenance: Provenance | None = None)[source]

Bases: ElementRecord

One single-span ODOT standard slab bridge (SB-1-24 series): the SlabBridgeInput as a storable record. The slab thickness and the full A/B/M/N bar schedule come from the standard’s design table at emit time — they are the standard’s content, so they are never stored here (design rule: never copy what the era registry derives).

BIM_TYPE = 'bridge'
SUBTYPE = 'slab'
edge_condition: str = 'over_the_side'
provenance: Provenance | None = None
skew_deg: float = 0.0
span_ft: int
standard: str | None = None
standard_year: int | None = None
to_input()[source]

The SlabBridgeInput the emit engine consumes.

width_ft: float
class civilpy.structural.bim_spec.SpecRecord[source]

Bases: object

Validation mixin: the type guarantor in front of a schema-free JSON store. Checks every field against its annotation and its spec_field metadata (enum membership, gt/ge bounds), recursing into nested records. Returns problem strings; validate(strict=True) raises instead.

validate(*, strict: bool = False, _prefix: str = '') list[str][source]
class civilpy.structural.bim_spec.SteelGirderRecord(section: GirderSectionRecord, length_ft: float, grade: str = 'Grade 50', plates: tuple[PlateSegmentRecord, ...] | None = None, composite: CompositeRecord | None = None, transverse_stiffener: TransverseStiffenerRecord | None = None, bearing_stiffener: BearingStiffenerRecord | None = None, longitudinal_stiffener: LongitudinalStiffenerRecord | None = None, cross_frame: CrossFrameRecord | None = None, splices: tuple[FieldSpliceRecord, ...] | None = None, standard: str | None = None, standard_year: int | None = None, provenance: Provenance | None = None)[source]

Bases: ElementRecord

One steel girder line as a storable parametric record – the Phase-6a superstructure schema space. section is the controlling section (a catalog label or a built-up plate section); the optional customization records give cross-frames, stiffeners, the composite block, and a field splice somewhere to live. plates carries the full variable-depth schedule for the mesh emit. The check inputs are in GIRDER_CHECK_INPUTS.

BIM_TYPE = 'girder'
SUBTYPE = 'steel'
bearing_stiffener: BearingStiffenerRecord | None = None
composite: CompositeRecord | None = None
cross_frame: CrossFrameRecord | None = None
grade: str = 'Grade 50'
length_ft: float
longitudinal_stiffener: LongitudinalStiffenerRecord | None = None
plates: tuple[PlateSegmentRecord, ...] | None = None
provenance: Provenance | None = None
section: GirderSectionRecord
splices: tuple[FieldSpliceRecord, ...] | None = None
standard: str | None = None
standard_year: int | None = None
to_bridge_input(*, girder_count: int, girder_spacing_ft: float, overhang_ft: float, spans_ft=None, **overrides)[source]

Reconstitute the BridgeInput the superstructure emit consumes today. Only a cataloged section.label maps into the current prismatic engine; a plate schedule and the stiffener/cross-frame/splice records are carried for the emit extension (§3a follow-on) and raise here until it lands.

transverse_stiffener: TransverseStiffenerRecord | None = None
class civilpy.structural.bim_spec.TransverseStiffenerRecord(plate_width_in: float, plate_thickness_in: float, spacing_in: float, single_sided: bool = False, fy_ksi: float = 50.0)[source]

Bases: SpecRecord

Transverse web stiffener plates on the governing shear zone: dims + the panel spacing d_o that turns on 6.10.9’s stiffened-panel path. The record’s presence means a stiffened web (absent = the unstiffened default).

fy_ksi: float = 50.0
plate_thickness_in: float
plate_width_in: float
single_sided: bool = False
spacing_in: float
class civilpy.structural.bim_spec.WingwallRecord(length_ft: float, stem_height_ft: float, stem_thickness_ft: float, base_width_ft: float, footing_thickness_ft: float)[source]

Bases: SpecRecord

Wingwall panel dimensions — the executed RetainingWall stem/footing as stored parameters, plus the run along the roadway. The Section-11 wall checks are not yet in the ported library, so this record is geometry/quantity space; its check-coverage entries land when they port (the §3 fallback: manual enumeration until the check exists).

base_width_ft: float
footing_thickness_ft: float
length_ft: float
stem_height_ft: float
stem_thickness_ft: float
civilpy.structural.bim_spec.abutment_metrics(geom) dict[source]

Derived metrics for one placed abutment (AbutmentGeometry) — same emit-time sidecar contract as pier_metrics().

civilpy.structural.bim_spec.girder_metrics(record: SteelGirderRecord) dict[source]

Derived, JSON-safe metrics for the sidecar/generated columns: the query-hot superstructure numbers, computed from the record (not remeasured off the mesh) the way pier_metrics is.

civilpy.structural.bim_spec.pier_metrics(geom) dict[source]

Derived metrics for one placed pier (PierGeometry), reported at emit time — civilpy knows the semantic element, so this is cheaper and safer than re-measuring meshes. The application side lands these next to the JSONB record and promotes the query-hot ones to generated columns.

civilpy.structural.bim_spec.record_from_dict(cls, data: dict)[source]

Rebuild cls from record_to_dict() output (tuples restored, nested records recursed via the type hints — the single schema).

civilpy.structural.bim_spec.record_paths(cls) set[str][source]

Every dotted field path reachable on a record class (Optionals unwrapped, nested records recursed) — the schema surface the coverage test resolves against.

civilpy.structural.bim_spec.record_to_dict(rec) dict[source]

Flatten a record to JSON-safe primitives (nested dicts/lists).

civilpy.structural.bim_spec.select_box(span_ft: float, *, composite: bool = True) str | None[source]

The shallowest PSBDD-1-25 box designation whose cataloged span range covers span_ft (48-in widths; the standard’s selection logic), or None when no box reaches — spans are cataloged in 5-ft steps, so the lookup rounds up to the next step.

civilpy.structural.bim_spec.spec_field(default=<dataclasses._MISSING_TYPE object>, *, unit: str | None = None, desc: str | None = None, enum: tuple | None = None, gt: float | None = None, ge: float | None = None, checks: tuple[str, ...]=())[source]

A dataclass field carrying its engineering metadata: unit, bounds (gt/ge), enum options, and the article numbers of the checks consuming it. The metadata rides on the one field declaration — validation and the coverage test both read it here.

civilpy.structural.bim_spec.stem_rebar_layers(record: HammerheadPierRecord, n_layers: int = 2)[source]

Break the stored stem steel into strain-compatibility layers for the P-M checks: half at each face, cover from the extreme fibers (the conventional two-layer idealization of a ring).

civilpy.structural.box_beam_pipeline module

L1 verification of the ODOT standard box-beam designs.

The PSBDD-1-25 tables are pre-engineered designs; this module is the pure-Python gate that re-derives the governing checks for one design line — the box-beam analog of the steel line-girder envelope:

  • HL-93 demands from the same girder_line_envelope() influence-line machinery the steel slice uses, distributed with the adjacent-box factors (LRFD 4.6.2.2.2b/3c, type “g” cross-section) — torsion constant from the thin-wall closed section;

  • prestress per the tabulated strand pattern: elastic shortening (5.9.3.2.3a) + the approximate lump-sum time-dependent loss (5.9.3.3). The transfer check assumes every strand fully bonded at the transfer length — conservative for the longest catalog spans, where the standard drawing debonds strands near the ends (a D/C slightly over 1.0 on transfer tension there reproduces exactly the condition debonding exists to fix, not a defect in the tabulated design);

  • concrete stress checks at transfer (5.9.2.3.1) and service (5.9.2.3.2, Service III tension with the 0.8 live-load factor);

  • Strength I flexural resistance (5.6.3) against the factored envelope;

  • release/erection camber passed through from the design line (the sheet’s own tabulated values).

Units: kip / inch / ksi internally; spans and uniform loads enter in feet and klf. Simple spans only (the PSBD standard’s scope).

class civilpy.structural.box_beam_pipeline.BoxBeamDeadLoads(beam: float = 0.0, deck: float = 0.0, barrier: float = 0.0, asphalt: float = 0.0, fws: float = 0.0, sources: dict = <factory>)[source]

Bases: object

Per-beam uniform dead loads (klf) for an adjacent box-beam bridge, split into the LRFD load cases and traceable to the BDM article that sets each one.

dc1 acts on the bare beam (the beam itself plus a wet CIP deck), dc2 on the composite section (barriers, railings) and dw is the wearing-surface case that takes the LRFD 3.4.1 gamma of 1.50 rather than 1.25.

asphalt: float = 0.0

asphalt wearing surface in place today

barrier: float = 0.0

railing / barrier share

beam: float = 0.0

beam self weight, klf

property dc1: float
property dc2: float
deck: float = 0.0

CIP deck incl. monolithic wearing surface

property dw: float
fws: float = 0.0

future wearing surface allowance

sources: dict
summary() str[source]
class civilpy.structural.box_beam_pipeline.BoxBeamLineChecks(design: object, df_moment: float, df_shear: float, midspan_moments: dict = <factory>, losses: dict = <factory>, stresses: dict = <factory>, checks: dict = <factory>, camber_release_in: float = 0.0, camber_erection_in: float = 0.0)[source]

Bases: object

Everything box_beam_line_checks() derives for one interior beam of one PSBDD-1-25 design line. Moments in kip-ft; stresses in ksi (compression positive); checks values are CheckResult.

property all_ok: bool
camber_erection_in: float = 0.0
camber_release_in: float = 0.0
checks: dict
design: object
df_moment: float
df_shear: float
losses: dict
midspan_moments: dict
stresses: dict
summary() str[source]
class civilpy.structural.box_beam_pipeline.DeckLayout(n_lanes: int, lane_width_ft: float, shoulder_ft: tuple[float, float], barrier: str | None, barrier_width_ft: float, n_beams: int, beam_width_ft: float, deck_width_ft: float, roadway_ft: float, required_ft: float, spare_ft: float, lane_offsets_ft: tuple[float, ...], governing_loaded_lanes: int, exterior_lane_fraction: float)[source]

Bases: object

A deck sized from the traffic it has to carry.

The designer states lanes, shoulders and railing; everything else – how wide the deck has to be, how many boxes that takes, and where the lanes have to sit to govern – follows.

barrier: str | None
barrier_width_ft: float
beam_width_ft: float
deck_width_ft: float
exterior_lane_fraction: float
governing_loaded_lanes: int
lane_offsets_ft: tuple[float, ...]
lane_width_ft: float
n_beams: int
n_lanes: int
required_ft: float
roadway_ft: float
shoulder_ft: tuple[float, float]
spare_ft: float
summary() str[source]
civilpy.structural.box_beam_pipeline.F_PJ_KSI = 202.5

jacking stress, low-relaxation (5.9.2.2)

civilpy.structural.box_beam_pipeline.TOPPING_FC_KSI = 4.5

PSBD cast-in-place topping strength

civilpy.structural.box_beam_pipeline.barrier_width_ft(barrier: str | None) float[source]

Base width of one railing run, feet, from the ODOT catalog (BR-1 (36 in) is 18 in, SBR-1 (42 in) 21 in, …).

civilpy.structural.box_beam_pipeline.box_beam_dead_loads(box: str, span_ft: float, n_beams: int, *, barrier: str | None = 'BR-1 (36 in)', n_barriers: int = 2, barrier_klf: float | None = None, fws_ksf: float | None = None, asphalt_in: float | None = None, deck_in: float | None = None, composite: bool | None = None) BoxBeamDeadLoads[source]

The BDM’s dead loads for one interior beam, in klf.

The wearing surface is not a free choice – BDM 309.1 ties it to whether the beam is composite:

  • composite (a CB design): a 6 in CIP deck, of which the top 1 in is the monolithic wearing surface that 309.1.A excludes from the composite section but not from the load. No asphalt.

  • non-composite (a B design): no deck; a 3 in minimum asphalt concrete wearing surface (309.1.B), 8 in maximum (308.2.3.3), at the BDM 909.A unit weight of 145 pcf – not LRFD’s 140.

Both get the BDM 303.1.2 future wearing surface of 0.060 ksf on top, which the manual states unconditionally; pass fws_ksf=0.0 for the two cases that exempt it (temporary structures, BDM 501, and the dead load used for shop camber, BDM 308.2.2.1.f).

The parapets come from the ODOT railing catalog: barrier names a standard ("BR-1 (36 in)" = 0.441 klf per run, "SBR-1 (42 in)" = 0.613, …) and n_barriers how many runs the deck carries, so they are in the dead load by default rather than something a caller has to remember. barrier_klf overrides with an explicit bridge total; barrier=None with no override means no parapet at all. Either way the total is shared equally across the beams – the standard’s own assumption for adjacent units, and what BDM 308.2.2.1.f endorses for camber.

Deck and wearing-surface loads are computed on each beam’s own 4 ft width, so a deck overhanging the fascia beams (up to 8 in per side, BDM 308.2.3.3.c) is not included – add it to barrier_klf.

civilpy.structural.box_beam_pipeline.box_beam_line_checks(box: str, span_ft: float, n_beams: int, *, strand_area_in2: float = 0.153, fci_ksi: float = 4.0, fc_ksi: float = 5.5, barrier_klf: float = 0.0, fws_klf: float = 0.0, humidity_pct: float = 70.0) BoxBeamLineChecks[source]

Re-derive the governing LRFD checks for one interior beam of a PSBDD-1-25 standard design.

barrier_klf / fws_klf are bridge-total railing and future wearing surface, shared equally across the beams (the standard’s own assumption for adjacent units). fci_ksi / fc_ksi default to the low end of the sheet’s designer-selected ranges (conservative for the stress checks).

civilpy.structural.box_beam_pipeline.box_torsion_constant_in4(depth_in: float, width_in: float = 48.0, web_in: float = 6.0, flange_in: float = 5.5) float[source]

St. Venant torsion constant of the closed box (thin-wall, C4.6.2.2.1-3): J = 4 A0^2 / sum(s/t) with the shear-flow path on the wall midlines, webs and flanges at their own thicknesses. Bredt is conservative here – these walls are thick relative to the cell, and an exact St. Venant solve runs ~10-15% higher.

civilpy.structural.box_beam_pipeline.design_lane_offsets_ft(deck_width_ft: float, *, barrier_width_ft: float = 1.75, lane_width_ft: float = 12.0, align: str = 'center') list[float][source]

Transverse centreline of each design lane, from the deck edge.

LRFD 3.6.1.1.1 fixes the countINT(clear roadway / 12 ft), so three lanes on the 40.5 ft clear roadway of an 11-beam bridge – but deliberately leaves the position open: 3.6.1.1.1 has the lanes placed wherever produces the extreme force effect, and on a 40.5 ft roadway three 12 ft lanes leave 4.5 ft of slack to slide them in.

align picks the placement:

"center" (default)

lanes packed adjacent and centred, the usual starting point and the worst case for an interior beam;

"left" / "right"

packed against one barrier – the case that governs the exterior beam, and the one a centred layout will never find.

Warning

Do not try to cover both by handing MIDAS extra candidate lane positions and letting the combination search choose. It treats lanes as independent and will happily load two that overlap, which is worse than the real governing case rather than equal to it. Run the placements as separate analyses and envelope them yourself.

civilpy.structural.box_beam_pipeline.layout_deck(n_lanes: int, *, lane_width_ft: float = 12.0, shoulder_ft: float | tuple[float, float] = 8.0, barrier: str | None = 'BR-1 (36 in)', beam_width_ft: float = 4.0, skew_deg: float = 0.0, radius_ft: float | None = None) DeckLayout[source]

Size an adjacent box-beam deck from the traffic it carries.

n_lanes design lanes at lane_width_ft (LRFD 3.6.1.1.1 uses 12 ft), plus shoulder_ft each side, plus a railing run each side at its cataloged base width, rounded up to a whole number of boxes – you cannot buy two thirds of a beam.

shoulder_ft is the designer’s, not the code’s: the BDM does not set shoulder widths, the L&D Manual does, by route class. The 8 ft default is ODOT’s usual mainline value and should be checked, not assumed.

On a skewed bridge the beams stay square to the abutments and the deck simply runs longer, so the width is unaffected. A curved alignment is different: BDM 302.1 allows box beams only where the mid-ordinate is 6 in or less, and the chorded beams need that mid-ordinate added to the width, so radius_ft widens the deck by L^2 / 8R and refuses the layout outright past the 6 in limit.

civilpy.structural.box_beam_pipeline.structural_model_from_box(box: str, span_ft: float, n_beams: int | None = None, *, n_lanes: int | None = None, lane_width_ft: float = 12.0, shoulder_ft: float | tuple[float, float] = 8.0, ties: bool = True, dead_loads: bool = True, shear_keys: bool = True, deck: bool | None = None, lanes: bool = True, lane_offsets_ft: list[float] | None = None, barrier: str | None = 'BR-1 (36 in)', n_barriers: int = 2, barrier_klf: float | None = None, fws_klf: float | None = None, fws_ksf: float | None = None, asphalt_in: float | None = None, deck_in: float | None = None, fc_ksi: float = 5.5, skew_deg: float = 0.0, mesh_ft: float = 5.0)[source]

Build the StructuralModel hub for an adjacent box-beam bridge — the MIDAS spoke: one line of beam elements per box, transverse ties at the diaphragm stations, the grouted shear key between each pair of boxes, the composite deck, and the BDM dead loads.

Vertical datum: z = 0 is the top of the beams. Every section rides the default center-center offset, so each node line sits at its own section’s centroid — girders at Yb - D, shear keys at their own centroid, the deck at its mid-surface — and the assembly lines up in a rendered view without any section offsets to keep in sync.

Geometry

mesh_ft sets the target longitudinal segment length. The diaphragm stations are always nodes; this subdivides between them, which both resolves the moving-load influence lines and gives the deck plates a sane aspect ratio. Pass mesh_ft=0 for the diaphragm stations alone.

Shear keys (shear_keys)

A longitudinal beam element line at each joint carrying the actual grout cross-section from shear_key_shape() — about 48 in^2 on a 27 in box, 2 1/2 in wide, so it reads clearly in a non-hidden view instead of hiding inside the beams. It is tied to the boxes each side by the transverse ties, which are split at the key rather than run past it: that is the physical load path (tie rod through the diaphragm, grout key in vertical shear) and it needs no element type beyond BEAM. The key’s own longitudinal stiffness is ~0.25% of one box’s, so it does not meaningfully stiffen the span — it is there to be seen and to carry shear.

Deck (deck, defaults to on for a composite design)

The 5 in structural thickness of the BDM 308.2.3.3.c deck as plate elements at their mid-surface, rigid-linked to every girder node in all 6 DOF — full composite action, and the deck’s own concrete modulus rather than a transformed width. The 1 in monolithic wearing surface is deliberately not in the plates: BDM 309.1.A excludes it from the composite section. It is in the loads.

Warning

The deck plates carry stiffness only. Their weight is in the DC1 beam loads, where it belongs — the deck is wet when it is placed, so the bare beam carries it. Do not add a self-weight load case on top of this or the deck is counted twice. This is a single-stage idealization: for the transfer and service stress checks, which need the load on the section that was there at the time, use box_beam_line_checks().

Dead loads (dead_loads)

From box_beam_dead_loads() — see there for the BDM articles. fws_klf is accepted for backward compatibility as a bridge total in klf; prefer fws_ksf, which defaults to the BDM 303.1.2 value of 0.060 ksf and needs no deck-width bookkeeping.

civilpy.structural.box_beam_pipeline.worst_lane_placement(n_lanes: int, n_beams: int, *, lane_width_ft: float = 12.0, beam_width_ft: float = 4.0, barrier_width: float = 0.0, step_ft: float = 0.25) tuple[tuple[float, ...], int, float][source]

Where to put the lanes so the analysis finds the governing case.

LRFD 3.6.1.1.1 fixes how many design lanes there are but deliberately leaves where they go – they are placed wherever produces the extreme force effect, and a roadway wider than n_lanes x 12 ft leaves slack to slide them in.

This sweeps the lane group across that slack and scores each position with the rigid cross-section reaction of LRFD C4.6.2.2.2d – R = N_L/N_b + x_ext * sum(e) / sum(x^2) – times the multiple presence factor of Table 3.6.1.1.2-1, over every number of loaded lanes. It returns the placement, the governing lane count, and the lanes the fascia beam carries there.

The answer always comes out packed against one barrier, which is worth knowing because it means one analysis covers everything: with the lanes packed, the subset MIDAS loads for N_L = 1 is already the worst single-lane position, the subset for N_L = 2 the worst pair, and so on. A centred layout – the obvious default, and what this used to do – cannot reach any of them, and understates the fascia beam.

Do not instead hand MIDAS extra candidate positions and let its lane-combination search choose: it treats lanes as independent and will load two that overlap, which is not conservative but fictitious.

civilpy.structural.bridge_layout module

Parametric steel-girder bridge layout for the Rhino/Grasshopper front end.

Pure geometry/engineering layout — no Rhino imports — so the whole bridge description is computable and testable outside Rhino. A Grasshopper component (see Notebooks/Rhino Components) feeds user inputs into layout_bridge() and turns the returned primitives into Rhino geometry; every primitive carries the gdr.* user-text tags the civilpy.structural.rhino_gdr reader (and therefore the MIDAS pipeline) consumes, so a generated model is immediately analyzable.

Engineering content comes from the sibling modules and is not duplicated here: deck thickness/mats from the ODOT BDM standard designs (civilpy.structural.odot.deck_design), haunches per BDM 309.3.5, section dimensions from civilpy.structural.steel.W(), barriers from the civilpy.structural.odot.bridge_railing SCD catalog.

Conventions (matching the gdr contract): plan frame with X = stations along the layout centerline, Y = transverse (girder 1 at the lowest Y), Z = up. Plan coordinates and lengths in feet; section dimensions in inches (names carry _in suffixes). Z = 0 is the TOP OF DECK; the structure hangs below it. Skew is the angle (degrees) between a support line and the perpendicular to the centerline — positive skew rotates support lines counterclockwise in plan, so points at +Y shift toward +X. Supports are parallel (constant skew) and deck plan edges stop at the end support lines. The deck is crowned: the top surface peaks at the roadway crown (crown_offset_ft, default mid-width) and falls at cross_slope_pct to each side, and everything hung from it — soffit, haunches, girder seats, bearing seats, and both rebar mats — follows that surface. Z = 0 is the top of deck at the crown.

class civilpy.structural.bridge_layout.BarrierRun(designation: 'str', edge: "Literal['left', 'right']", line: 'tuple[Point, Point]', height_in: 'float | None', base_width_in: 'float | None', weight_plf: 'float | None')[source]

Bases: object

base_width_in: float | None
designation: str

ODOT railing designation (see railing_by_scd()).

edge: Literal['left', 'right']
height_in: float | None
line: tuple[tuple[float, float, float], tuple[float, float, float]]
weight_plf: float | None
class civilpy.structural.bridge_layout.BearingPoint(line_no: 'int', station_index: 'int', location: 'Point', fixity: "Literal['fixed', 'expansion']", tags: 'dict[str, str]')[source]

Bases: object

fixity: Literal['fixed', 'expansion']

"fixed" or "expansion" (see default_fixity()).

line_no: int
location: tuple[float, float, float]
station_index: int
tags: dict[str, str]
class civilpy.structural.bridge_layout.BridgeInput(spans_ft: tuple[float, ...], girder_count: int, girder_spacing_ft: float, girder_label: str, overhang_ft: float, railing: str = 'SBR-1-20', grade: str = 'Grade 50', skew_deg: float = 0.0, design_haunch_in: float = 2.0, deck_thickness_in: float | None = None, deck_fc_ksi: float = 4.5, cross_slope_pct: float = 2.0, crown_offset_ft: float | None = None, composite: bool = True)[source]

Bases: object

Everything a Grasshopper slider panel specifies.

deck_thickness_in of None means “use the ODOT standard design” (BDM Figure 309-3, which also fixes the rebar mats); giving a value switches to a custom deck and skips the standard-design table (the BDM 309.3.1 minimum is still enforced). railing is an SCD designation from the bridge_railing catalog (e.g. "SBR-1-20").

composite: bool = True

Whether the deck acts compositely with the girders (shear studs / rebar extending into the deck). Drives the deck-to-girder connection in the refined-grillage analysis model (grillage_model_from_layout()): a composite deck is rigidly tied to the girders (full plane-section action); a non-composite deck bears on the girders vertically but is free to slip longitudinally. Both are used in practice, so the design engineer sets it – it materially changes the girder demands for steel and prestressed concrete superstructures.

cross_slope_pct: float = 2.0

Deck cross slope, percent. The roadway crowns at crown_offset_ft and falls this grade to each side, so the girder seat elevations vary across the width (they follow the crown/cross-slope, at full precision – not rounded). 0.0 is a flat deck.

crown_offset_ft: float | None = None

Transverse offset (ft, girder 1 at 0) of the roadway crown / high point. None centers it on the girder group.

deck_fc_ksi: float = 4.5
deck_thickness_in: float | None = None
design_haunch_in: float = 2.0
girder_count: int
girder_label: str

AISC W-shape label as cataloged in civilpy.structural.steel, e.g. "W36X150".

girder_spacing_ft: float
grade: str = 'Grade 50'

"Grade 36", "Grade 50", "Grade 50W", or "Grade HPS70W".

Type:

Structural steel grade label (AASHTO M270)

overhang_ft: float
railing: str = 'SBR-1-20'

ODOT SCD railing designation, resolved through railing_by_scd() (e.g. "SBR-1-20", "BR-1-13").

skew_deg: float = 0.0
spans_ft: tuple[float, ...]
class civilpy.structural.bridge_layout.BridgeLayout(inputs: ~civilpy.structural.bridge_layout.BridgeInput, section: ~civilpy.structural.bridge_layout.GirderSection, effective_span_ft: float, girders: tuple[~civilpy.structural.bridge_layout.GirderLine, ...], bearings: tuple[~civilpy.structural.bridge_layout.BearingPoint, ...], haunches: tuple[~civilpy.structural.bridge_layout.HaunchRun, ...], deck: ~civilpy.structural.bridge_layout.DeckSlab, barriers: tuple[~civilpy.structural.bridge_layout.BarrierRun, ...], standard_design: ~civilpy.structural.odot.deck_design.StandardDeckDesign | None, doc_tags: dict[str, str] = <factory>)[source]

Bases: object

The full generated bridge: geometry primitives plus the document tags (doc_tags) the .3dm must carry for the Python reader.

barriers: tuple[BarrierRun, ...]
bearings: tuple[BearingPoint, ...]
property crown_y_ft: float

Transverse offset of the roadway crown (girder 1 at y = 0).

deck: DeckSlab
deck_profile_yz() tuple[tuple[float, float], ...][source]

Closed deck cross-section as (y, z) pairs — the crowned top, the parallel soffit, and the thickened overhangs per BDM Figure 309-4: the overhang soffit runs at overhang_thickness_in below the top (parallel to it) from the deck edge to the outboard top-flange tip of the fascia girder, where it steps up to the uniform thickness_in soffit. With the standard t + 2 in overhang and a 2 in design haunch that step lands exactly at the flange-top / haunch-bottom plane, reproducing the figure. Order: along the top from y_lo to y_hi (crown included when interior), then back along the soffit. A backend maps (y, z) into 3D at each bridge end (x = station + y*tan(skew)) and lofts the two profiles into the deck solid.

deck_soffit_z(y: float) float[source]

Deck-soffit elevation (ft) at y, parallel to the top surface (uniform slab; the overhang thickening past the fascia girders is additional and belongs to deck_profile_yz()).

deck_top_z(y: float) float[source]

Top-of-deck elevation (ft) at transverse offset y: 0 at the crown, falling at cross_slope_pct to each side.

property deck_width_ft: float
doc_tags: dict[str, str]
effective_span_ft: float
girders: tuple[GirderLine, ...]
haunches: tuple[HaunchRun, ...]
inputs: BridgeInput
section: GirderSection
standard_design: StandardDeckDesign | None
property total_length_ft: float
class civilpy.structural.bridge_layout.DeckSlab(outline: tuple[tuple[float, float, float], tuple[float, float, float], tuple[float, float, float], tuple[float, float, float]], thickness_in: float, overhang_thickness_in: float, structural_thickness_in: float, rebar: tuple[RebarSet, ...])[source]

Bases: object

Plan outline (counterclockwise, each corner at the local deck-top elevation of the crowned surface), thicknesses, and the mats. outline corners run girder-1 start edge -> girder-N start edge -> girder-N end edge, honoring skew.

outline: tuple[tuple[float, float, float], tuple[float, float, float], tuple[float, float, float], tuple[float, float, float]]
overhang_thickness_in: float
rebar: tuple[RebarSet, ...]
structural_thickness_in: float
thickness_in: float
class civilpy.structural.bridge_layout.GirderLine(line_no: 'int', start: 'Point', end: 'Point', tags: 'dict[str, str]')[source]

Bases: object

end: tuple[float, float, float]
line_no: int
start: tuple[float, float, float]
tags: dict[str, str]
class civilpy.structural.bridge_layout.GirderSection(label: str, depth: float, flange_width: float, flange_thickness: float, web_thickness: float, fillet_k: float)[source]

Bases: object

W-shape display dimensions (in), pulled from the AISC database.

depth: float
fillet_k: float
flange_thickness: float
flange_width: float
label: str
web_thickness: float
class civilpy.structural.bridge_layout.HaunchRun(line_no: int, start: tuple[float, float, float], end: tuple[float, float, float], width_in: float, depth_in: float)[source]

Bases: object

One haunch prism: the girder line at deck-soffit level plus the section (width x depth, in) to extrude along it.

The square cross-section is deliberate: BDM 309.3.5 (2020 Ed.) says “Detail the sides of the haunch as vertical and aligned with the edges of the top flange” — no sloped forming at any depth — with a 2 in minimum design haunch (see Figures 309-7/309-8).

depth_in: float
end: tuple[float, float, float]
line_no: int
start: tuple[float, float, float]
width_in: float
civilpy.structural.bridge_layout.NOMINAL_PARAPET_PLF_PER_FT = 130.0

Preliminary concrete-parapet weight per foot of barrier height, lb/ft/ft, used only when the SCD catalog has no tabulated weight_per_ft. Calibrated so a 42 in single-slope (ODOT SBR-1) comes out ~455 plf; replace with the real barrier weight for final design.

class civilpy.structural.bridge_layout.RebarSegment(points: tuple[tuple[float, float, float], ...], rebar_set: RebarSet)[source]

Bases: object

One physical bar: a polyline at the set’s depth below the local deck surface, so bars follow the crown/cross-slope. points has two vertices for a straight bar and three when a transverse bar crosses the crown (the crank at the high point); start/end are the first and last vertices.

property end: tuple[float, float, float]
property length_ft: float
points: tuple[tuple[float, float, float], ...]
rebar_set: RebarSet
property start: tuple[float, float, float]
class civilpy.structural.bridge_layout.RebarSet(name: str, direction: str, mat: str, size: int, spacing_in: float, depth_in: float, angle_deg: float = 0.0, extent: str = 'deck', overhang_cutoff_in: float | None = None)[source]

Bases: object

One family of deck bars for the generator to instantiate.

direction is "transverse" or "longitudinal"; mat is "top" / "bottom". depth_in is the bar centerline depth below the top of deck. angle_deg rotates transverse bars in plan (0 = perpendicular to the centerline; the BDM 309.3.4.2 skew rule is applied by the layout). extent is "deck" for full-width/length mats or "overhang" for the additional overhang bars, which run from each deck edge to overhang_cutoff_in beyond the fascia girder centerline.

angle_deg: float = 0.0
depth_in: float
direction: str
extent: str = 'deck'
mat: str
name: str
overhang_cutoff_in: float | None = None
size: int
spacing_in: float
civilpy.structural.bridge_layout.deck_rebar_segments(layout: BridgeLayout, side_cover_in: float = 2.0) list[RebarSegment][source]

Instantiate every deck bar (feet), clipped to the deck plan inset by side_cover_in on the edges. The Grasshopper generator draws these directly; nothing here needs Rhino.

Bars sit depth_in below the local deck surface, so both mats follow the crown/cross-slope and stay inside the slab (a level bar would exit the soffit at the crown and the top surface at the edges). A transverse bar whose run crosses the crown is emitted as one 3-vertex polyline cranked at the high point.

The deck plan is the skewed parallelogram y in [y_lo, y_hi], u = x - y*tan(skew) in [0, L].

civilpy.structural.bridge_layout.default_fixity(station_index: int, station_count: int) Literal['fixed', 'expansion'][source]

Bearing-fixity starting point, mirroring the C# plugin’s rule: a single span is fixed at its first bearing; a continuous unit at the interior support nearest mid-length; everything else expansion.

civilpy.structural.bridge_layout.effective_span_ft(spacing_ft: float, section: GirderSection) float[source]

Effective deck span (ft) per LRFD 9.7.2.3 for slabs on steel girders: clear distance between flange tips plus the flange overhang (tip to web face) — i.e. S - bf/2 - tw/2.

civilpy.structural.bridge_layout.girder_line_loads(layout: BridgeLayout, girder_index: int) dict[source]

Uniform dead loads (klf, positive = downward) on one girder line.

girder_index is 0-based (0 = first fascia). Returns {"dc1", "dc2", "dw"}: DC1 = girder self-weight + wet deck slab on the girder’s tributary width; DC2 = the barrier weight, on fascia girders only; DW = future wearing surface on the tributary width. These are the per-line loads the hub applies and the native line-girder envelope (girder_pipeline) consumes – preliminary magnitudes (no haunch/cross-slope), same basis as structural_model_from_layout().

civilpy.structural.bridge_layout.girder_section(label: str) GirderSection[source]

Resolve an AISC W label to display dimensions via steel.W.

civilpy.structural.bridge_layout.grillage_model_from_layout(layout: BridgeLayout, *, composite: bool | None = None, seg_target_ft: float | None = None, dead_loads: bool = True, girder_subset: list[int] | None = None)[source]

Build the refined-grillage hub: bare-steel girder beams plus a physical deck of plate elements, tied together with rigid links.

This is the “deck as plate, girder as frame” model MIDAS recommends when the full deck surface is needed (moving-load traffic lanes) – unlike structural_model_from_layout(), which is a line-girder grid with no deck. The deck’s longitudinal stiffness lives entirely in the plate elements and the girders stay bare steel, so the deck stiffness is never double-counted (the trap of pairing a composite girder section with deck plates).

The composite toggle (defaulting to layout.inputs.composite) sets the deck-to-girder connection, mirroring whether the real bridge has shear studs / rebar extending into the deck:

  • composite – a fully rigid link (all 6 DOF) at every deck node over a girder, so slab and girder share plane sections (transformed-section action emerges from the geometry).

  • non-composite – the deck bears on the girders vertically and is located transversely/rotationally, but longitudinal slip (DX) is free, so the slab adds no composite flexural stiffness to the girder; a single longitudinal anchor at the fixed-bearing line removes rigid-body drift.

seg_target_ft is the target longitudinal plate length (default: the girder spacing, giving roughly square plates); each span is divided into a whole number of segments nearest that target, always keeping a node on every support line. Dead loads (DC1/DC2/DW) are applied to the girders as in structural_model_from_layout(); the plates carry stiffness only (self-weight is not separately activated, so the slab weight is not double-counted).

girder_subset (0-based, contiguous) builds only those girder lines and the deck they carry – a construction phase. The phase deck runs from the outer overhang (where a fascia girder is included) to the mid-bay closure joint where the phase is cut from the girders not yet built, so the phase’s inner girder carries a deck cantilever to that joint. This is the stage-1 (or stage-2) structure a closure-pour analysis needs – see civilpy.structural.construction_staging.

Returns the StructuralModel (units kips/ft) with rigid_links populated.

civilpy.structural.bridge_layout.layout_bridge(inp: BridgeInput) BridgeLayout[source]

Generate the full bridge layout from the Grasshopper-level inputs.

Raises ValueError when the inputs violate the ODOT standard-design assumptions (unless a custom deck_thickness_in is given) or are geometrically impossible.

civilpy.structural.bridge_layout.railing_by_scd(scd: str) BridgeRailing[source]

First cataloged railing on the given SCD (e.g. "SBR-1-20").

civilpy.structural.bridge_layout.structural_model_from_layout(layout: BridgeLayout, *, diaphragms: bool = True, dead_loads: bool = True)[source]

Build the canonical StructuralModel hub straight from a BridgeLayout – no Rhino .3dm round-trip.

This is the faithful full-bridge analysis model the MIDAS spoke consumes (via civilpy.structural.midas_models.midas_payloads()): every girder line is a continuous chain of beam elements broken at every support station, each carrying its resolved AISC section and steel grade; each bearing becomes a 6-DOF restraint (fixed or expansion) on its girder node; and, when diaphragms is set, a transverse beam ties adjacent girders at every support line. Contrast the equivalent-strip slab model – here the whole grid goes to MIDAS, so it can host traffic lanes for a moving-load run.

With dead_loads the three dead-load cases are applied as downward (negative GZ) uniform beam loads on the girder elements:

  • DC1 (non-composite): girder self-weight + the wet deck slab on each girder’s tributary width,

  • DC2 (composite SDL): each barrier’s weight on its fascia girder,

  • DW: future wearing surface (FWS_KSF) on the tributary width.

Haunch weight, cross-slope, and the exact wearing-surface extent (roadway only) are preliminary-level approximations; refine per BDM for final design.

Returns the StructuralModel (units kips/ft).

civilpy.structural.bridge_type module

Preliminary bridge-type feasibility and selection advisor.

The first gate of the conceptual-design loop: given a span arrangement, say which superstructure types are reasonable and steer the engineer away from ones that are not. A 300 ft single span asked of a reinforced concrete slab has to be redirected to a continuous steel plate girder (or a comparable long-span type) before any detailing or analysis happens – that redirect is the job of assess().

This is planning-level guidance, not a code check. The span ranges are practical/economical envelopes drawn from ODOT BDM Section 302 (structure type studies) and common AASHTO practice, not hard limits: a type near the edge of its range is flagged “marginal,” and the controlling span (the longest, and for simple spans the only span) is what a type is judged against. The engineer still runs the real design; civilpy only keeps the conceptual phase honest.

Pure Python (no numpy/heavy deps) so it runs anywhere the rest of the station/offset stack does, including Rhino’s bundled CPython.

Conventions: spans in feet, one entry per span in spans_ft (a single-span bridge is [L]). A type is “continuous” if it carries negative moment over interior supports; single-span or simple-span-made-continuous-for-live-load types are marked accordingly.

class civilpy.structural.bridge_type.BridgeType(key: str, name: str, material: str, min_span_ft: float, max_span_ft: float, simple_max_span_ft: float | None = None, needs_continuity: bool = False, note: str = '')[source]

Bases: object

One superstructure family and the span envelope it is economical in.

min_span_ft / max_span_ft bound a single span of this type; max_span_ft is the practical/economical ceiling, above which the type is infeasible (not merely uneconomical). needs_continuity marks types that only reach their upper range as a continuous unit (so a single-span request is capped lower, at simple_max_span_ft).

key: str
material: str
max_span_ft: float
min_span_ft: float
name: str
needs_continuity: bool = False
note: str = ''
simple_ceiling_ft() float[source]

Economical single-span ceiling (falls back to max_span_ft).

simple_max_span_ft: float | None = None
civilpy.structural.bridge_type.CATALOG: tuple[BridgeType, ...] = (BridgeType(key='rc_slab', name='Reinforced concrete slab (single span)', material='concrete', min_span_ft=11.0, max_span_ft=40.0, simple_max_span_ft=40.0, needs_continuity=False, note='ODOT SB-1-24 covers 11-38 ft; reinforced slab gets uneconomical and heavy past ~40 ft.'), BridgeType(key='rc_slab_cont', name='Continuous reinforced concrete slab', material='concrete', min_span_ft=20.0, max_span_ft=60.0, simple_max_span_ft=40.0, needs_continuity=True, note='ODOT CS-1-24; negative moment over piers extends the slab to ~60 ft interior spans.'), BridgeType(key='adjacent_box', name='Prestressed adjacent box beams', material='prestressed', min_span_ft=20.0, max_span_ft=100.0, simple_max_span_ft=None, needs_continuity=False, note='Shallow, fast to erect; adjacent-box distribution + transverse post-tensioning.'), BridgeType(key='ps_i_girder', name='Prestressed I-girder, composite deck', material='prestressed', min_span_ft=40.0, max_span_ft=160.0, simple_max_span_ft=None, needs_continuity=False, note='AASHTO/ODOT I-beams; made continuous for live load over piers.'), BridgeType(key='steel_rolled', name='Steel rolled-beam, composite deck', material='steel', min_span_ft=40.0, max_span_ft=120.0, simple_max_span_ft=None, needs_continuity=False, note='Rolled W-shapes; economical single or continuous spans.'), BridgeType(key='steel_plate_girder', name='Continuous steel plate girder, composite deck', material='steel', min_span_ft=90.0, max_span_ft=400.0, simple_max_span_ft=180.0, needs_continuity=True, note='Welded plate girders with field splices and flange transitions; the workhorse long-span type.'))

Common ODOT superstructure types, ordered short-span to long-span. Ranges are conceptual-design envelopes (ODOT BDM 302 / AASHTO practice), deliberately generous at the top; see the module docstring.

class civilpy.structural.bridge_type.Feasibility(type: BridgeType, verdict: str, controlling_span_ft: float, continuous: bool, reasons: tuple[str, ...] = ())[source]

Bases: object

How a single type scores against a span arrangement.

continuous: bool
controlling_span_ft: float
property feasible: bool
reasons: tuple[str, ...] = ()
type: BridgeType
verdict: str
class civilpy.structural.bridge_type.Recommendation(type: BridgeType, spans_ft: tuple[float, ...], verdict: str, resplit: bool, reason: str)[source]

Bases: object

A redirect target: a type and the span arrangement it wants.

spans_ft may differ from what the engineer asked for – the whole point of the long-span redirect is to turn one over-long span into a continuous unit of several feasible spans. resplit is True when that happened.

describe() str[source]
reason: str
resplit: bool
spans_ft: tuple[float, ...]
type: BridgeType
verdict: str
class civilpy.structural.bridge_type.TypeAssessment(requested: BridgeType, spans_ft: tuple[float, ...], result: Feasibility, recommended: tuple[Recommendation, ...] = ())[source]

Bases: object

The advisor’s answer for a requested type against a span arrangement.

allowed is True when the requested type is feasible (possibly marginal). When it is not, recommended holds the redirect targets (type + span arrangement), best first.

property allowed: bool
recommended: tuple[Recommendation, ...] = ()
requested: BridgeType
result: Feasibility
spans_ft: tuple[float, ...]
summary() str[source]

A human-readable planning message (what the notebook/GH prints).

property verdict: str
civilpy.structural.bridge_type.assess(requested_key: str, spans_ft, *, n_recommend: int = 3) TypeAssessment[source]

Assess a requested bridge type against a span arrangement and, when it is infeasible, recommend the best feasible alternatives – re-splitting an over-long single span into a continuous multi-span unit as needed.

Parameters:
  • requested_key (str) – Catalog key of the type the engineer wants (see CATALOG).

  • spans_ft (sequence of float) – Span lengths, feet; [L] for a single span. The longest span controls type feasibility.

  • n_recommend (int) – How many alternatives to return when the request is infeasible.

Returns:

.allowed / .verdict / .summary() and, on a redirect, .recommended (Recommendation targets, ranked best-first).

Return type:

TypeAssessment

Examples

>>> a = assess("rc_slab", [300.0])
>>> a.allowed
False
>>> [r.type.material for r in a.recommended][:1]
['steel']
>>> a.recommended[0].resplit          # 300 ft becomes a continuous unit
True
>>> assess("rc_slab", [30.0]).allowed
True
civilpy.structural.bridge_type.evaluate_type(bt: BridgeType, spans_ft, *, marginal_band: float = 0.1) Feasibility[source]

Score one type against spans_ft.

A single-span arrangement (len(spans_ft) == 1) is judged against the type’s simple ceiling; a multi-span arrangement lets continuous types use their full range. Within marginal_band (fraction) of either end of the range the verdict is “marginal” rather than “ok”.

civilpy.structural.bridge_type.feasible_types(spans_ft, *, include_marginal: bool = True) list[Feasibility][source]

Every catalog type that is not infeasible for spans_ft, ordered by how centered the controlling span sits in each type’s range (best first).

civilpy.structural.bridge_type.get_type(key: str) BridgeType[source]

Look up a catalog type by key; KeyError with the valid keys if bad.

civilpy.structural.bridge_type.recommend_for_length(total_ft: float, *, exclude: str | None = None, n: int = 3) list[Recommendation][source]

Best types for crossing total_ft, re-splitting into continuous units where a single span will not do. Ranked fewest-spans then most-centered, ok before marginal.

civilpy.structural.cande module

Box-culvert soil-structure interaction through CANDE.

CANDE (Culvert ANalysis and DEsign, FHWA) solves the buried-structure problem the AASHTO simplified methods approximate: incremental embankment construction, nonlinear soil, and arching around the structure. This module builds a CANDE Level-2 box-culvert model (the engine’s canned box mesh), runs it through the cande-wrapper package, and returns member force envelopes in the kip/ft units the civilpy.structural.aashto checks expect — so CANDE supplies the demands and civilpy the capacities.

Install the engine with pip install civilpy[cande] (or pip install cande-wrapper). Building the input file needs nothing beyond civilpy:

>>> from civilpy.structural.cande import BoxCulvertModel, SoilMaterial
>>> model = BoxCulvertModel(span_ft=8, rise_ft=6, cover_ft=4,
...                         top_thickness_in=10, wall_thickness_in=8)
>>> print(model.to_cid().splitlines()[0].strip())
A-1!!ANALYS   2 0  1Box culvert 8 ft x 6 ft, 4 ft cover

The model is a half-mesh of the culvert (symmetric about midspan) with 14 beam elements on the slab/wall centerlines, three soil zones (in situ, bedding, fill), and the fill placed in incremental lifts. span_ft and rise_ft are centerline dimensions. Beam elements carry no self-weight — add the structure dead load to the demands separately.

Results are per foot of culvert length: moments in kip-in/ft, thrust and shear in kip/ft, enveloped over all construction increments.

class civilpy.structural.cande.BoxCulvertModel(span_ft: float, rise_ft: float, cover_ft: float, *, top_thickness_in: float, wall_thickness_in: float, bottom_thickness_in: float | None = None, f_c_ksi: float = 4.0, e_concrete_psi: float | None = None, nu_concrete: float = 0.17, fill: SoilMaterial | None = None, in_situ: SoilMaterial | None = None, bedding: SoilMaterial | None = None, trench_width_ft: float | None = None, bedding_depth_in: float = 12.0, load_steps: int | None = None, title: str | None = None)[source]

Bases: object

A single-cell box culvert under earth fill, analyzed with the CANDE Level-2 box mesh.

span_ft/rise_ft are centerline dimensions; cover_ft the fill above the top slab. Wall/slab stiffnesses come from gross concrete sections of the given thicknesses (e_concrete_psi defaults to 57000*sqrt(f’c) with f’c = 4 ksi). trench_width_ft switches the mesh from embankment to trench installation. Fill is placed in incremental lifts; when the cover exceeds 1.5 times the rise the mesh is truncated and the remainder applied as surface pressure (handled by the engine; an extra load step is added automatically).

run(work_dir=None, prefix: str = 'culvert') BoxCulvertResults[source]

Run the model through the CANDE engine and return the parsed results. Requires the cande-wrapper package; outputs land in work_dir (a temporary directory by default).

to_cid() str[source]

The complete CANDE input file as a string.

write_cid(work_dir, prefix: str = 'culvert') Path[source]

Write the input file to work_dir and return its path.

class civilpy.structural.cande.BoxCulvertResults(model: BoxCulvertModel, work_dir, prefix: str)[source]

Bases: object

Member force envelopes parsed from a CANDE box-culvert run.

members maps "top_slab"/"wall"/"bottom_slab" to MemberForces enveloped over every construction increment; corner nodes contribute to both adjacent members. steps holds the raw nodal rows per increment for custom post-processing.

flexure_check(member: str, a_s: float, d_s: float, f_y: float = 60.0, load_factor: float = 1.3)[source]

Flexural check of one member (per-foot strip): the enveloped CANDE moment times load_factor (EV maximum by default) becomes the demand of 5.6.3.2.3. a_s in in^2/ft, d_s in inches.

slab_shear_check(member: str, a_s: float, d_e: float, load_factor: float = 1.3)[source]

Shear check of a slab member under >= 2 ft of fill (5.12.7.3), using the enveloped shear and its concurrent moment from the CANDE run.

class civilpy.structural.cande.MemberForces(moment: float = 0.0, thrust: float = 0.0, shear: float = 0.0, shear_concurrent_moment: float = 0.0)[source]

Bases: object

Force envelope for one culvert member, per foot of length: moment kip-in/ft, thrust kip/ft (compression positive), shear kip/ft, and the moment concurrent with the peak shear (for the Vu*de/Mu term of the slab shear check).

moment: float = 0.0
property moment_kip_ft: float
shear: float = 0.0
shear_concurrent_moment: float = 0.0
thrust: float = 0.0
class civilpy.structural.cande.SoilMaterial(density_pcf: float, model: str, name: str, e_psi: float = 0.0, nu: float = 0.0)[source]

Bases: object

One CANDE soil zone: linear-elastic or a canned Duncan/Selig hyperbolic model. Use the constructors rather than building directly.

density_pcf: float
classmethod duncan_selig(name: str, density_pcf: float) SoilMaterial[source]

A canned Duncan/Selig soil (e.g. "SW90" for gravelly sand at 90% standard Proctor).

e_psi: float = 0.0
classmethod elastic(e_psi: float, nu: float, density_pcf: float, name: str = 'ELASTIC') SoilMaterial[source]

Linear-elastic soil with Young’s modulus e_psi and Poisson’s ratio nu.

model: str
name: str
nu: float = 0.0

civilpy.structural.concrete module

ACI 318-19 Chapter 17 - Anchoring to Concrete

All lengths in inches, forces in pounds (lb), stresses in psi.

class civilpy.structural.concrete.AnchorBolts(f_c: float, h_a: float, d_a: float, h_ef: float, f_ya: float, f_uta: float, n_x: int = 1, n_y: int = 1, s_x: float = 0.0, s_y: float = 0.0, c_a1: float = inf, c_a2: float = inf, c_a_min: float | None = None, A_se_N: float | None = None, A_se_V: float | None = None, A_brg: float | None = None, e_h: float | None = None, lambda_a: float = 1.0, is_cracked: bool = True, has_supp_reinf: bool = False, is_ductile: bool = True, grout_pad: bool = False, e_N_prime: float = 0.0, e_V_prime: float = 0.0, A_Nc: float | None = None, A_Vc: float | None = None, N_ua: float = 0.0, V_ua: float = 0.0, shear_direction: Literal['perpendicular', 'parallel', 'away'] = 'perpendicular', anchor_type: Literal['cast_in', 'post_installed', 'adhesive'] = 'cast_in', tau_uncr: float | None = None, tau_cr: float | None = None, sdc: str = 'A', anchor_reinf_tension: float | None = None, anchor_reinf_shear: float | None = None, bolt_circle_radius: float | None = None, shaft_radius: float | None = None, coupler_depth: float | None = None, c_ac: float | None = None)[source]

Bases: object

ACI 318-19 Chapter 17 anchor design checks for cast-in and post-installed anchors.

All inputs in US customary units (inches, pounds, psi).

Parameters:
  • f_c (float) – Specified concrete compressive strength (psi). Capped at 10,000 psi for cast-in anchors per ACI 318 Section 17.3.1.

  • h_a (float) – Thickness of concrete member measured parallel to anchor axis (in).

  • d_a (float) – Nominal anchor diameter (in).

  • h_ef (float) – Effective embedment depth (in).

  • f_ya (float) – Anchor yield strength (psi). e.g. ASTM F1554 Gr 36 → 36,000.

  • f_uta (float) – Anchor tensile strength (psi). e.g. ASTM F1554 Gr 36 → 58,000. Used value is capped at min(1.9*f_ya, 125,000) per ACI 318-19 17.6.1.2.

  • n_x (int) – Number of anchors in the x-direction (direction of applied shear).

  • n_y (int) – Number of anchors in the y-direction.

  • s_x (float) – Anchor spacing in x-direction (in). Use 0 if n_x == 1.

  • s_y (float) – Anchor spacing in y-direction (in). Use 0 if n_y == 1.

  • c_a1 (float) – Edge distance from anchor to free edge in direction of applied shear (in). Use a large value (e.g. 100) if no near edge in shear direction.

  • c_a2 (float) – Edge distance from anchor to free edge perpendicular to shear (in). Use a large value if no near edge perpendicular to shear.

  • c_a_min (float, optional) – Minimum edge distance in any direction (in). Defaults to min(c_a1, c_a2).

  • A_se_N (float, optional) – Effective cross-sectional area of anchor in tension (in²). Computed from UNC thread formula if not provided.

  • A_se_V (float, optional) – Effective cross-sectional area in shear (in²). Defaults to A_se_N.

  • A_brg (float, optional) – Net bearing area of anchor head (in²). Required for headed-bolt pullout per 17.6.3.2.2a. If None, pullout check uses the hooked-bolt formula with e_h, or is skipped.

  • e_h (float, optional) – Hook length (in) for J- or L-bolts; from inside of hook to bearing surface. Required for hooked-bolt pullout per 17.6.3.2.2b.

  • lambda_a (float) – Lightweight concrete modification factor per ACI 318 Table 17.2.4.1. Use 1.0 for normalweight concrete (default).

  • is_cracked (bool) – True if concrete is assumed cracked at service load levels (conservative default). False for uncracked.

  • has_supp_reinf (bool) – True if supplementary reinforcement conforming to ACI 318 17.5.2.1 is provided (Condition A → higher φ for concrete limit states).

  • is_ductile (bool) – True if anchor material meets ductility requirements (elongation ≥ 14%, reduction ≥ 30%). ASTM F1554 Gr 36 and Gr 55 qualify.

  • grout_pad (bool) – True if anchor is used with a built-up grout pad. Reduces V_sa by 0.80 per ACI 318 Section 17.7.1.2.1.

  • e_N_prime (float) – Eccentricity of resultant tensile force from centroid of anchor group (in). Use 0 for concentric loading.

  • e_V_prime (float) – Eccentricity of shear force from centroid of anchor group (in). Use 0 for concentric shear.

  • A_Nc (float, optional) – Projected concrete failure area for tension (in²). Computed automatically if not provided.

  • A_Vc (float, optional) – Projected concrete failure area for shear (in²). Computed automatically if not provided.

  • N_ua (float) – Total factored tensile demand on the anchor group (lb).

  • V_ua (float) – Total factored shear demand on the anchor group (lb).

  • shear_direction (str) – Direction of applied shear relative to the nearest edge: “perpendicular” (default), “parallel” (ACI 17.7.2.1(c) — doubles V_cb, sets psi_ed=1.0), or “away” (shear directed away from edge — no edge breakout check).

  • anchor_type (str) – “cast_in” (default), “post_installed”, or “adhesive”. Affects k_c in basic breakout (24 vs 17) and enables bond check.

  • tau_uncr (float, optional) – Characteristic bond stress in uncracked concrete (psi), from ACI 355.4 evaluation report. Required for adhesive bond strength check.

  • tau_cr (float, optional) – Characteristic bond stress in cracked concrete (psi). Required for adhesive bond strength check when is_cracked=True.

  • sdc (str) – Seismic Design Category (“A”–“F”). For SDC C–F, concrete failure mode design strengths are multiplied by 0.75 per ACI 318-19 Section 17.10.5.3.

  • anchor_reinf_tension (float, optional) – Design strength (φ*Rn, lb) of properly developed anchor reinforcement in tension per ACI 17.5.2.1. When provided, replaces concrete breakout in the tension limit-state check.

  • anchor_reinf_shear (float, optional) – Design strength (φ*Rn, lb) of anchor reinforcement in shear per ACI 17.5.2.1. When provided, replaces concrete breakout in the shear limit-state check.

  • bolt_circle_radius (float, optional) – Radius of the bolt circle (in) for circular anchor patterns (e.g. highway poles, luminaires, sign structures). When provided, _A_Nc and _A_Vc use annular/circular geometry instead of the rectangular array formulas. Use from_circular() to construct typical highway pole base patterns.

  • shaft_radius (float, optional) – Outer radius of a drilled shaft (in). When provided together with bolt_circle_radius, automatically sets c_a1 = shaft_radius − bolt_circle_radius (edge distance from bolt to shaft perimeter).

  • coupler_depth (float, optional) – Depth (in) of a heavy-hex coupling nut used to extend a damaged anchor rod via field repour. Per AISC Design Guide 1 and ACI 318-19 Section 17.10.5.3, the coupler must be frictionally isolated from the new concrete (e.g., with polyethylene tape or PVC sleeving) to preserve the anchor’s stretch length and maintain ductile behavior. If the coupler is NOT isolated, it acts as the bearing surface, reducing effective embedment: h_ef_eff = h_ef − coupler_depth.

  • c_ac (float, optional) – Critical edge distance for post-installed anchors (in). Used in the splitting factor ψ_cp,N per ACI 318-19 Eq. 17.6.2.6.1b. Defaults to 2.5·h_ef per ACI Table 17.9.5 if not provided. Only used when anchor_type=’post_installed’ and is_cracked=False.

static bolt_demands_from_pole(n_bolts: int, bolt_circle_radius: float, M_u: float, P_u: float = 0.0, V_u: float = 0.0, T_u: float = 0.0) tuple[source]

Max individual bolt demands from global AASHTO pole reactions.

Uses the elastic circular bolt group method (AASHTO LTS-1 / AISC Design Guide 1). All inputs in consistent units (lb and in).

Parameters:
  • n_bolts (int) – Number of anchor rods on the bolt circle.

  • bolt_circle_radius (float) – Radius from pole centroid to bolt centerlines (in).

  • M_u (float) – Factored overturning moment at base (lb·in).

  • P_u (float) – Factored axial load; positive = uplift/tension (lb).

  • V_u (float) – Factored in-plane shear at base (lb).

  • T_u (float) – Factored torsional moment at base (lb·in).

Returns:

(N_ua_max, V_ua_max) – Maximum tension demand (lb) and shear demand (lb) on a single bolt.

Return type:

tuple[float, float]

Notes

Tension from moment: N_M = 2·M_u / (n·r)  [elastic, Σy² = n·r²/2]
Axial share:         N_P = P_u / n
Shear from V_u:      V_v = V_u / n
Shear from torsion:  V_t = T_u / (n·r)  [all bolts at equal radius]
Final:               N_ua_max = max(0, N_M + N_P)
                     V_ua_max = V_v + V_t  (conservative, additive)
bond_strength_tension() AnchorCheckResult | None[source]

Bond strength of adhesive anchors in tension — ACI 318-19 Section 17.6.5.

Requires anchor_type=”adhesive” and tau_uncr (or tau_cr when is_cracked). Returns None for non-adhesive anchors or missing bond stress data.

N_ba = tau * π * d_a * h_ef (Eq. 17.6.5.2.1) c_Na = 10 * d_a * sqrt(tau / 1100) (Eq. 17.6.5.1.5, influence distance)

breakout_strength_shear() AnchorCheckResult | None[source]

Concrete breakout strength of anchor group in shear — ACI 318-19 17.7.2.

shear_direction="perpendicular": standard edge breakout (17.7.2.1a/b). Corner condition (both c_a1 and c_a2 finite): automatically computes both edges and uses the minimum per ACI 17.7.2.1(d). shear_direction="parallel": doubles V_cb with psi_ed=1.0 (17.7.2.1(c)). shear_direction="away": returns None (no edge breakout).

Returns None when c_a1 is infinite (no near edge in shear direction).

breakout_strength_tension() AnchorCheckResult[source]

Concrete breakout strength of anchor group in tension.

Single anchor: Eq. 17.6.2.1a Anchor group: Eq. 17.6.2.1b (includes psi_ec_N)

property c_ac: float

Critical edge distance for post-installed anchors (in) — ACI Table 17.9.5.

check_all() dict[source]

Run all applicable limit-state checks.

Returns a dict mapping check label to AnchorCheckResult. Interaction check is included only when both N_ua and V_ua are nonzero. Stretch length check is included for SDC C–F.

classmethod from_circular(n_bolts: int, bolt_circle_radius: float, f_c: float, h_a: float, d_a: float, h_ef: float, f_ya: float, f_uta: float, M_u: float | None = None, P_u: float = 0.0, V_u: float | None = None, T_u: float = 0.0, **kwargs) AnchorBolts[source]

Constructor for circular bolt patterns — highway poles, luminaires, signs.

Parameters:
  • n_bolts (int) – Total number of anchor rods on the bolt circle.

  • bolt_circle_radius (float) – Radius of the bolt circle (in), measured to bolt centerlines.

  • M_u (float, optional) – Factored overturning moment at pole base (lb·in). When provided, bolt demands are computed automatically via bolt_demands_from_pole().

  • P_u (float, optional) – Factored axial load; positive = uplift/tension (lb). Used with M_u.

  • V_u (float, optional) – Factored base shear at pole base (lb). When provided, shear demands are computed automatically via bolt_demands_from_pole().

  • T_u (float, optional) – Factored torsional moment at pole base (lb·in). Used with V_u.

  • **kwargs – Any additional AnchorBolts parameters (shaft_radius, sdc, N_ua, …). Explicit N_ua/V_ua in kwargs take precedence over computed values.

Notes

n is stored as n_y=n_bolts, n_x=1. Projected areas _A_Nc and _A_Vc use annular/circular geometry automatically when bolt_circle_radius is set. Supply shaft_radius to auto-derive c_a1 for drilled shaft applications.

classmethod from_dxf(path: str, f_c: float, h_a: float, h_ef: float, f_ya: float, f_uta: float, bolts_layer: str = 'BOLTS', concrete_layer: str = 'CONCRETE', **kwargs) AnchorBolts[source]

Build an AnchorBolts instance by parsing geometry from a DXF file.

Reads two layers from the DXF modelspace:

  • bolts_layer (default "BOLTS") — CIRCLE entities whose centres define bolt locations and whose radius gives anchor_radius = d_a / 2.

  • concrete_layer (default "CONCRETE") — one LWPOLYLINE (or POLYLINE) whose vertices define the concrete boundary. Edge distances c_a1 (shear direction) and c_a2 (perpendicular) are derived from the minimum distances from the bolt-group centroid to the boundary edges, classified by edge orientation.

Bolt pattern detection:

  • Circular — all bolts lie within 10 % of the same radius from the centroid. Delegates to from_circular().

  • Rectangular — grid spacing is inferred from the sorted unique x/y coordinates of the bolt positions.

Parameters:
  • path (str) – Absolute or relative path to the .dxf file.

  • f_c (float) – Material and geometry properties not derivable from CAD.

  • h_a (float) – Material and geometry properties not derivable from CAD.

  • h_ef (float) – Material and geometry properties not derivable from CAD.

  • f_ya (float) – Material and geometry properties not derivable from CAD.

  • f_uta (float) – Material and geometry properties not derivable from CAD.

  • bolts_layer (str) – Layer names to query (case-sensitive, DXF convention is UPPERCASE).

  • concrete_layer (str) – Layer names to query (case-sensitive, DXF convention is UPPERCASE).

  • **kwargs – Additional AnchorBolts keyword arguments (N_ua, V_ua, sdc, …).

Return type:

AnchorBolts

governing_shear_strength() AnchorCheckResult[source]

Return the governing (minimum φSn) shear limit state.

governing_tension_strength() AnchorCheckResult[source]

Return the governing (minimum φSn) tension limit state.

plot(title: str = '', figsize: tuple = (8, 7))[source]

2D plan-view diagram of the anchor group.

Draws the concrete boundary, bolt locations, the 1.5·h_ef tension breakout zone (dashed red), and demand arrows for V_ua and N_ua.

Parameters:
  • title (str) – Optional plot title; defaults to repr(self).

  • figsize (tuple) – Matplotlib figure size in inches, default (8, 7).

Returns:

(fig, ax) – Matplotlib Figure and Axes objects for further customisation or saving.

Return type:

tuple

pryout_strength() AnchorCheckResult[source]

Concrete pryout strength of anchors in shear — ACI 318-19 Eq. 17.7.3.1.

V_cp = k_cp * N_cp (single anchor) V_cpg = k_cp * N_cpg (group)

k_cp = 1.0 for h_ef < 2.5 in, else 2.0. N_cp / N_cpg taken as concrete breakout strength in tension.

pullout_strength() AnchorCheckResult | None[source]

Pullout strength of cast-in anchors — ACI 318-19 Section 17.6.3.

Returns None if insufficient geometry is provided (no A_brg or e_h).

For headed studs/bolts: N_p = 8 * A_brg * f_c’ (Eq. 17.6.3.2.2a) For J- or L-bolts: N_p = 0.9 * f_c’ * e_h * d_a (Eq. 17.6.3.2.2b) N_pn = psi_c,P * N_p (Eq. 17.6.3.1)

Note: Pullout is evaluated per anchor; total = n * phi * N_pn.

side_face_blowout() AnchorCheckResult | None[source]

Side-face blowout strength of headed anchors — ACI 318-19 Section 17.6.4.

Applicable only when h_ef > 2.5 * c_a1 (deep anchor near edge). Returns None if condition not met or A_brg not provided.

N_sb = 160 * c_a1 * sqrt(A_brg) * lambda_a * sqrt(f_c’) (Eq. 17.6.4.1) Group: N_sbg = (1 + s/6/c_a1) * N_sb (Eq. 17.6.4.2, simplified)

steel_shear_strength() AnchorCheckResult[source]

Steel strength of anchors in shear — ACI 318-19 Eq. 17.7.1.2b.

For cast-in headed bolts (threads may be in shear plane):

V_sa = 0.6 * A_se,V * f_uta   (per anchor)

Grout pad reduces by 0.80 (Section 17.7.1.2.1).

steel_tension_strength() AnchorCheckResult[source]

Steel strength of anchors in tension — ACI 318-19 Eq. 17.6.1.2.

N_sa = A_se,N * f_uta (per anchor) Total group design strength = n * phi * N_sa

stretch_length_check() AnchorCheckResult | None[source]

Stretch length adequacy for seismic ductile designs — ACI 318-19 Section 17.10.5.3.

For SDC C–F where anchor yielding is relied upon for ductility, the frictionally isolated length must be ≥ 8·d_a. Returns None if not in seismic SDC C–F. coupler_depth is used as the isolated stretch length when provided; otherwise h_ef is used as a conservative proxy.

summary() str[source]

Print a formatted table of all limit-state checks.

tension_shear_interaction() AnchorCheckResult[source]

Tension-shear interaction — ACI 318-19 Section 17.8.

If N_ua/(φN_n) ≤ 0.2, shear governs and the full shear capacity is available. If V_ua/(φV_n) ≤ 0.2, tension governs and the full tension capacity is available. Otherwise (Eq. 17.8.3):

N_ua/(φN_n) + V_ua/(φV_n) ≤ 1.2
class civilpy.structural.concrete.AnchorCheckResult(label: str, reference: str, N_n: float, V_n: float, phi: float, phi_Sn: float, demand: float, is_ratio: bool = False)[source]

Bases: object

Result for a single limit-state check.

N_n: float
V_n: float
property dcr: float

Demand-to-capacity ratio.

demand: float
is_ratio: bool = False
label: str
property ok: bool
phi: float
phi_Sn: float
reference: str
class civilpy.structural.concrete.ShearLugCheck(f_c: float, h_a: float, n_sl: int, A_ef_sl: float, h_sl: float, c_a1: float = inf, c_a2: float = inf, lambda_a: float = 1.0, has_supp_reinf: bool = False, V_ua: float = 0.0, sdc: str = 'A')[source]

Bases: object

ACI 318-19 Section 17.11 shear lug design checks.

Shear lugs transfer in-plane shear by bearing against concrete rather than through anchor rod bending. Two limit states are checked:

  1. Bearing strength of lug (17.11.2.1): V_brg,sl = n_sl × 1.7 × λ_a × f_c × A_ef,sl

  2. Concrete breakout of lug (17.11.3.2.1): V_b,sl = 3.5 × λ_a × √f_c × A_ef,sl^0.5 × h_sl^1.5 Full breakout: V_cb,sl = (A_Vc,sl / A_Vco,sl) × psi_ed × V_b,sl

All lengths in inches, forces in pounds (lb), stresses in psi.

Parameters:
  • f_c (float) – Specified concrete compressive strength (psi), capped at 10,000 psi.

  • h_a (float) – Member thickness in the direction of applied shear (in).

  • n_sl (int) – Number of shear lugs.

  • A_ef_sl (float) – Effective bearing area of a single shear lug (in²). This is the projected bearing face area perpendicular to applied shear, excluding any grout pocket area.

  • h_sl (float) – Height of shear lug above the concrete or grout surface (in).

  • c_a1 (float) – Edge distance from the lug to the nearest free edge in the direction of applied shear (in). Use float(‘inf’) if no near edge.

  • c_a2 (float) – Edge distance perpendicular to applied shear (in). Use float(‘inf’) if no near edge.

  • lambda_a (float) – Lightweight concrete factor (1.0 for normalweight).

  • has_supp_reinf (bool) – True if supplementary reinforcement is provided (Condition A, φ=0.75).

  • V_ua (float) – Factored shear demand on the lug group (lb).

  • sdc (str) – Seismic Design Category; applies 0.75 seismic reduction for SDC C–F.

bearing_strength() AnchorCheckResult[source]

Bearing strength of shear lugs — ACI 318-19 Eq. 17.11.2.1.

V_brg,sl = n_sl × 1.7 × λ_a × f_c × A_ef,sl

breakout_strength() AnchorCheckResult | None[source]

Concrete breakout strength of shear lug — ACI 318-19 Section 17.11.3.

V_b,sl = 3.5 × λ_a × √f_c × A_ef,sl^0.5 × h_sl^1.5 (Eq. 17.11.3.2.1) Returns None when c_a1 is infinite (no near edge).

check_all() dict[source]

Run bearing and breakout checks; return dict of label → AnchorCheckResult.

summary() str[source]

Formatted table of shear lug limit-state checks.

civilpy.structural.construction_staging module

Staged / phased-construction helpers for the refined-grillage bridge model.

Maintenance-of-traffic (MOT) staged construction, deck replacement, and widening all share the same moves this module supports on top of civilpy.structural.bridge_layout.grillage_model_from_layout():

  • shift traffic onto a subset of the girders – during a phase, the open lanes are squeezed over the girder lines that are carrying traffic (lanes_over_girders()), and the live/lane load is applied there;

  • place an anchored portable concrete barrier as a temporary dead load at the work-zone edge (PORTABLE_BARRIERS, add_barrier_dead_load());

  • analyze a construction phase in isolation – a stage-1 (or stage-2) structure carrying only the girders built so far, via the girder_subset phase model, so a closure pour can be checked: the wet concrete of the closure strip (closure_pour_line_load_plf()) loads each half before it cures, and the differential deflection at the joint governs the pour geometry/timing.

Everything is applied as named load cases ("MOT-LANE", "PCB", "CLOSURE", …) on the hub, so the phases go to MIDAS through the ordinary civilpy.structural.midas_models.midas_payloads() path and can be combined or run as construction stages there. Loads are placed on the girders by the transverse lever rule (the standard line-girder distribution) – a preliminary-level idealization; the deck plates still provide the real transverse distribution in the solved grillage.

Units: transverse offsets and widths in feet (layout Y, girder 1 at Y = 0); barrier/line weights in lb/ft (plf); beam loads on the hub in kip/ft, negative GZ = downward (matching the DC/DW convention).

civilpy.structural.construction_staging.LANE_LOAD_KLF = 0.64

HL-93 design lane load (AASHTO LRFD 3.6.1.2.4), kip/ft per 10 ft lane.

civilpy.structural.construction_staging.MULTIPLE_PRESENCE = {1: 1.2, 2: 1.0, 3: 0.85}

AASHTO LRFD 3.6.1.1.2 multiple-presence factor m by number of loaded lanes.

civilpy.structural.construction_staging.PORTABLE_BARRIERS: dict[str, PortableBarrier] = {'JERSEY-PORTABLE': PortableBarrier(designation='JERSEY-PORTABLE', height_in=32.0, weight_plf=400.0, anchored=False, note='unanchored precast Jersey segments'), 'PCB-32': PortableBarrier(designation='PCB-32', height_in=32.0, weight_plf=470.0, anchored=True, note='32 in F-shape portable concrete barrier, anchored'), 'PCB-42': PortableBarrier(designation='PCB-42', height_in=42.0, weight_plf=650.0, anchored=True, note='42 in tall single-slope portable barrier, anchored')}

Nominal portable-barrier catalog (confirm weights against the ODOT SCD).

class civilpy.structural.construction_staging.PortableBarrier(designation: str, height_in: float, weight_plf: float, anchored: bool = True, note: str = '')[source]

Bases: object

A temporary/portable concrete barrier used for work-zone separation.

weight_plf is the self-weight per foot of run (lb/ft); anchored notes whether it is pinned/bolted to the deck (which changes overturning and the deck anchor design, not the gravity load this module applies). Values are nominal catalog figures – confirm against the governing ODOT SCD / shop drawing for a specific product.

anchored: bool = True
designation: str
height_in: float
note: str = ''
weight_plf: float
class civilpy.structural.construction_staging.RoadwayLanes(roadway_width_ft: float, lane_width_ft: float, cl_offset: float, n_lanes: int, curb_lines: tuple[float, float], lane_edges: tuple[float, ...], lane_centers: tuple[float, ...])[source]

Bases: object

The traffic-lane layout of the roadway, in the layout transverse frame (Y, ft; girder 1 at Y = 0).

Built from a lane-width variable and the curb-to-curb roadway width: cl_offset is the roadway centerline, curb_lines the two roadway edges, lane_edges every design-lane boundary (n_lanes + 1 of them), and lane_centers the design-lane centers. These are the lines a Rhino grip interface edits (drag the CL / lane edges) and the reference positions handed to MIDAS for the lane-load live load. n_lanes follows AASHTO LRFD 3.6.1.1.1 (int(roadway_width / 12)).

cl_offset: float
curb_lines: tuple[float, float]
lane_centers: tuple[float, ...]
lane_edges: tuple[float, ...]
lane_width_ft: float
n_lanes: int
roadway_width_ft: float
civilpy.structural.construction_staging.add_barrier_dead_load(model, layout, offset_ft: float, *, designation: str = 'PCB-32', case: str = 'PCB') dict[int, float][source]

Place an anchored portable concrete barrier as a dead load at offset_ft (lever rule to the straddling girders). Returns the per-girder applied klf.

civilpy.structural.construction_staging.add_closure_pour_load(model, layout, edge_girder_line: int, *, closure_width_ft: float = 2.0, share: float = 0.5, case: str = 'CLOSURE', wet_factor: float = 1.0) float[source]

Apply the wet closure-pour weight carried by one phase to that phase’s closure-edge girder line.

share is the fraction of the strip weight this phase carries (0.5 when the joint is centered between the two phases). Applied to every element of edge_girder_line in case case. Returns the applied line load (kip/ft, negative = downward). The load sits on the deck cantilever outboard of the edge girder, so in a refined grillage it also induces a small torsion the plates carry – for the girder demand this vertical line load is the preliminary quantity.

civilpy.structural.construction_staging.add_design_lane_load(model, layout, lanes: RoadwayLanes, *, n_loaded: int | None = None, case: str = 'LL-LANE', lane_load_klf: float = 0.64, apply_multiple_presence: bool = True) dict[int, float][source]

Apply the HL-93 lane load to n_loaded of the design lanes (default: all), with the AASHTO multiple-presence factor.

The lane load is placed at each loaded lane’s center (lever rule to the girders beneath) – the distributed lane-load portion of HL-93, the piece handed to MIDAS from the Rhino-defined lanes. Returns per-girder klf. The design truck/tandem and its longitudinal positioning for the governing per-support reaction are a MIDAS moving-load run (see the notebook note) – this static lane load is the preliminary live-load reaction.

civilpy.structural.construction_staging.add_lane_load(model, layout, lane_centers: list[float], *, case: str = 'MOT-LANE', lane_load_klf: float = 0.64) dict[int, float][source]

Apply the HL-93 static lane load at each lane center (lever rule).

The AASHTO multiple-presence factor is not applied here (one or two staged lanes are usually m = 1.0/1.0); scale lane_load_klf if a different m governs. Returns the per-girder total applied klf.

civilpy.structural.construction_staging.add_line_load_at_offset(model, layout, offset_ft: float, w_plf: float, *, case: str, downward: bool = True) dict[int, float][source]

Apply a longitudinal line load (w_plf lb/ft) at a transverse offset_ft to the girders it sits between, by the lever rule.

A load between girders k and k+1 splits to each in inverse proportion to its distance; a load outboard of a fascia girder goes fully to it (an overhang load’s cantilever moment is not added here – preliminary). Returns {girder_line_no: applied_klf} (negative = downward), and adds the loads to every element of those girder lines in load case case.

civilpy.structural.construction_staging.closure_pour_line_load_plf(layout, *, closure_width_ft: float = 2.0, wet_factor: float = 1.0) float[source]

Self-weight (lb/ft of bridge length) of the wet closure-pour strip.

A staged deck is cast in phases with a longitudinal gap left between them; the closure pour fills that closure_width_ft strip last. Before it cures it is dead weight the already-built phases carry – half to each side. wet_factor scales for over-pour / screed surcharge. Uses the deck thickness and the concrete unit weight (CONCRETE_UNIT_WT_KCF).

civilpy.structural.construction_staging.design_lanes(layout, *, roadway_width_ft: float, lane_width_ft: float = 12.0, cl_offset: float | None = None, n_lanes: int | None = None) RoadwayLanes[source]

Lay out design traffic lanes across the roadway from the lane width.

cl_offset defaults to the girder-group center; n_lanes defaults to int(roadway_width_ft / 12) (AASHTO number of design lanes). The n_lanes lanes of width lane_width_ft are centered on the roadway centerline. Returns a RoadwayLanes.

civilpy.structural.construction_staging.girder_elements(model, line_no: int) list[source]

The girder beam elements of one girder line (1-based line_no).

civilpy.structural.construction_staging.lanes_over_girders(layout, girder_lines: list[int], *, n_lanes: int | None = None, lane_width_ft: float = 12.0) list[float][source]

Transverse center offsets for design lanes shifted over girder_lines.

During a construction phase the open roadway is carried on a subset of the girders; this centers n_lanes design lanes over the span of those girder lines (1-based). With n_lanes omitted, as many 12 ft lanes as fit between the outermost of girder_lines are used (at least one). Returns the lane center offsets (ft, Y) – feed each to add_lane_load() (static lane load) or use them as MIDAS traffic-line lane reference positions for a moving-load run.

civilpy.structural.construction_staging.multiple_presence(n_lanes: int) float[source]

Multiple-presence factor m for n_lanes loaded design lanes.

civilpy.structural.construction_staging.portable_barrier(designation: str = 'PCB-32') PortableBarrier[source]

Look up a PortableBarrier by designation.

civilpy.structural.continuous_beam module

Continuous-beam analysis for line girders (the offline substitute for a live MIDAS run when producing dead-load and moving-load moment envelopes).

A prismatic (or piecewise-EI) beam on simple supports is solved by the direct-stiffness / slope-deflection method with the support rotations as the only unknowns (every support pins the vertical translation). From the solved member-end moments the support reactions follow, and the bending moment at any section is then pure statics.

Moving-load envelopes come from influence lines: a unit load is walked across the span and the moment at the section of interest recorded, giving an InfluenceLine that the existing hl93_effect / maximize_axle_train machinery turns into an HL-93 envelope.

Sign convention: downward loads positive; sagging bending moment positive. Lengths in feet, loads in kips / kip-ft, ei in consistent units (its absolute value cancels for statically-determinate reactions but sets the distribution across an indeterminate beam, so only relative ei matters).

class civilpy.structural.continuous_beam.ContinuousBeam(supports: list[float], ei: float = 1.0, _loads: list = <factory>)[source]

Bases: object

A continuous beam defined by its ordered support positions (ft).

>>> b = ContinuousBeam([0.0, 20.0, 40.0])   # two 20-ft spans
>>> b.add_udl(2.0)                            # 2 klf everywhere
>>> round(b.moment_at(20.0), 3)               # interior support: -wL^2/8
-100.0
add_point(p: float, x: float) ContinuousBeam[source]

Add a point load p (kip, down +) at station x (ft).

add_udl(w: float, x0: float | None = None, x1: float | None = None) ContinuousBeam[source]

Add a uniform load w (kip/ft, down +) over [x0, x1] (default the whole beam).

deflection_diagram(e_ksi: float, i_in4: float, n: int = 1001, loads=None, xs=None)[source]

(stations, deflections) — deflection in inches, negative = downward, for a prismatic section (e_ksi in ksi, i_in4 in in^4).

The sagging-positive moment diagram (n even samples, or xs) is integrated twice as curvature M/EI; the rigid-body line is then removed by a least-squares fit through the support stations, so every support sits at (numerically) zero deflection.

diagrams(xs=None, n: int = 201, loads=None)[source]

(stations, shears, moments) at xs (or n even stations).

One reactions solve, then vectorized statics — the fast path for diagram sampling and moving-load envelopes. Shear and moment take their left-limit value at a support or point-load station.

ei: float = 1.0
property length: float
moment_at(x: float, loads=None) float[source]

Bending moment (kip-ft, sagging +) at station x by statics from the solved reactions and the loads to the left of x.

moment_diagram(n: int = 201, loads=None)[source]

(stations, moments) sampled n points along the beam.

moment_influence_line(x_section: float, *, n: int = 201) InfluenceLine[source]

Influence line for the bending moment at x_section: a unit downward load walked across the beam. Feed the result to InfluenceLine.hl93_effect for the HL-93 live-load envelope.

reactions(loads=None) list[float][source]

Support reactions (kip, up +) under the applied (or supplied) loads.

shear_at(x: float, loads=None) float[source]

Shear (kip, left-limit) at station x by statics: reactions minus applied loads to the left of x.

shear_diagram(n: int = 201, loads=None)[source]

(stations, shears) sampled n points along the beam.

supports: list[float]
class civilpy.structural.continuous_beam.EnvelopeExtreme(value: float, station: float)[source]

Bases: object

One extreme of a moving-load envelope: its value and station (ft).

station: float
value: float
class civilpy.structural.continuous_beam.MovingLoadEnvelope(stations: ndarray, moment_max: ndarray, moment_min: ndarray, shear_max: ndarray, shear_min: ndarray)[source]

Bases: object

Per-station extreme shear and moment from stepping an axle train (plus any pattern-placed lane load) across a ContinuousBeam.

Arrays share stations; conventions follow the beam (sagging moment positive, left-limit shear). These are 1D line-girder demands — distribution factors and impact are the caller’s business.

max_negative_moment() EnvelopeExtreme[source]

Most negative moment (hogging; 0 at midspan stations is normal).

max_positive_moment() EnvelopeExtreme[source]
max_shear() EnvelopeExtreme[source]

Largest absolute shear (signed value returned).

moment_max: ndarray
moment_min: ndarray
plot(ax=None)[source]

Stacked shear/moment envelope plot; returns the figure.

shear_max: ndarray
shear_min: ndarray
stations: ndarray
class civilpy.structural.continuous_beam.UnitResponses(xs: ndarray, shear: ndarray, moment: ndarray)[source]

Bases: object

The unit-load response matrices of one beam configuration on one grid, reusable across every vehicle: build once with from_beam(), then envelope() each truck against it (the per-configuration solve dominates; each envelope afterwards is a cheap gather-and-sum).

envelope(loads, positions, *, both_directions: bool = True, lane_klf: float = 0.0) MovingLoadEnvelope[source]

Envelope one axle train (see moving_load_envelope() for the parameter semantics; placements step at the grid spacing).

classmethod from_beam(beam: ContinuousBeam, step: float = 0.5)[source]

Solve the unit-load responses of beam on a grid of spacing step (ft, snapped so the supports land on grid points).

moment: ndarray
shear: ndarray
xs: ndarray
civilpy.structural.continuous_beam.m_about_a_to_shear(w_tot, m_about_a, span)[source]

Simple-span left reaction share: V_a = w_tot - m_about_a/span (the part of the load carried by the left end before the end-moment couple).

civilpy.structural.continuous_beam.moving_load_envelope(beam: ContinuousBeam, loads, positions, *, step: float = 0.5, both_directions: bool = True, lane_klf: float = 0.0) MovingLoadEnvelope[source]

Step an axle train across beam and envelope shear and moment at every station.

loads (kip) sit at positions (ft from the first axle) — pass a catalog truck as *vehicle.train() from civilpy.structural.aashto.vehicles. Axles off the beam contribute nothing (trains enter and leave the span). step sets both the station grid and the train-placement increment; axle placements between grid points interpolate linearly between unit-load rows, so keep step a divisor of the axle spacings where exactness at the peaks matters (the catalog trucks are all on 0.5-ft multiples).

lane_klf adds a uniform lane load placed patterned per station: positive influence regions only for the maxima, negative only for the minima — the adverse-placement rule, computed from the same unit-load responses. Impact and distribution factors are deliberately not applied here.

Enveloping several vehicles on the same beam? Build a UnitResponses once and call its envelope() per truck — this function rebuilds the unit-load matrices every call.

>>> b = ContinuousBeam([0.0, 100.0])
>>> env = moving_load_envelope(b, [8.0, 32.0, 32.0], [0.0, 14.0, 28.0])
>>> round(env.max_positive_moment().value, 1)   # HS20, 100-ft span
1523.9
civilpy.structural.continuous_beam.unit_response_matrices(beam: ContinuousBeam, xs)[source]

Shear and moment at every station of xs for a unit (1 kip) load at every position of xs: two (len(xs), len(xs)) arrays whose row i is the beam’s response to the unit load at xs[i].

Column k read down the rows is therefore the influence line of the effect at station xs[k] — one stiffness solve per row buys every truck, direction, and placement afterwards as a cheap gather-and-sum.

civilpy.structural.effective_length module

Column effective-length factors: the alignment-chart (nomograph) equations solved exactly, so no more reading K off a fuzzy chart.

g_a/g_b are the joint stiffness ratios sum(EI/L columns) / sum(EI/L girders); use 10 for a pinned base and 1.0 for a fixed base (the recommended practical values).

Examples

>>> round(k_factor(1.0, 1.0, sway=False), 2)
0.77
>>> round(k_factor(1.0, 1.0, sway=True), 2)
1.32
civilpy.structural.effective_length.k_factor(g_a: float, g_b: float, sway: bool = False, tol: float = 1e-09) float[source]

Effective length factor K from the alignment-chart transcendental equations — sidesway-inhibited (0.5 <= K <= 1.0) or sidesway-permitted (K >= 1.0) — solved by bisection.

civilpy.structural.flexural_stress module

Elastic flexural stress distribution over a beam cross-section.

The classic sigma = M*y/I diagram: linear stress through the depth, zero at the neutral axis, extreme-fiber values labeled. Works for any section once its depth, neutral-axis location, and moment of inertia are known (e.g. from civilpy.structural.section_properties).

Sagging (positive) moment puts the top fiber in compression, drawn to the left of the zero line, matching the hand-sketch convention.

Examples

>>> round(flexural_stress(1200.0, 800.0, 6.0), 3)   # M=1200 k-in, I=800
9.0
>>> sigma_top, sigma_bot = extreme_fiber_stresses(1200.0, 800.0,
...                                               c_top=8.0, c_bot=4.0)
>>> round(sigma_top, 2), round(sigma_bot, 2)
(-12.0, 6.0)
civilpy.structural.flexural_stress.extreme_fiber_stresses(m_kip_in: float, i_in4: float, *, c_top: float, c_bot: float) tuple[float, float][source]

(sigma_top, sigma_bot) in ksi with the structural sign convention: compression negative. Positive (sagging) moment gives a negative top stress and positive bottom stress.

civilpy.structural.flexural_stress.flexural_stress(m_kip_in: float, i_in4: float, y_in: float) float[source]

Bending stress magnitude M*y/I (ksi) at distance y_in from the neutral axis.

civilpy.structural.flexural_stress.plot_flexural_stress(m_kip_in: float, i_in4: float, *, depth_in: float, y_bar_in: float, width_in: float | None = None, ax=None)[source]

Draw the linear elastic stress distribution through the depth.

y_bar_in locates the neutral axis above the bottom fiber, so asymmetric sections (composite girders, tees) show their unequal extreme-fiber stresses. When width_in is given a rectangular section silhouette is sketched beside the diagram for scale. Compression plots left of the zero line (negative), tension right. Returns the figure.

civilpy.structural.girder_optimizer module

Rolled-girder + field-splice optimizer (stage B5 – “a better design”).

Sweeps candidate rolled shapes and, for each, (1) checks the girder carries the span’s governing factored moment (φ·Mp = φ·Fy·Zx, the compact-shape plastic moment – a conservative gate that ignores the extra composite capacity), and (2) auto-sizes the flange/web splice plates and designs the bolted field splice with the ODOT/NSBA method. Feasible shapes (girder OK and splice OK) are ranked by total cost – girder steel weight plus splice fabrication/erection – so the lightest adequate design surfaces, to compare against the as-built.

Cost model (all parameters overridable): steel at $/lb over the girder length, plus per-bolt $fab fabrication and field_min erection minutes at $/min. The absolute dollars are illustrative; the ranking is the point.

This is the splice-region optimizer; a full B5 also folds in Service II, fatigue, 6.10.2 proportion, and deflection limit states and lets the section vary between splice pieces – built on this same sweep.

class civilpy.structural.girder_optimizer.GirderOption(shape: str, weight_lb_ft: float, steel_lb: float, total_bolts: int, steel_cost: float, splice_cost: float, total_cost: float, girder_ok: bool, splice_ok: bool)[source]

Bases: object

One candidate shape’s feasibility + cost.

girder_ok: bool
property ok: bool
shape: str
splice_cost: float
splice_ok: bool
steel_cost: float
steel_lb: float
total_bolts: int
total_cost: float
weight_lb_ft: float
civilpy.structural.girder_optimizer.auto_splice_plates(left_label: str, right_label: str, grade: str = 'Grade 50', *, weld_size: float = 0.3125)[source]

Proportion the flange (PlatePair) and web (WebPlate) splice plates for a shape pair using the NSBA sizers + the AISC geometry.

civilpy.structural.girder_optimizer.cheapest_feasible(options)[source]

The lowest-cost fully-feasible GirderOption, or None.

civilpy.structural.girder_optimizer.girder_plastic_moment(label: str, f_y: float = 50.0) float[source]

Plastic moment capacity Mp = Fy*Zx (kip-ft) of a rolled shape.

civilpy.structural.girder_optimizer.optimize_splice_shape(loads, candidates, *, length_ft: float, max_factored_moment: float, deck_thickness: float, deck_eff_width: float, rebar_area: float = 0.0, deck_fc: float = 4.0, grade: str = 'Grade 50', f_y: float = 50.0, bolt_dia: float = 0.875, steel_cost_per_lb: float = 1.5, bolt_fab: float = 20.0, field_min_per_bolt: float = 10.0, labor_per_min: float = 1.5, design_year: int = 2016)[source]

Sweep candidates (uniform-shape girders) and return a list of GirderOption, ranked feasible-first then by total_cost.

loads is the unfactored splice demand; max_factored_moment is the girder’s governing Strength I moment (from the envelope) used for the plastic-moment gate. Shapes whose flanges are too narrow to splice are skipped.

civilpy.structural.girder_optimizer.weight_lb_ft(label: str) float[source]

Nominal weight (lb/ft) of an AISC shape from steel.W (falls back to the trailing number in the label, which is the nominal weight).

civilpy.structural.girder_pipeline module

Line-girder field-splice placement (stage G6).

Given a girder’s moving-load moment envelope (per load case, sampled along the span), decide how many field splices shipping length forces and where to put them: in the low-moment windows near the dead-load contraflexure points, subject to every field piece being no longer than the shippable length.

n_splices = ceil(L / L_ship) - 1

Each candidate is returned with the unfactored per-case demand set at its station, ready to hand to the splice designer as a SpliceLoads. The engineer accepts or drags the gdr.kind=splice markers G8 then stamps into the .3dm.

This module is pure: it operates on sampled envelopes (from MIDAS via G5, or any line-girder analysis) and never touches a live session.

class civilpy.structural.girder_pipeline.SpliceCandidate(station: float, factored_moment: float, loads: SpliceLoads)[source]

Bases: object

A suggested field-splice location and the demand there.

factored_moment: float
loads: SpliceLoads
station: float
civilpy.structural.girder_pipeline.girder_line_envelope(supports, *, dc1_klf, dc2_klf=0.0, dw_klf=0.0, n_sections: int = 41, gdf: float = 1.0, im: float = 0.33, lane_klf: float = 0.64, il_samples: int = 201)[source]

Per-case moment envelope for one continuous girder line.

dc1_klf (non-composite dead load: steel + wet slab), dc2_klf (barrier/SDL), and dw_klf (future wearing surface) are uniform loads; the live load is the HL-93 moment envelope from an influence line at each section, scaled by the girder distribution factor gdf.

Returns (stations, moments) where moments has the dc1/dc2/dw/ ll_pos/ll_neg keys place_splices() consumes.

civilpy.structural.girder_pipeline.hl93_pos_neg(il, *, im: float = 0.33, lane_klf: float = 0.64)[source]

Governing positive and negative HL-93 effects for one influence line.

Positive uses InfluenceLine.hl93_effect() (truck/tandem placed for the maximum, lane load on the positive influence area). Negative mirrors it: the most-negative truck/tandem placement with the lane load on the negative area (a reasonable single-truck approximation of 3.6.1.3.1 – the two-truck negative-moment rule for interior supports is a refinement).

civilpy.structural.girder_pipeline.n_field_splices(length_ft: float, ship_max_ft: float) int[source]

Number of field splices shipping length forces on a girder of length_ft when the longest shippable piece is ship_max_ft.

civilpy.structural.girder_pipeline.place_splices(stations, moments, ship_max_ft, *, samples: int = 400)[source]

Suggest field-splice stations for one girder.

Parameters:
  • stations (list[float]) – Increasing sample positions along the girder (ft), from 0 to L.

  • moments (dict[str, list[float]]) – Per-case moments (k-ft) aligned with stations. Must include the keys dc1, dc2, dw, ll_pos, ll_neg. dc1 (the non-composite dead load) locates the contraflexure the splice hides in.

  • ship_max_ft (float) – Longest shippable field piece (gdr.ship_max).

  • samples (int) – Search resolution across each splice’s feasible shipping window.

Returns:

n_field_splices(L, ship_max) candidates, ordered along the span, each at the lowest-magnitude Strength I station within its shipping-feasible window (contraflexure wins, since dead + live both pass through zero there).

Return type:

list[SpliceCandidate]

civilpy.structural.influence_lines module

Influence lines for statically determinate beams (overhangs allowed), with axle-train stepping for moving-load maxima — the classic line-girder approach, so ordinates can be compared against any rating software.

Conventions follow civilpy.structural.beam_bending: kips/ft, plots accept ax=None and return the figure. Unit load is 1 kip downward; sign conventions are the usual ones (positive reaction up, positive moment sagging, shear by the left-segment rule).

Examples

>>> il = InfluenceLine.moment(span=20, section=10)   # midspan moment
>>> round(il.eta(10), 4)                             # peak = L/4
5.0
>>> peak = il.maximize_axle_train([32, 32, 8], [0, 14, 28])
>>> round(peak.value, 1)                             # one axle fits the peak
160.0
>>> round(il.uniform_load_effect(0.64), 1)           # 0.64 klf lane load
32.0
class civilpy.structural.influence_lines.InfluenceLine(eta, length: float, label: str = '')[source]

Bases: object

Influence line for one effect (reaction, shear, or moment at a section) on a beam with a pin at support_a and roller at support_b; the beam runs from 0 to length (defaults to support_b), so overhangs come from supports inside the ends.

eta(x)[source]

Ordinate(s) at unit-load position(s) x: a float for scalar input, an array for array input.

hl93_effect(im: float = 0.33, lane_klf: float = 0.64, rear_spacings=(14.0, 30.0)) dict[source]

Governing HL-93 effect on this line (3.6.1.3): design truck (rear-axle spacing swept) with dynamic load allowance, plus lane load on the positive regions. Returns the pieces and the total.

maximize_axle_train(loads, positions, step: float = 0.05, both_directions: bool = True, sign: float = 1.0) TrainResult[source]

Step an axle train across the beam and return the extreme effect. loads (kips) sit at positions (ft, from the first axle); axles off the beam contribute nothing. sign=-1 finds the most negative effect instead.

classmethod moment(span: float, section: float, support_a: float = 0.0, support_b: float | None = None, length: float | None = None)[source]

IL for bending moment at section.

ordinates(n: int = 1001)[source]
plot(ax=None, n: int = 1001, axle_train=None)[source]

Plot the influence line (positive area shaded), optionally with an axle train drawn at its governing position.

classmethod reaction(span: float, support: str = 'A', support_a: float = 0.0, support_b: float | None = None, length: float | None = None)[source]

IL for a vertical reaction. span is shorthand for supports at 0 and span; or place them with support_a/support_b.

classmethod shear(span: float, section: float, support_a: float = 0.0, support_b: float | None = None, length: float | None = None)[source]

IL for shear at section (left-segment sign convention).

classmethod two_span_moment(spans, section: float)[source]

IL for bending moment at section (ft from A) of a two-span continuous beam. section = L1 gives the classic negative-moment IL over the middle support.

classmethod two_span_reaction(spans, support: str = 'B')[source]

IL for a reaction of a two-span continuous beam with supports A-B-C and (possibly unequal) spans = (L1, L2). The middle reaction’s IL is the Müller-Breslau deflected shape of the beam with support B released.

classmethod two_span_shear(spans, section: float)[source]

IL for shear at section (ft from A) of a two-span continuous beam, left-segment sign convention.

uniform_load_effect(w: float, positive_only: bool = True, n: int = 2001) float[source]

Effect of a uniform load w (klf): w times the influence-line area — positive regions only by default, the way lane load is placed to maximize.

class civilpy.structural.influence_lines.TrainResult(value: float, position: float, reversed_train: bool)[source]

Bases: object

Extreme effect of an axle train on an influence line.

position: float
reversed_train: bool
value: float
civilpy.structural.influence_lines.influence_line_from_midas(midas, element, cases, *, component='Moment-y', part='Part I', length=None, label='', unit=None)[source]

Assemble an InfluenceLine for one beam-element force effect from a set of unit-load analysis cases already solved in a MIDAS model.

cases is an ordered iterable of (position_ft, load_case_name) pairs, one per unit-load (1 kip) position along the lane. This reads midas.beam_forces([element], …) once for all the cases, takes component (e.g. "Moment-y" / "Shear-z") at the given part ("Part I" or "Part J") of element under each case, and returns the interpolated influence line — ready for InfluenceLine.maximize_axle_train() permit runs.

This does not create or modify load cases; it reads the response of unit cases you (or a generator) set up, so it stays side-effect free and works against whatever load-application scheme the model uses. Reactions and displacements follow the same pattern via MidasCivil.result_table.

civilpy.structural.influence_lines.influence_line_from_ordinates(positions, etas, length=None, label='')[source]

Build an InfluenceLine from sampled ordinates.

positions (ft, ascending) and etas are paired samples of an influence line obtained from any source — a MIDAS unit-load analysis, a finite-element model, or measured data — letting the full axle-train machinery (InfluenceLine.maximize_axle_train(), hl93_effect(), uniform_load_effect()) run permit and rating trucks against a real structure rather than only the closed-form prismatic cases.

The line is reconstructed by linear interpolation between samples (numpy.interp); load positions off the sampled range contribute zero. Accuracy is therefore limited by the sample spacing — and, because an influence line presumes linear superposition, by how linear the source structure is (see the walkthrough notes on nonlinear bridges).

length defaults to the largest position.

civilpy.structural.line_girder_tool module

Basic line-girder shear / moment / deflection tool.

The simplest useful form of a line-girder run, with no Rhino or MIDAS dependence: pick a handful of settings (barrier type, AASHTO vehicle, deck thickness, span arrangement, girder size/spacing), get back

  1. the loads the tool derived from each selection (klf per girder, plus the AASHTO distribution factors), and

  2. shear, moment, and deflection diagrams for an interior and an exterior girder — dead load exactly, live load as a stepped-vehicle / patterned-lane envelope.

The beam solver behind the diagrams is ContinuousBeam; it and the envelope logic here are verified against the AISC Steel Construction Manual Table 3-23 beam cases in tests/structural/test_line_girder_tool.py (cases 1, 29, 30, 43, 44) and tests/structural/test_beam_bending.py.

Simplifications, kept loud on purpose (this is a first-pass tool):

  • equal spans, prismatic girder, bare-steel I for every deflection (no composite section);

  • both barriers shared equally by all girders (a common DC2 practice);

  • HL-93 negative moment uses a single truck (the 90 % two-truck rule of 3.6.1.3.1 is a refinement this tool skips);

  • live-load deflection distributes m x n_lanes / n_girders per AASHTO 2.5.2.6.2, using the truck alone.

civilpy.structural.line_girder_tool.BARRIER_CHOICES = ('BR-1 (36 in)', 'BR-1 (42 in)', 'SBR-3 (36 in)', 'SBR-1 (42 in)', 'SBR-2 (57 in median)', 'SBR-2 (57 in median, back-to-back)', 'TST-2 (three steel tube)')

Catalog barriers with a computable dead weight — the dropdown choices.

class civilpy.structural.line_girder_tool.BridgeConfig(n_spans: int = 3, span_ft: float = 50.0, girder: str = 'W24X104', n_girders: int = 5, spacing_ft: float = 7.0, overhang_ft: float = 2.5, deck_t_in: float = 8.5, haunch_in: float = 1.0, barrier: str = 'SBR-1 (42 in)', vehicle: str = 'HL-93 (governing of truck/tandem + lane)', fws_ksf: float = 0.06, e_ksi: float = 29000.0)[source]

Bases: object

One dropdown’s worth of settings for the basic line-girder tool.

barrier: str = 'SBR-1 (42 in)'

A key of BARRIER_CHOICES (catalog railings with a computable dead weight).

property barrier_base_ft: float
deck_t_in: float = 8.5
property deck_width_ft: float
e_ksi: float = 29000.0
fws_ksf: float = 0.06
girder: str = 'W24X104'

AISC W-shape label from civilpy.structural.steel.

haunch_in: float = 1.0
n_girders: int = 5
property n_lanes: int
n_spans: int = 3
overhang_ft: float = 2.5
property roadway_ft: float
spacing_ft: float = 7.0
span_ft: float = 50.0
property supports: list[float]
vehicle: str = 'HL-93 (governing of truck/tandem + lane)'

A key of VEHICLES (HL-93 truck / tandem / governing, HS-20 and the Ohio legal trucks).

civilpy.structural.line_girder_tool.CONCRETE_KCF = 0.15

Reinforced-concrete unit weight (kcf) for deck and barrier dead load.

class civilpy.structural.line_girder_tool.GirderDiagrams(loads: GirderLoads, stations: list[float], v_dead: ndarray, m_dead: ndarray, d_dead: ndarray, v_ll_pos: ndarray, v_ll_neg: ndarray, m_ll_pos: ndarray, m_ll_neg: ndarray, d_ll: ndarray)[source]

Bases: object

Sampled diagrams for one girder (kip / kip-ft / inches; station ft).

d_dead: ndarray
d_ll: ndarray
property d_total
loads: GirderLoads
m_dead: ndarray
m_ll_neg: ndarray
m_ll_pos: ndarray
property m_total_neg
property m_total_pos
stations: list[float]
v_dead: ndarray
v_ll_neg: ndarray
v_ll_pos: ndarray
property v_total_neg
property v_total_pos
class civilpy.structural.line_girder_tool.GirderLoads(label: str, trib_ft: float, girder_klf: float, deck_klf: float, dc1_klf: float, barrier_klf: float, dc2_klf: float, dw_klf: float, df_moment: float, df_shear: float, df_deflection: float, im: float, lane_klf: float)[source]

Bases: object

The per-girder loads the tool derived from the user’s selections.

barrier_klf: float
dc1_klf: float
dc2_klf: float
property dead_total_klf: float
deck_klf: float
df_deflection: float
df_moment: float
df_shear: float
dw_klf: float
girder_klf: float
im: float
label: str
lane_klf: float
trib_ft: float
class civilpy.structural.line_girder_tool.LineGirderResult(config: ~civilpy.structural.line_girder_tool.BridgeConfig, loads: dict[str, ~civilpy.structural.line_girder_tool.GirderLoads], girders: dict[str, ~civilpy.structural.line_girder_tool.GirderDiagrams] = <factory>)[source]

Bases: object

Everything analyze() produced: config, loads, and diagrams.

config: BridgeConfig
girders: dict[str, GirderDiagrams]
loads: dict[str, GirderLoads]
civilpy.structural.line_girder_tool.VEHICLES = {'HL-93 (design tandem + lane)': {'im': 0.33, 'lane_klf': 0.64, 'trains': [([25.0, 25.0], [0.0, 4.0])]}, 'HL-93 (design truck + lane)': {'im': 0.33, 'lane_klf': 0.64, 'trains': [([8.0, 32.0, 32.0], [0.0, 14.0, 28.0]), ([8.0, 32.0, 32.0], [0.0, 14.0, 44.0])]}, 'HL-93 (governing of truck/tandem + lane)': {'im': 0.33, 'lane_klf': 0.64, 'trains': [([8.0, 32.0, 32.0], [0.0, 14.0, 28.0]), ([8.0, 32.0, 32.0], [0.0, 14.0, 44.0]), ([25.0, 25.0], [0.0, 4.0])]}, 'HS20-44 (truck only, Std. Spec. impact)': {'im': None, 'lane_klf': 0.0, 'trains': [([8.0, 32.0, 32.0], [0.0, 14.0, 28.0])]}}

Axle trains (loads kip, offsets ft) per selectable design vehicle. HL-93 rear-axle spacing varies 14-30 ft; stepping both bounding trains captures the governing spread for negative moment over interior supports.

civilpy.structural.line_girder_tool.analyze(cfg: BridgeConfig, *, n: int = 401) LineGirderResult[source]

Run the basic line-girder tool for one configuration.

civilpy.structural.line_girder_tool.barrier_weight_klf(designation: str) float[source]

Dead load of one barrier run (klf) from the ODOT railing catalog: gross concrete area x unit weight, else the catalog steel weight.

civilpy.structural.line_girder_tool.compute_loads(cfg: BridgeConfig) dict[str, GirderLoads][source]

Loads + distribution factors for the interior and exterior girders.

civilpy.structural.line_girder_tool.lane_load_envelope(supports, w_klf: float, *, n: int = 401, xs=None)[source]

Envelope of a uniform lane load placed on every combination of spans (the adverse-span patterning of AASHTO 3.6.1.3.1, done exhaustively — at most 2**n_spans - 1 load cases). Same return keys as moving_load_envelope() (without deflection).

civilpy.structural.line_girder_tool.loads_table(result: LineGirderResult)[source]

The loads the tool associated with each selection, as a DataFrame (rows = load items, columns = interior / exterior girder).

civilpy.structural.line_girder_tool.moving_load_envelope(supports, trains, *, n: int = 401, xs=None, step: float | None = None, e_ksi: float | None = None, i_in4: float | None = None)[source]

Step each axle train (both travel directions) across the beam and envelope shear, moment, and (optionally) deflection at n stations.

trains is a list of (axle_loads_kip, offsets_ft) pairs; axles off the beam contribute nothing. Stations are n even samples (or xs when given). Returns a dict with stations and the v_max/v_min/m_max/m_min envelopes (kip, kip-ft), plus d_min (inches, most-downward) when e_ksi/i_in4 are given.

civilpy.structural.line_girder_tool.plot_diagrams(result: LineGirderResult, figsize=(12, 9))[source]

Shear / moment / deflection, interior and exterior girder side by side.

Gray = dead load alone; colored band = dead + live-load envelope.

civilpy.structural.midas module

MIDAS Civil API request templates and helpers.

Prebuilt JSON payloads and helpers for the MIDAS Civil (MOTIVE) REST API: requesting analysis-result tables (beam forces, reactions) and mapping result cases, used by the model-interrogation tools in civilpy.structural.midas_models.

exception civilpy.structural.midas.MidasApiError(message, response=None)[source]

Bases: RuntimeError

Raised when the MIDAS API returns an HTTP or application error.

class civilpy.structural.midas.MidasCivil(base_url=None, mapi_key=None, timeout=60, secrets_path=None, analysis_timeout=None, reconnect_retries=None, retry_backoff=None)[source]

Bases: object

A connection to a running MIDAS Civil NX (or Gen NX) instance through the MIDAS API — the same API the MIDAS GH Rhino/Grasshopper extension uses, so anything sent from here lands in the connected session.

Three lines:

from civilpy.structural.midas import MidasCivil

midas = MidasCivil()              # key from ~/secrets.json MIDAS_API_KEY
nodes = midas.nodes()             # GET  /db/NODE
midas.put_nodes({"1": {"X": 0, "Y": 0, "Z": 0}})   # PUT /db/NODE

Coverage:

Unlike the module-level functions above (kept for backwards compatibility), this class holds no global state, supports custom base URLs (regional/local API servers), applies request timeouts, and raises MidasApiError instead of printing.

ANALYSIS_TIMEOUT = 600

Read timeout (s) for long operations — analysis, open, result tables — on large finite-element models (a 10k-element solve exceeds the default). Override per instance with analysis_timeout= for very large models.

DEFAULT_BASE_URL = 'https://moa-engineers.midasit.com:443/civil'
RECONNECT_RETRIES = 2

Default retry policy for transport (connection) failures. A dropped Civil NX session is the failure that clusters mid-batch, so one or two backoff retries recover most blips without masking a real outage.

RETRY_BACKOFF = 0.5
TRIAGE_TABLES = ('NODE', 'ELEM', 'CONS', 'STLD', 'MVLD', 'LCOM-GEN', 'SECT')

Tables read to judge whether a model can be analyzed / rated.

analyze()[source]

POST /doc/ANAL — run the analysis on the current model (long timeout; a large finite-element solve exceeds the default request timeout).

beam_forces(elem_ids, load_case_names, components=('Elem', 'Load', 'Part', 'Axial', 'Shear-y', 'Shear-z', 'Moment-y', 'Moment-z'), unit=None, styles=None, validate=True)[source]

POST /post/TABLE BEAMFORCE in the request shape confirmed against live Civil NX: integer element ids in NODE_ELEMS["KEYS"] plus UNIT, STYLES, and PARTS — omitting any of those returns the "second query is wrong" HTTP 400. elem_ids are integer beam element ids; load_case_names use the result suffixes ("…(ST)" static, "…(MV:all)" moving). Returns the raw /post/TABLE JSON (flatten with civilpy...()/brr.rating.parse_result_table).

When validate is set (default), the returned table is checked for duplicate column headers via duplicate_columns() and a warning is issued if any are found — guarding against the component aliasing that produced identical Axial / Shear-z envelopes on some models.

can_analyze()[source]

True when the current model has enough defined to run an analysis.

capability(summary=None)[source]

(can_analyze, can_rate_live_load, note) for the current model.

A model needs geometry, supports, and at least one load case to solve; live-load rating additionally needs a moving-load case. Pass a cached summarize() result as summary to avoid a second round-trip.

delete_db(table, ids=None)[source]

DELETE /db/{table} — remove ids (everything when ids is None).

static duplicate_columns(response)[source]

Column headers that appear more than once in a /post/TABLE response. A non-empty result flags a component→column aliasing problem (e.g. Axial and Shear-z resolving to the same field, seen on some BEAMFORCE result shapes) — the values under aliased headers are unreliable and should be re-queried.

elements()[source]

All elements — {"ELEM": {id: {TYPE, MATL, SECT, NODE, ...}}}.

export_file(path)[source]

POST /doc/EXPORT — export the model to a file path.

get_db(table)[source]

GET /db/{table} — full contents of any model database table.

groups()[source]

Structure groups — {"GRUP": {...}}.

import_file(path)[source]

POST /doc/IMPORT — import a file (e.g. .mct) into the model.

load_combinations()[source]

General load combinations — {"LCOM-GEN": {...}}.

materials()[source]
new()[source]

POST /doc/NEW — start a blank model (discards unsaved work).

nodes()[source]

All nodes — {"NODE": {id: {X, Y, Z}}}.

open(path)[source]

POST /doc/OPEN — open an .mcb model file on the Civil NX machine (long timeout; large files take time to load).

ping()[source]

True when Civil NX is reachable and the key is accepted.

post_db(table, assign)[source]

POST /db/{table} — create rows (errors when an id exists).

put_db(table, assign)[source]

PUT /db/{table} — create/update rows. assign maps id → fields.

put_elements(assign)[source]

Create/update elements: {id: {"TYPE": "BEAM", "MATL":, "SECT":, "NODE": [i, j]}}.

put_materials(assign)[source]

Create/update materials: {id: {"TYPE": "STEEL"|"CONC"|"USER", "NAME":, ...}}.

put_nodes(assign)[source]

Create/update nodes: {id: {"X":, "Y":, "Z":}}.

put_sections(assign)[source]
put_static_loads(assign)[source]
put_supports(assign)[source]

Create/update supports: {node_id: {"ITEMS": [{"ID": 1, "CONSTRAINT": "1110000"}]}}.

request(method, command, body=None, timeout=None, retries=None)[source]

Send one request to the MIDAS API and return the parsed JSON.

command is the endpoint path, e.g. "/db/NODE" or "/doc/ANAL". timeout overrides the instance default for one call (used by analyze(), open(), and result_table() on large models). retries overrides reconnect_retries for one call (0 disables the backoff retry).

Error handling distinguishes three failure modes so a batch caller can react sensibly:

  • MidasConnectionError — the API was unreachable (dropped session); retried with backoff first, then raised.

  • MidasTimeoutError — the request outran its read timeout (a large solve); raised immediately, not retried.

  • MidasLicenseError — the license tier rejected the model (HTTP 400, e.g. too many construction stages).

All three subclass MidasApiError, as does a generic HTTP or {"error": ...} payload failure.

result_table(table_name, table_type=None, components=None, node_elems=None, load_case_names=None, parts=None, unit=None, styles=None, timeout=None)[source]

POST /post/TABLE — extract an analysis results table.

Example:

midas.result_table(
    "BeamForce", table_type="BEAMFORCE",
    components=["Elem", "Load", "Moment-y"],
    node_elems={"KEYS": ["1to20"]},
    load_case_names=["DL(CB)"],
)

For element-force tables, prefer beam_forces(), which sends the request shape confirmed to work against live Civil NX. timeout overrides the request timeout for large result sets.

save()[source]

POST /doc/SAVE — save the current model.

save_as(path)[source]

POST /doc/SAVEAS — save the model to a new path.

sections()[source]
set_units(force='KIPS', dist='FT', heat='BTU', temper='F')[source]

Set the model unit system (defaults: kips / feet).

static_loads()[source]

Static load cases — {"STLD": {...}}.

summarize()[source]

Counts of the current model’s structural ingredients.

One round-trip per TRIAGE_TABLES entry; returns a dict with nodes, elems, elem_types, sect_types, supports, static_loads, moving_loads and combinations. Feed it to capability() to decide whether a model is worth analyzing before spending a long solve on it.

supports()[source]

Nodal boundary conditions — {"CONS": {...}}.

units()[source]
exception civilpy.structural.midas.MidasConnectionError(message, response=None)[source]

Bases: MidasApiError

The MIDAS API could not be reached — Civil NX is closed, the session dropped, or the network blipped. Transport-level and typically transient, so MidasCivil.request() retries it before giving up. Subclasses MidasApiError so existing except MidasApiError paths still catch it.

exception civilpy.structural.midas.MidasLicenseError(message, response=None)[source]

Bases: MidasApiError

The connected Civil NX license tier rejected the operation — e.g. opening a model with more construction stages than a Plus license allows. Not retryable: skip the model rather than re-sending.

exception civilpy.structural.midas.MidasTimeoutError(message, response=None)[source]

Bases: MidasConnectionError

A request outran its read timeout — usually a large finite-element solve or model open that needs a longer analysis_timeout rather than a retry (re-issuing it just doubles the wait).

civilpy.structural.midas.column_values(rows, column, cast=<class 'float'>)[source]

Pull one column from parsed rows, dropping blanks and un-castable cells.

civilpy.structural.midas.convert_node_units(from_units: str = None, to_units: str = None, in_place: bool = True) None[source]

Converts the existing nodes from one unit system to another, for instance if you import a dxf file that was drawn in feet, but your midas units were set to inches, :param from_units: :type from_units: str = The units the drawing is currently in ie ‘inches’ will grab them from midas if None :param to_units: :type to_units: str = The desired units to convert the drawing to, if None, will not run :param in_place: :type in_place: bool = Whether you want the function to push the data to the midas, or just return the updated values

Returns:

  • None - Converts the units in the open model to the desired units

  • nodes - if in_place is False

civilpy.structural.midas.envelope(rows, column, *, absolute=True)[source]

Largest value of column across rows (by magnitude unless told not to).

Returns 0.0 for no usable values — the controlling demand of an empty set is nothing.

civilpy.structural.midas.get_api_key(secrets_path=None)[source]

Retrieve the API key from the secrets.json file in the civilpy directory

Parameters:

secrets_path (the path where the secrets.json file is located)

Return type:

None - Sets a global variable for the module

civilpy.structural.midas.get_elements()[source]

Retrieve all elements from the active MIDAS Civil model.

Returns:

MIDAS API response containing element definitions keyed by element ID under "ELEM".

Return type:

dict

civilpy.structural.midas.get_elements_by_material_index(material_index: int = None)[source]

Get the elements of a model by specifying a material index value :param material_index: :type material_index: int = Index of the material you want to sort by in str format

Returns:

return_dict

Return type:

dict = The elements of a model that match the material specified

civilpy.structural.midas.get_elements_by_section_index(section_index: int = None)[source]

Returns a dictionary containing all elements of the midas model that match the section index

Parameters:

section_index (int = The index value of the section you want to search the elements for)

Returns:

return_dict – specified

Return type:

A dictionary containing all elements of the midas model that match the section

civilpy.structural.midas.get_materials()[source]

Retrieve all material definitions from the active MIDAS Civil model.

Returns:

MIDAS API response containing material properties keyed by material ID under "MATL".

Return type:

dict

civilpy.structural.midas.get_nodes()[source]

Retrieve all nodes from the active MIDAS Civil model.

Returns:

MIDAS API response containing node coordinates keyed by node ID under "NODE".

Return type:

dict

civilpy.structural.midas.get_sections()[source]

Retrieve all section definitions from the active MIDAS Civil model.

Returns:

MIDAS API response containing section properties keyed by section ID under "SECT".

Return type:

dict

civilpy.structural.midas.get_static_loads()[source]

Retrieve all static load case definitions from the active MIDAS Civil model.

Returns:

MIDAS API response containing static load cases keyed by load case ID under "STLD".

Return type:

dict

civilpy.structural.midas.get_supports()[source]

Retrieve all support (constraint) definitions from the active MIDAS Civil model.

Returns:

MIDAS API response containing nodal boundary conditions keyed by node ID under "CONS".

Return type:

dict

civilpy.structural.midas.get_units()[source]

Retrieve the current unit system settings from the active MIDAS Civil model.

Returns:

MIDAS API response containing force, length, heat, and temperature unit settings.

Return type:

dict

civilpy.structural.midas.midas_api(method, command, body=None)[source]

Make a request to the MIDAS API and return the response as a JSON object

Parameters:
  • method (str - 'GET', 'PUT', 'POST' or 'DELETE')

  • command (str - The particular method within the API you want) – to target, such as ‘db/elem’ for the elements you want to access

  • body (dict - The body of the request you want to send to the api, usually contains) – the values you want MIDAS to update with

Return type:

response.json - the response from the MIDAS API

civilpy.structural.midas.parse_result_table(response, table_name=None)[source]

Flatten a MIDAS POST /post/TABLE response into a list of row dicts.

MIDAS returns {<table>: {"HEAD": [col, ...], "DATA": [[val, ...], ...]}}. When table_name is omitted the first block that carries HEAD/DATA is used, so parse_result_table(resp) works without knowing the key. Cell values are left as MIDAS sent them (usually strings); cast with column_values().

civilpy.structural.midas.setup_output_directory(output_directory: str = '/builds/Dane/civilpy')[source]

Create an output subdirectory inside output_directory if absent.

Parameters:

output_directory (str) – Parent directory path. Defaults to the current working directory.

Note

Prints a status message indicating whether the directory was found or created.

civilpy.structural.midas_models module

Payload builders for the bridge model types MIDAS Civil NX is built for.

These are pure functions: each returns the {id: {...}} assign dictionaries the MIDAS API expects, so they can be unit-tested with no live session and then pushed with civilpy.structural.midas.MidasCivil.put_db(table, assign). They cover the geometries a line-girder framing plan cannot express:

Table schemas (NODE, ELEM, CONS, FRLS, ELNK, RIGD) follow the /db/* manual pages. The point-spring table name is not yet verified against a live releasesoil_spring_supports() returns the body and takes the table name as an argument; capture a real one per the live-capture checklist before relying on it.

Units: geometry in the model’s length unit (feet in the civilpy/BrR default KIPS/FT system); spring stiffnesses are passed through in model units – use lb_per_in_to_kip_per_ft() to convert the geotech curves’ lb/in values. Constraint/release flag strings are DX DY DZ RX RY RZ RW (‘1’ = fixed for CONS, ‘1’ = released for FRLS), matching the rest of the API client.

civilpy.structural.midas_models.BDM_908_DESIGN_DB = {'HL-93 tandem': ('AASHTO-LRFD', 'HL-93TDM'), 'HL-93 truck': ('AASHTO-LRFD', 'HL-93TRK'), 'HS20-44 lane': ('AASHTO-STD', 'HS20-44L'), 'HS20-44 truck': ('AASHTO-STD', 'HS20-44')}

The design loads ODOT BDM 908.2 specifies for inventory/operating ratings, as {label: (STANDARD_CODE, VEHICLE_TYPE_NAME)}. Figures 908.2-1/-2 reproduce the standard AASHTO definitions unchanged (HS20 lane 0.640 klf with the 18 kip moment / 26 kip shear concentrated loads; HL-93 tandem 25 kip axles at 4 ft), so the DB entries apply.

civilpy.structural.midas_models.BDM_908_LANE_LOADS = frozenset({'HS20-44 lane'})

Entries in BDM_908_DESIGN_DB that are a LANE load rather than a truck or tandem. BDM 924.4.C: “Dynamic load allowance shall only be applied to the truck or tandem portion of HL93 loading (dynamic load allowance shall not be provided to the lane portion).” These get IM = 0.

civilpy.structural.midas_models.BDM_908_STANDARD_DB = {'2F1': ('OHDOT LOAD', 'OH Legal load 2F1'), '3F1': ('OHDOT LOAD', 'OH Legal load 3F1'), '4F1': ('OHDOT LOAD', 'OH Legal load 4F1'), '5C1': ('OHDOT LOAD', 'OH Legal load 5C1'), 'EV2': ('FAST ACT EV LOADS', 'Type EV2'), 'EV3': ('FAST ACT EV LOADS', 'Type EV3'), 'SU4': ('AASHTO LEGAL/PERMIT LOAD', 'AASHTO Posting load SU4'), 'SU5': ('AASHTO LEGAL/PERMIT LOAD', 'AASHTO Posting load SU5'), 'SU6': ('AASHTO LEGAL/PERMIT LOAD', 'AASHTO Posting load SU6'), 'SU7': ('AASHTO LEGAL/PERMIT LOAD', 'AASHTO Posting load SU7'), 'Type 3': ('AASHTO LEGAL/PERMIT LOAD', 'AASHTO Legal Type 3'), 'Type 3-3': ('AASHTO LEGAL/PERMIT LOAD', 'AASHTO Legal Type 3-3'), 'Type 3S2': ('AASHTO LEGAL/PERMIT LOAD', 'AASHTO Legal Type 3S2')}

Midas standard-DB entries for the ODOT BDM 908.3 rating vehicles, as {civilpy name: (STANDARD_CODE, VEHICLE_TYPE_NAME)}. Strings are read back off a live model where dane added them through the UI (2026-07-28). Note the spaces: the online API manual renders these with all spaces stripped (“AASHTOLegalType3”, “OHDOTLOAD”), which is a rendering artifact – those forms are wrong and, because the API does not validate names, would store cleanly and then apply ZERO load silently. Always confirm nonzero envelopes after the first analysis.

The two ODOT state permit loads (S-PL60T/S-PL65T) have no DB entry at all. Build those with midas_vehicle_payload() – but note the IM caveat there.

Warning

These map a BDM vehicle NAME to a Midas DB entry; they do not assert that Midas’s axle train matches the current BDM figure. Midas’s built-in definitions are a vendor transcription frozen at some past spec revision – their ODOT standard box sections are known to be out of date, and the same risk applies here. A DB vehicle also hides its axle data: the stored record carries only the type name, so the API cannot read back what Midas will actually apply. Verify each against RATING_VEHICLES (which IS checked against BDM Figures 908.3-1..-5) before using the DB entry for a rating.

civilpy.structural.midas_models.DUMMY_PROPS = {'DEN': 0.0, 'ELAST': 144.0, 'POISN': 0.3, 'THERMAL': 0.0}

A weightless, near-rigid-free material for dummy elements (traffic lane lines). They exist to carry a lane definition and must not attract load or add stiffness: 1 ksi and zero density.

civilpy.structural.midas_models.ROD_STEEL_PROPS = {'DEN': 0.49, 'ELAST': 4176000.0, 'POISN': 0.3, 'THERMAL': 6.5e-06}

A709 Grade 50 is the default steel; tie rods are ASTM A307 Grade 307A. Same modulus and density – the grade matters for capacity, not for the stiffness this model carries.

civilpy.structural.midas_models.STEEL_PROPS = {'DEN': 0.49, 'ELAST': 4176000.0, 'POISN': 0.3, 'THERMAL': 6.5e-06}

A709 Grade 50 steel in KIPS/FT model units (E in ksf, density in kcf) – the structural-steel property block both exporters share.

civilpy.structural.midas_models.abutment_connection(kind: str, girder_end_nodes: Iterable[int], seat_node: int, *, bearing_stiffness: list[float] | None = None, link_start: int = 1) dict[source]

Super-/substructure connection at an abutment.

kind="integral" – girders are cast monolithically into the abutment cap: a rigid link (/db/RIGD) makes seat_node the master and every girder end a slave for all six DOF, so the connection is moment-continuous and thermal movement is taken by the (flexible) pile bent below – give that bent soil_spring_supports(), not fixed bases.

kind="semi-integral" – the deck/backwall is continuous but the girders bear on bearings at the seat: an elastic link (/db/ELNK, LINK="GEN") per girder carries vertical load stiffly and frees longitudinal translation and rotation. bearing_stiffness is the SDR vector [kdx, kdy, kdz, krx, kry, krz] in model units; the default is a near-rigid vertical (kdz) bearing, soft elsewhere.

Returns a dict of /db/* assign bodies to merge into the model ({"RIGD": ...} or {"ELNK": ...}).

civilpy.structural.midas_models.bifurcated_girder_model(stem_length: float, stem_segments: int, branches: list[dict], *, stem_offset: float = 0.0, matl: int = 1, sect: int = 1, node_start: int = 1, elem_start: int = 1) dict[source]

A girder line that runs straight for stem_length then splits at a gore node into diverging branches.

The stem runs along +X from (0, stem_offset) to (stem_length, stem_offset) in stem_segments beams; the last stem node is the gore. Each entry in branches is {"length", "end_offset", "segments"} – a branch from the gore to (stem_length + length, end_offset), straight-line interpolated.

Returns {"NODE", "ELEM", "gore_node", "branch_end_nodes", "meta"}.

civilpy.structural.midas_models.circular_curve_nodes(radius: float, central_angle_deg: float, n_segments: int, girder_offsets: Iterable[float], *, z: float = 0.0, node_start: int = 1) tuple[dict, dict][source]

Nodes for girders following a horizontal circular curve.

Each girder is a concentric arc: girder g at radial offset rides radius radius + offset (offset positive toward the outside of the curve). The arc sweeps central_angle_deg in n_segments equal steps; the curve centre is at the origin and station 0 lies on the +Y axis, so X = r*sin(theta) and Y = r*cos(theta).

Returns (assign, grid) where assign is the /db/NODE body {id: {"X", "Y", "Z"}} and grid maps (girder_index, station_index) -> node_id for wiring elements.

civilpy.structural.midas_models.concrete_elastic_modulus_ksf(fc_psi: float, unit_wt_pcf: float = 145.0) float[source]

Concrete elastic modulus in kips/ft^2 from f'c (psi).

AASHTO LRFD 5.4.2.4 / ACI 318: Ec = 33000 * K1 * (wc/1000)^1.5 * sqrt(f'c) with wc in kcf and f'c in ksi gives Ec in ksi; convert to ksf (x144) for a model whose DIST unit is feet. For f'c = 4500 psi, wc = 145 pcf this is ~3952 ksi, the value MIDAS should hold for an SB-1-24 slab – not the ~29000 ksi steel modulus midas_payloads otherwise defaults to.

civilpy.structural.midas_models.concrete_material_block(name: str = 'Class-S-4500', *, matl_id: int = 1, fc_psi: float = 4500.0, unit_wt_pcf: float = 145.0, length_unit: str = 'ft') dict[source]

A /db/MATL USER concrete material with Ec from concrete_elastic_modulus_ksf().

ELAST and DEN are carried in the model’s own length unit – ksf and kcf when length_unit is "ft", ksi and kip/in^3 when it is "in". MIDAS applies one length unit model-wide, so a material block built for feet inside an inch model overstates Ec by 144x and the density by 1728x, with nothing in the response to say so: pass the hub’s units.length.

civilpy.structural.midas_models.constraint_assign(cons_by_id: dict[int, str]) dict[source]

Wrap {node_id: "DX..RW" flag string} into the /db/CONS body {node_id: {"ITEMS": [{"ID": 1, "CONSTRAINT": flags}]}}.

civilpy.structural.midas_models.curved_girder_model(radius: float, central_angle_deg: float, n_segments: int, girder_offsets: Iterable[float], *, matl: int = 1, sect: int = 1, diaphragm_sect: int | None = None, node_start: int = 1, elem_start: int = 1) dict[source]

A horizontally curved multi-girder model: concentric chorded girder lines plus, when diaphragm_sect is given, a transverse diaphragm between adjacent girders at every station.

Returns {"NODE": ..., "ELEM": ..., "grid": ..., "meta": ...} where NODE/ELEM are /db/* assign bodies. Chorded straight beams approximate the curve (standard practice); refine n_segments until the chord offset is acceptable.

civilpy.structural.midas_models.hub_section_material_blocks(model, *, sect_start: int = 1, matl_start: int = 1, default_grade: str = 'Grade 50', length_unit: str | None = None, db_name: str | None = 'AISC10(US)') dict[source]

Assign a real SECT per distinct AISC shape and a MATL per distinct grade from the hub’s elements (stage G5 – replaces the single placeholder SECT/MATL that midas_payloads emits by default).

Every shape is sent as a reference into MIDAS’s own db_name section database (default "AISC10(US)") rather than a re-entered set of dimensions – see rolled_i_section_block(). Pass db_name=None if the model’s shapes are built-up/historic sections with no library entry; length_unit (defaulting to the model’s own units.length) only matters in that fallback, to keep section dimensions consistent with the NODE coordinates and the UNIT table’s DIST.

Returns {"SECT", "MATL", "sect_by_shape", "matl_by_grade", "elem_assign"} where elem_assign maps each Element.id to its (sect_id, matl_id) so the caller can wire real sections onto elements. Elements with no section label get sect_id=None (keep the placeholder for those).

civilpy.structural.midas_models.lb_per_in_to_kip_per_ft(k_lb_per_in: float) float[source]

Convert a spring constant from lb/in (the geotech curve unit) to kip/ft (the KIPS/FT model unit): k[kip/ft] = k[lb/in] * 12 / 1000.

civilpy.structural.midas_models.load_oh_vehicles(client, *, im_percent: float = 33.0, include_design: bool = True, use_standard_db: bool = False, replace: bool = False) dict[source]

Push every vehicle ODOT BDM Section 908 specifies into a live model.

Loads exactly the BDM set and nothing else – the ten commercial legal vehicles and two emergency vehicles of 908.3, the two state permit loads of 908.3.3, and (with include_design) the 908.2 inventory/operating design loads. Vehicles Midas offers but the BDM does not call for – the AASHTO National Rating Load, the H15/H20/HS15/ HS25/AML standard set, Ohio 4F1 – are deliberately excluded.

Parameters:
  • client – A live MidasCivil.

  • im_percent

    Dynamic load allowance written onto standard-DB records, per BDM 924.4.A (33% for all non-buried bridges). Two rules from that article are applied automatically: lane loads get IM = 0 (924.4.C, BDM_908_LANE_LOADS), and the state permit loads get IM = 0 (924.4.E allows the allowance to be ignored for permit loads under controlled conditions). The rest of 924.4 is bridge-specific and must be handled by the caller: 15% for fatigue (B), no IM on wood components (D), and the buried- structure reduction IM = 33(1 - 0.125*DE) (F).

    The value is silently discarded on user-defined records – see midas_vehicle_payload() – so when use_standard_db is False the returned "im_applied" is False and the allowance must be applied downstream.

  • include_design – Also load the 908.2 HL-93 / HS20 design loads.

  • use_standard_db – Reference Midas’s built-in definitions instead of writing explicit axle trains. Default False: the built-ins are a vendor transcription frozen at some past spec revision (Midas’s ODOT standard box sections are demonstrably out of date), a DB record stores only its type name so the API cannot read back what will actually be applied, and an unresolved name applies ZERO load silently. The user-defined axle trains come from RATING_VEHICLES, which is checked against BDM Figures 908.3-1..-5. The two permit loads have no DB entry and are always user-defined.

  • replace – Delete every vehicle already in the model first.

Returns:

{"pushed": {id: label}, "im_applied": bool, "user_defined": [...], "standard_db": [...]}.

Return type:

dict

civilpy.structural.midas_models.midas_payloads(model: StructuralModel, *, node_start: int = 1, elem_start: int = 1, material_name: str = 'A709-50', db_name: str | None = 'AISC10(US)') dict[source]

Serialize a StructuralModel to MIDAS PUT /db/* assign bodies – the Rhino -> Midas payload step.

Pure (no live session): returns {table: assign} in send order UNIT, MATL, SECT, NODE, ELEM, CONS, STLD, CNLD. The hub’s stable string ids are mapped to the 1-based integer ids MIDAS uses (node insertion order); full 6-DOF restraints become the 7-char CONS flag string via Restraint.to_constraint_string(), so fix_z/rx/ry/rz reach MIDAS intact. Loads are grouped into STLD cases with their nodal forces and moments in CNLD.

Every element gets a real SECT per distinct AISC shape label (hub_section_material_blocks()) and a real MATL per distinct grade; an element with no shape label falls back to the placeholder square section (still real steel material) so it always references a valid section. By default each SECT references the shape directly out of MIDAS’s own db_name database ("AISC10(US)") instead of re-entering dimensions; pass db_name=None for built-up/historic shapes with no library entry.

Note

The CNLD concentrated-nodal-load layout follows the API manual but is unverified against a live Civil NX release – check the send report’s errors first when debugging (same caveat as the midas_models builders).

civilpy.structural.midas_models.midas_standard_vehicle(type_name: str, *, name: str | None = None, standard_code: str = 'AASHTO-LRFD', im_percent: float = 33.0) dict[source]

A /db/mvhl standard-DB vehicle record.

Verified live 2026-07-27: STANDARD_CODE is REQUIRED for the DB reference to resolve – without it the record stores cleanly but the vehicle applies zero load, silently. LRFD DB names include “HL-93TRK”, “HL-93TDM”, “HS20-FTG”; standard_code “OHDOT LOAD” reaches the Ohio legal/permit set (“OH Legal load 5C1”, 2F1, 3F1, 4F1 – the BDM’s EV vehicles are absent, use midas_vehicle_payload() for those).

civilpy.structural.midas_models.midas_vehicle_payload(vehicle, *, lane_load_klf: float | None = None, plm_kip: float = 0.0, plv_kip: float = 0.0, im_percent: float = 0.0, length_unit: str = 'in') dict[source]

A /db/mvhl user-defined Truck/Lane vehicle record from a RatingVehicle – the way to run vehicles Midas’s DB lacks (ODOT’s FAST-Act EV2/EV3, custom permit trains).

Schema verified live 2026-07-27: axles ride a LOAD_ITEMS array of {POINT_LOAD, POINT_DIST} (distance to the NEXT axle; kips and model length units); the lane component and concentrated moment/shear loads (PLM/PLV) sit in VEH_DEFAULT. lane_load_klf defaults to the vehicle’s own definition.

length_unit must match the model’s UNIT DIST – axle spacings and the lane load are carried in it, so a truck built for inches and pushed to a model in feet is 12x too long, stores without complaint, and simply produces the wrong envelope. Read it off the model with model_length_unit() rather than assuming.

Warning

im_percent is silently discarded for user-defined vehicles. Verified live 2026-07-28: a standard-DB record (midas_standard_vehicle()) stores VEH_DEFAULT.DYN_LOAD_ALLOWANCE, but on a Truck/Lane user vehicle Midas drops the field entirely – it is absent from the record on read-back, i.e. IM = 0. Apply the dynamic load allowance downstream instead (moving-load-case scale factor, or the load combination), or the live load effect will be low by the full IM. The parameter is kept so the axle loads stay nominal and auditable against the source figure.

civilpy.structural.midas_models.model_length_unit(client, default: str = 'in') str[source]

The live model’s UNIT table DIST, lower-cased ("in", "ft", …).

Axle spacings, section coordinates, material moduli and densities are all carried in this one unit, and nothing in an API response says which it is – a vehicle built in feet and pushed to an inch model stores cleanly and then runs a truck 12x too short. Read it, don’t assume it.

civilpy.structural.midas_models.placeholder_section_block(*, sect_id: int = 1, side_ft: float = 1.0, name: str = 'PLACEHOLDER-1ft-SQ') dict[source]

A solid-square /db/SECT placeholder so exported elements reference a valid section. Swap to the real shape inside Civil NX for flexure – the equal-area square keeps axial stiffness honest meanwhile (same convention as TrussBridge).

civilpy.structural.midas_models.push_midas(model: StructuralModel, midas=None, **client_kwargs) dict[source]

Send a hub to a live Civil NX session (built from ~/secrets.json when midas is not given). Pushes each midas_payloads() table in order, keeps going on errors, and returns {table: {"sent": n} | {"error": msg}} – the same report shape as TrussBridge.to_midas.

civilpy.structural.midas_models.rolled_i_section_block(shape_label: str, *, sect_id: int = 1, length_unit: str = 'in', db_name: str | None = 'AISC10(US)') dict[source]

A /db/SECT body for a rolled I/H shape.

By default this references the shape directly out of MIDAS’s own section database (db_name="AISC10(US)", DATATYPE=1) rather than re-entering dimensions as a user-input section: shape_label (e.g. "W24X104") must match a name in that database, and MIDAS – not civilpy – is then the source of truth for the section properties used in analysis. This is the correct choice for standard rolled shapes; civilpy’s own steel.W dimensions are not sent at all in this path. Confirmed against a live Civil NX round-trip: GET /db/SECT echoed back exactly this SECT_BEFORE shape for a section entered as “DB/Shape -> AISC10(US) -> W24X104” in the Civil NX UI.

Pass db_name=None for a built-up or historic shape with no library entry – this falls back to a user-input (DATATYPE=2) section with dimensions pulled from steel.W (the AISC database module), the previous default behavior. length_unit is a Pint unit string ("in", "ft", …) and must match the model’s own ``UNIT`` table DIST in that fallback – MIDAS applies one length unit to every geometric quantity in the model, section dimensions included, so a mismatch silently scales the section by the conversion factor (inches sent while the model is in feet gives a 12x oversized, self-intersecting section). Callers building a full model payload should pass the model’s own units.length (see hub_section_material_blocks()); the "in" default is only for standalone use.

civilpy.structural.midas_models.soil_spring_supports(node_springs: dict[int, list[float]], *, table: str = 'SPRING', spring_type: str = 'LINEAR') dict[source]

Nodal foundation springs from per-node stiffness vectors.

node_springs maps node_id -> [kdx, kdy, kdz, krx, kry, krz] in model units (convert the geotech curves’ lb/in values with lb_per_in_to_kip_per_ft()). Typically kdx/kdy come from the p-y secant modulus (civilpy.geotech.lateral_pile), kdz from the t-z springs and the q-z tip spring (civilpy.geotech.axial_load_transfer).

Returns the assign body {node_id: {"ITEMS": [{"ID": 1, "TYPE": ..., "SDR": [...]}]}}. The MIDAS point-spring table name is unverified – pass the live-confirmed table to MidasCivil.put_db(table, body) (see the live-capture checklist).

civilpy.structural.midas_models.solid_rect_section_block(width: float, height: float, *, sect_id: int = 1, name: str | None = None) dict[source]

A solid-rectangle (SB) /db/SECT body, width x height in the model’s length unit.

This is the honest section for an equivalent-strip slab beam (a 1 ft wide x thickness deep concrete strip), where an AISC I-shape reference makes no sense. Dimension order matches the placeholder square: vSIZE = [H, B] is MIDAS’s [depth, width] for the SB shape.

civilpy.structural.midas_models.solid_round_section_block(diameter: float, *, sect_id: int = 1, name: str | None = None) dict[source]

A solid-round (SR) /db/SECT body – the honest section for a tie rod or anchor dowel, which is a bar, not a rectangle.

civilpy.structural.midas_models.steel_material_block(name: str = 'A709-50', *, matl_id: int = 1, props: dict | None = None, length_unit: str = 'ft') dict[source]

The /db/MATL assign body for a USER-defined steel material.

STEEL_PROPS is stated in KIPS/FT; length_unit rescales ELAST (force/length^2) and DEN (force/length^3) into the model’s own length unit – see concrete_material_block() for why that matters.

civilpy.structural.midas_models.thickness_block(t_ft: float, *, thik_id: int = 1, name: str | None = None) dict[source]

A /db/THIK value-thickness body (t_ft in the model length unit), the plate-element analogue of a beam SECT. A plate ELEM references this id through its ``SECT`` field (verified against a live Civil NX release: the plate body is {"TYPE":"PLATE","MATL":m,"SECT":<thik id>, "NODE":[n1..n4,0,0,0,0],"ANGLE":0,"STYPE":1}).

civilpy.structural.midas_models.unit_block(force: str = 'KIPS', dist: str = 'FT', heat: str = 'BTU', temper: str = 'F') dict[source]

The /db/UNIT assign body {"1": {FORCE, DIST, HEAT, TEMPER}}.

civilpy.structural.midas_models.unit_block_for(units) dict[source]

unit_block from a hub Units (maps the kips/ft labels to the MIDAS enum; unknown labels pass through upper-cased).

civilpy.structural.mohrs_circle module

Mohr’s circle for plane stress: principal stresses, maximum shear, and the classic circle diagram with the rotated-element companion sketch.

Follows the same conventions as civilpy.structural.beam_bending: plot methods accept an optional ax (a new figure is created when omitted) and return the matplotlib figure so they drop straight into Jupyter notebooks. Stresses are sign-convention standard: tension positive, and a positive tau_xy acts upward on the right (+x) face. The shear axis is drawn positive-down so clockwise rotation of the physical element matches counterclockwise travel around the circle, the way it’s taught.

Examples

>>> mc = MohrsCircle(sigma_x=80, sigma_y=20, tau_xy=30)
>>> round(mc.sigma_1, 2), round(mc.sigma_2, 2)
(92.43, 7.57)
>>> round(mc.tau_max, 2)
42.43
>>> round(mc.theta_p_deg, 2)
22.5
>>> sx, sy, txy = mc.stresses_at(22.5)
>>> round(sx, 2), round(txy, 6)
(92.43, 0.0)
class civilpy.structural.mohrs_circle.MohrsCircle(sigma_x: float, sigma_y: float, tau_xy: float, units: str = 'ksi')[source]

Bases: object

Plane-stress state and its Mohr’s circle.

Parameters are the normal stresses on the x and y faces and the shear tau_xy, in any consistent stress unit (the unit label used on the plots can be set with units).

property center: float

Average normal stress (circle center).

plot(ax=None, show_out_of_plane: bool = False, theta_deg: float | None = None)[source]

Draw the Mohr’s circle with the X/Y stress points, principal stresses, and maximum shear labeled.

show_out_of_plane adds the two sigma_3 = 0 circles governing the absolute maximum shear; theta_deg marks the stress point for an element rotated by that angle. Returns the figure.

plot_element(theta_deg: float = 0.0, ax=None)[source]

Sketch the square stress element rotated theta_deg with its normal- and shear-stress arrows labeled. Returns the figure.

property radius: float
property sigma_1: float

Major principal stress.

property sigma_2: float

Minor principal stress.

stresses_at(theta_deg: float) tuple[float, float, float][source]

Stress components on an element rotated theta_deg counterclockwise: (sigma_x’, sigma_y’, tau_x’y’).

property tau_abs_max: float

Absolute maximum shear including the out-of-plane circles (plane stress, sigma_3 = 0).

property tau_max: float

Maximum in-plane shear stress (= circle radius).

property theta_p_deg: float

Angle from the x-axis to the major principal plane’s normal (degrees, counterclockwise positive).

property theta_s_deg: float

Angle to the maximum-shear orientation (45 deg from principal).

civilpy.structural.moment_distribution module

Hardy Cross moment distribution for prismatic continuous beams, with the iteration table (the part they make you do by hand) and the final moment diagram.

Sign convention: counterclockwise end moments positive (the standard moment-distribution convention); the plotted bending-moment diagram is converted to the sagging-positive beam convention.

Examples

Two equal 20-ft spans, 2 klf everywhere, pinned ends:

>>> md = MomentDistribution()
>>> md.add_span(length=20, w=2.0)
>>> md.add_span(length=20, w=2.0)
>>> moments = md.solve()
>>> round(-moments[1][0], 1)    # support moment = wL^2/8 = 100 (hogging)
100.0
class civilpy.structural.moment_distribution.MomentDistribution(left_fixed: bool = False, right_fixed: bool = False)[source]

Bases: object

Continuous prismatic beam on simple supports (ends optionally fixed), loaded with a uniform load and/or one point load per span.

add_span(length: float, ei: float = 1.0, w: float = 0.0, point_load: tuple[float, float] | None = None)[source]

w is a uniform load (klf, downward positive); point_load = (P kips, a ft from the span’s left end).

moment_at(span_index: int, x: float) float[source]

Sagging-positive bending moment at x ft along a span, superposing the simple-span moment and the end-moment gradient.

plot(ax=None, n_per_span: int = 120)[source]

Plot the final bending-moment diagram across all spans with supports marked. Returns the figure.

solve(tolerance: float = 1e-06, max_cycles: int = 100)[source]

Iterate balance/carry-over to convergence. Returns the end moments per span [(M_left, M_right), …] (CCW positive) and stores the cycle-by-cycle history for the classic table.

civilpy.structural.pier module

Multi-column pier (bent) substructure.

A PierCap is the cap beam spanning the columns: it carries the girder bearing reactions as point loads plus its own self weight and is analyzed as a continuous beam on the columns (rigid vertical supports) with a direct-stiffness solver, giving the design shear / moment envelope and the axial load delivered to each column. Its flexural and shear design reuse the AASHTO LRFD reinforced-concrete resistance functions.

A MultiColumnBent puts the cap on top of PierColumn columns, distributes the vertical reactions and a lateral force among the columns, and checks each column with the P-M interaction and moment-magnification routines already in civilpy.structural.aashto.lrfd.columns – a native build, not an RC-Pier automation.

Units: kip, inch, ksi (matching the LRFD column/concrete checks). Seismic demand is out of scope here; supply the lateral force from the governing non-seismic combination.

class civilpy.structural.pier.BeamSolution(x: ndarray, moment: ndarray, shear: ndarray, reactions: dict)[source]

Bases: object

Continuous-beam results: nodal coordinates and the moment / shear diagrams plus the support reactions.

property max_moment: float
property max_shear: float
moment: ndarray
reactions: dict
shear: ndarray
x: ndarray
class civilpy.structural.pier.BentResult(column_axials: list[float], column_shears: list[float], column_moments: list[float], cap_solution: BeamSolution, pm_checks: list[CheckResult])[source]

Bases: object

Per-column demands and checks for a multi-column bent.

property all_columns_ok: bool
cap_solution: BeamSolution
column_axials: list[float]
column_moments: list[float]
column_shears: list[float]
pm_checks: list[CheckResult]
class civilpy.structural.pier.MultiColumnBent(cap: PierCap, columns: list[PierColumn], lateral_force: float = 0.0, column_dead_load: float | list[float] = 0.0)[source]

Bases: object

A pier cap on multiple columns, with vertical girder reactions and a lateral force distributed to the columns by their stiffness.

analyze() BentResult[source]

Distribute loads to the columns and run each column’s P-M check (with the moment magnified for slenderness).

lateral_distribution() list[float][source]

Lateral shear in each column (kip), in proportion to its translational stiffness.

class civilpy.structural.pier.PierCap(length: float, width: float, depth: float, column_positions: list[float], loads: list[~civilpy.structural.pier.PointLoad] = <factory>, f_c: float = 4.0, f_y: float = 60.0, unit_weight: float = 8.680555555555556e-05, e_concrete: float | None = None)[source]

Bases: object

Pier cap beam. length (in), width/depth the cross section (in), f_c/f_y (ksi). column_positions are the support x-locations; loads the girder bearing reactions; unit_ weight the concrete weight (kci, kip/in^3) for self weight.

analyze(n_per_span: int = 12) BeamSolution[source]

Continuous-beam analysis for the design force envelope and the column reactions.

column_positions: list[float]
column_reactions() dict[source]

Axial load delivered to each column top (kip).

depth: float
e_concrete: float | None = None
property ei: float
f_c: float = 4.0
f_y: float = 60.0
flexure_check(a_s: float, cover: float = 3.0, bar_dia: float = 1.128, solution: BeamSolution | None = None) CheckResult[source]

Flexural check at the maximum cap moment, reusing rc_rectangular_flexural_resistance(). a_s is the tension steel (in^2).

length: float
loads: list[PointLoad]
property self_weight_udl: float

Self weight as a uniform load (kip/in).

shear_check(a_v: float = 0.0, s: float = 12.0, cover: float = 3.0, bar_dia: float = 1.128, solution: BeamSolution | None = None) CheckResult[source]

Shear check at the maximum cap shear, reusing rc_shear_resistance().

unit_weight: float = 8.680555555555556e-05
width: float
class civilpy.structural.pier.PierColumn(height: float, layers: list[RebarLayer], f_c: float = 4.0, f_y: float = 60.0, b: float | None = None, h: float | None = None, diameter: float | None = None, spiral: bool = False, fixity: Literal['fixed-fixed', 'fixed-free'] = 'fixed-fixed')[source]

Bases: object

One bent column. height (in) is the clear height; the section is rectangular b x h or circular diameter (in) with layers of longitudinal bars; f_c/f_y (ksi). fixity is "fixed-fixed" (cap restrains the top against rotation) or "fixed-free" (cantilever).

axial_resistance(p_u: float | None = None) CheckResult[source]
b: float | None = None
diameter: float | None = None
f_c: float = 4.0
f_y: float = 60.0
fixity: Literal['fixed-fixed', 'fixed-free'] = 'fixed-fixed'

"fixed-fixed" (cap restrains rotation) or "fixed-free" (cantilever).

Type:

End restraint for slenderness

property gross_area: float
h: float | None = None
height: float
lateral_moment(shear: float) float[source]

Column moment (kip-in) from a column shear shear (kip): V*L/2 fixed-fixed, V*L cantilever.

lateral_stiffness(e_concrete: float) float[source]

Translational stiffness of the column top (kip/in): 12EI/L^3 for a fixed-fixed column, 3EI/L^3 for a cantilever.

layers: list[RebarLayer]
magnified_moment(p_u: float, m_u: float, k_factor: float = 1.2, e_concrete: float | None = None) tuple[float, CheckResult][source]

Slenderness-magnified moment (kip-in). The critical buckling load uses the cracked-section EI = 0.25*Ec*Ig (5.6.4.3) and the effective length k_factor*height. Returns the magnified moment and the magnifier CheckResult.

property moment_of_inertia: float
pm_check(p_u: float, m_u: float) CheckResult[source]

P-M interaction adequacy at factored p_u (kip) and m_u (kip-in), via the strain-compatibility diagram.

spiral: bool = False
class civilpy.structural.pier.PointLoad(x: float, p: float)[source]

Bases: object

A downward point load p (kip) at position x (in) along the cap, measured from the left end.

p: float
x: float
civilpy.structural.pier.solve_continuous_beam(length: float, ei: float, supports: list[float], point_loads: list[PointLoad] | None = None, udl: float = 0.0, n_per_span: int = 12) BeamSolution[source]

Solve a continuous beam of span length (in) and stiffness ei (kip-in^2) on rigid vertical supports (x positions, in), under downward point_loads (kip) and a uniform load udl (kip/in).

The beam is discretized between breakpoints (supports, load points, ends), every segment subdivided into n_per_span elements so nodal moments capture the in-span peaks. Returns the moment / shear diagrams and the support reactions.

civilpy.structural.placement module

Placement and multi-representation contract for bridge components (A3).

This module defines the base classes for bridge components that can be placed on an Alignment and Terrain, and emit multiple representations (Rhino/MIDAS).

class civilpy.structural.placement.Bridge(alignment: Alignment, terrain: Terrain | None = None)[source]

Bases: BridgeComponent

A collection of bridge components forming a complete structure.

add_component(component: BridgeComponent, station: float, offset: float = 0.0)[source]
geometry() list[Any][source]

Returns the geometric representation (e.g. Rhino BREP/curves/points).

structural_model() StructuralModel[source]

Returns the analysis hub representation (StructuralModel).

class civilpy.structural.placement.BridgeComponent(*args, **kwargs)[source]

Bases: Protocol

Protocol for a bridge component that can be placed in a CDE.

geometry() Any[source]

Returns the geometric representation (e.g. Rhino BREP/curves/points).

structural_model() StructuralModel[source]

Returns the analysis hub representation (StructuralModel).

class civilpy.structural.placement.PlacedComponent(component: BridgeComponent, placement: Placement)[source]

Bases: object

Wraps a component with its placement information.

property frame

Returns the coordinate frame at the placement (origin, tangent, normal).

global_geometry()[source]

Returns geometry transformed into global coordinates.

structural_hub() StructuralModel[source]

Returns the structural model spoke for this component.

class civilpy.structural.placement.Placement(alignment: Alignment, station: float, offset: float = 0.0, terrain: Terrain | None = None)[source]

Bases: object

Defines where a component sits relative to the site’s project geometry.

alignment: Alignment
property elevation

Returns the elevation at this placement (alignment profile or terrain).

offset: float = 0.0
property point

Returns the 3D point in global coordinates.

station: float
terrain: Terrain | None = None

civilpy.structural.project_spec module

Storable project records — the project-object schema authority.

The historical-record system stores one “project object” per construction contract: a thin ProjectRecord parent (PID, the SFNs it touches, where and when it sold) composed with fat, per-discipline component records. The keystone component is EraStandardsRecord — the standards in force when the plan was sold (CMS spec year, the SCD/plan- insert/standard-bridge-drawing sheets listed on the title sheet, and the design-manual editions) — because a sparse historic bridge resolves any field it does not explicitly know through the era registry, and is marked as such via Provenance.

Everything follows civilpy.structural.bim_spec exactly: plain dataclasses of JSON-safe primitives, per-field engineering metadata via spec_field(), strict validate() in front of the schema-free JSONB store, and record_to_dict() round-trips. The application side (snbi_ui) stores the flattened document plus a few promoted, indexed columns — the same Bridge/OhioBridge split the BIM phases use.

The similarity pipeline consumes ProjectRecord.features() — a flat dict of JSON scalars (numbers + categorical strings) versioned by FEATURES_VERSION, fed to the EllisPCA-pattern engine whose OneHotEncoder handles the categoricals. Stored vectors are reproducible: same record + same version → same features.

MOT / real-estate / survey start deliberately thin (the fields a title sheet, MOT notes, and an RE summary reliably yield) and sharpen as the plan-corpus extractors populate them; adding a field is an additive JSON key, never a migration.

class civilpy.structural.project_spec.EraStandardsRecord(spec_year: int | None = None, scds: tuple[StandardRef, ...] = (), plan_inserts: tuple[StandardRef, ...] = (), sbd_set: tuple[StandardRef, ...] = (), bdm_edition: str | None = None, ld_vol1_edition: str | None = None, ld_vol2_edition: str | None = None, ld_vol3_edition: str | None = None)[source]

Bases: SpecRecord

The standards regime a project was designed and sold under.

For plan-holding projects this is read off the title sheet, not inferred: the sheet lists the SCDs with revision dates and the CMS spec year. For plan-less historic bridges the era registry supplies the set in force at spec_year / the letting date, and every field resolved that way is provenance-marked standard-default.

Manual editions are free-text labels validated against the hand-curated edition registry on the application side (a dozen rows per manual, not a schema concern here).

bdm_edition: str | None = None
ld_vol1_edition: str | None = None
ld_vol2_edition: str | None = None
ld_vol3_edition: str | None = None
plan_inserts: tuple[StandardRef, ...] = ()
sbd_set: tuple[StandardRef, ...] = ()
scds: tuple[StandardRef, ...] = ()
spec_year: int | None = None
standard_date(code: str) str | None[source]

The revision date recorded for code (searched across all three collections), or None.

civilpy.structural.project_spec.FEATURES_VERSION = 1

Bump when the meaning of a stored feature changes (not when one is added) — consumers persist vectors keyed by this.

class civilpy.structural.project_spec.MOTRecord(scheme: str | None = None, n_phases: int | None = None, detour_length_mi: float | None = None, temp_structure: bool = False, barrier_types: tuple[str, ...] = (), lanes_maintained: int | None = None, min_lane_width_ft: float | None = None)[source]

Bases: SpecRecord

Maintenance-of-traffic, at the complexity level MOT notes yield: the scheme, phase count, and what is maintained through the work.

barrier_types: tuple[str, ...] = ()
detour_length_mi: float | None = None
lanes_maintained: int | None = None
min_lane_width_ft: float | None = None
n_phases: int | None = None
scheme: str | None = None
temp_structure: bool = False
class civilpy.structural.project_spec.ProjectRecord(pid: str, sfns: tuple[str, ...] = (), district: int | None = None, county: str | None = None, route: str | None = None, letting_date: str | None = None, work_category: str | None = None, era: EraStandardsRecord | None = None, mot: MOTRecord | None = None, real_estate: RERecord | None = None, survey: SurveyRecord | None = None, provenance: Provenance | None = None)[source]

Bases: ElementRecord

One construction project (a PID / contract sale): the thin parent over the era-standards keystone and the per-discipline components. Bridge components live as the existing per-SFN element records (BridgeLayoutRecord and friends), joined to this record through sfns — a project does not restate them.

BIM_TYPE = 'project'
SUBTYPE = 'record'
county: str | None = None
district: int | None = None
era: EraStandardsRecord | None = None
features() dict[source]

The flat feature dict the PCA→kNN similarity pipeline consumes: JSON scalars only — numbers for numerics, strings for categoricals (the pipeline’s OneHotEncoder expands them), None for genuinely unknown. Keyed by FEATURES_VERSION so persisted vectors are reproducible.

letting_date: str | None = None
mot: MOTRecord | None = None
pid: str
provenance: Provenance | None = None
real_estate: RERecord | None = None
route: str | None = None
sfns: tuple[str, ...] = ()
survey: SurveyRecord | None = None
work_category: str | None = None
class civilpy.structural.project_spec.RERecord(n_parcels: int | None = None, n_takes_fee: int | None = None, n_takes_permanent_easement: int | None = None, n_takes_temporary_easement: int | None = None, n_utility_relocations: int | None = None, n_relocations_residential: int | None = None)[source]

Bases: SpecRecord

Real estate, as the RE summary tabulates it: parcel counts by take type and the utility-relocation load.

n_parcels: int | None = None
n_relocations_residential: int | None = None
n_takes_fee: int | None = None
n_takes_permanent_easement: int | None = None
n_takes_temporary_easement: int | None = None
n_utility_relocations: int | None = None
class civilpy.structural.project_spec.StandardRef(code: str, date: str | None = None)[source]

Bases: SpecRecord

One standard sheet in force on the project: the drawing code as printed on the title sheet (CSB-1-55, PSID-1-99…) and the revision date printed beside it. date=None means the title sheet listed the code without a date (or it has not been read yet) — the catalog join resolves it.

code: str
date: str | None = None
class civilpy.structural.project_spec.SurveyRecord(horizontal_datum: str | None = None, vertical_datum: str | None = None, n_control_monuments: int | None = None, row_monumented: bool | None = None)[source]

Bases: SpecRecord

Survey control as the title/schematic sheets state it — the datum era matters for georeferencing old plans.

horizontal_datum: str | None = None
n_control_monuments: int | None = None
row_monumented: bool | None = None
vertical_datum: str | None = None

civilpy.structural.ps_i_beam_pipeline module

L1 design + verification of ODOT PSID-1-13 prestressed I-beam lines.

The box-beam slice verifies pre-engineered PSBDD table designs; PSID-1-13 has no companion design-data sheet, so this module both designs the strand pattern on the sheet’s permissible grid (civilpy.structural.odot.ps_i_beam.strand_grid()) and re-derives the governing checks for one interior beam line — the prestressed-I analog of civilpy.structural.box_beam_pipeline:

  • HL-93 demands from the same girder_line_envelope() machinery, distributed with the type-k I-girder factors (LRFD 4.6.2.2.2b/3a, Kg from the composite girder/deck geometry);

  • a straight, fully-bonded strand pattern chosen as the smallest even count that passes Service III tension (5.9.2.3.2b) and Strength I flexure (5.6.3) — sheet 10’s 0.6 in Grade 270 low-relaxation strand at 0.217 in^2, jacked to 0.75 f_pu;

  • elastic shortening (5.9.3.2.3a) + approximate lump-sum time-dependent losses (5.9.3.3);

  • concrete stresses at transfer (5.9.2.3.1, at the 60-diameter transfer length and midspan) and service (5.9.2.3.2, composite section for loads applied after the deck cures). When the fully-bonded pattern overstresses the beam end at transfer, the designer debonds end strands (outermost bottom-row strands first — the standard’s own remedy, sheet 10 detail item 4) in pairs up to the 5.9.4.3.3 cap (45 % of the total, the 8th-Edition-and-later limit; pass 0.25 for pre-2018 designs); the end check then runs on the bonded subset at its transfer length while midspan keeps the full pattern. Only when the cap can’t fix the end — or midspan release compression itself is over, the true long-span limit at ODOT’s 5.0 ksi f'ci ceiling — does the result carry a debond_note flag / failed check;

  • release camber (elastic: prestress hog minus self-weight sag at E_ci).

The WF web locations the sheet marks “must be draped if utilized” are never used by the straight-pattern designer. Units: kip / inch / ksi internally; spans, spacings and uniform loads enter in feet and klf. Simple spans only.

class civilpy.structural.ps_i_beam_pipeline.CompositeSection(b_eff_in: float, n_deck_beam: float, t_struct_in: float, haunch_in: float, area_in2: float, ybc_in: float, i_in4: float, sbc_in3: float, stc_beam_in3: float)[source]

Bases: object

Transformed composite section of one interior beam and its deck tributary width. Inches; the deck is transformed into beam concrete by the deck/beam modular ratio. The haunch locates the slab vertically but its area is neglected (conservative on the moduli).

area_in2: float
b_eff_in: float
haunch_in: float
i_in4: float
n_deck_beam: float
sbc_in3: float
stc_beam_in3: float
t_struct_in: float
ybc_in: float
civilpy.structural.ps_i_beam_pipeline.DECK_FC_KSI = 4.5

ODOT Class QC2 deck concrete

civilpy.structural.ps_i_beam_pipeline.F_PJ_KSI = 202.5

jacking stress, low-relaxation (5.9.2.2)

class civilpy.structural.ps_i_beam_pipeline.PSIBeamDesign(section: object, span_ft: float, spacing_ft: float, n_strands: int, pattern: tuple = (), ybar_in: float = 0.0, e_in: float = 0.0, fci_ksi: float = 4.0, fc_ksi: float = 5.5, n_debonded: int = 0)[source]

Bases: object

A designed strand pattern for one PSID-1-13 beam line.

property a_ps_in2: float
e_in: float = 0.0
fc_ksi: float = 5.5
fci_ksi: float = 4.0
n_debonded: int = 0

Strands debonded at each end (outermost bottom-row first); 0 when the fully-bonded pattern passes transfer.

n_strands: int
pattern: tuple = ()
section: object
spacing_ft: float
span_ft: float
ybar_in: float = 0.0
class civilpy.structural.ps_i_beam_pipeline.PSIBeamLineChecks(design: PSIBeamDesign, composite: CompositeSection, df_moment: float, df_shear: float, midspan_moments: dict = <factory>, losses: dict = <factory>, stresses: dict = <factory>, checks: dict = <factory>, camber_release_in: float = 0.0, debond_note: str = '')[source]

Bases: object

Everything ps_i_beam_line_checks() derives for one interior beam line. Moments in kip-ft; stresses in ksi (compression positive); checks values are CheckResult.

property all_ok: bool
camber_release_in: float = 0.0
checks: dict
composite: CompositeSection
debond_note: str = ''
design: PSIBeamDesign
df_moment: float
df_shear: float
losses: dict
midspan_moments: dict
stresses: dict
summary() str[source]
civilpy.structural.ps_i_beam_pipeline.composite_section(name: str, spacing_ft: float, *, t_struct_in: float, haunch_in: float, fc_beam_ksi: float, fc_deck_ksi: float = 4.5) CompositeSection[source]

Transformed composite properties for an interior line: effective flange width = beam spacing (4.6.2.6.1), structural deck thickness only (the wearing course carries as DW, not stiffness).

civilpy.structural.ps_i_beam_pipeline.ps_i_beam_line_checks(name: str, span_ft: float, n_beams: int, *, spacing_ft: float, deck_t_in: float = 8.5, sacrificial_in: float = 1.0, haunch_in: float = 2.0, n_strands: int | None = None, fci_ksi: float = 4.0, fc_ksi: float = 5.5, barrier_klf: float = 0.0, fws_klf: float | None = None, humidity_pct: float = 70.0, max_debond_fraction: float = 0.45) PSIBeamLineChecks[source]

Design (or verify, when n_strands is given) one interior PSID-1-13 beam line and re-derive its governing LRFD checks.

deck_t_in is the full slab; sacrificial_in (the ODOT monodeck wearing course) is excluded from the composite stiffness but its weight stays in DC1. barrier_klf / fws_klf are bridge-total, shared equally across the beams; fws_klf defaults to the sheet 10 design loading (60 psf across the deck). fci_ksi / fc_ksi default to the low end of the sheet 10 designer-selected ranges (conservative for the stress checks).

civilpy.structural.ps_i_beam_pipeline.structural_model_from_ps_i(name: str, span_ft: float, n_beams: int, *, spacing_ft: float, deck_t_in: float = 8.5, haunch_in: float = 2.0, diaphragms: bool = True, dead_loads: bool = True, barrier_klf: float = 0.0, fws_klf: float | None = None)[source]

Build the StructuralModel hub for a prestressed I-beam bridge — one line of beam elements per girder broken at the sheet 5 intermediate- diaphragm stations, transverse diaphragm elements there, a pin + roller per line, and DC1/DC2/DW beam loads matching ps_i_beam_line_checks() exactly.

Elements carry the PSID section name as their section label plus the published constants in metadata (section.area_in2 / section.i_in4) for a value-type section on the MAPI side. fws_klf is bridge-total; it defaults to the sheet 10 design loading (60 psf over n_beams * spacing_ft).

civilpy.structural.rhino_barrier module

Traffic barriers, bridge railings, portable concrete barriers, lane markings, and their reinforcement for the girder-line model (stage G7, the barrier & markings companion to civilpy.structural.rhino_deck).

Where rhino_deck puts a single generic parapet on each deck edge, this module renders the actual ODOT standard section for a chosen catalog entry (civilpy.structural.odot.bridge_railing):

  • concrete parapets in their true shape family – the New Jersey safety shape (vertical toe, lower/upper batter), the single-slope face, or the symmetric F-shape of a freestanding portable concrete barrier (PCB);

  • steel post-and-beam railings (TST-1 / TST-2) as a low concrete curb carrying vertical posts and horizontal rail tubes;

  • placeable on the two deck edges, in the median, or along any picked transverse line, so a construction-phase PCB run or a median SBR-2 is authored the same way as an edge parapet.

Each concrete barrier is drawn with a rebar cage – the vertical face bars at the catalog vertical_bar_spacing plus longitudinal bars – as bar-centerline curves on the Deck::Rebar layer, tagged gdr.kind=rebar. A companion build_lane_lines() paints the traffic lane markings (a solid edge line each side, dashed lane dividers between the 12 ft lanes) as flat strips on the deck top.

Everything is display geometry tagged gdr.kind=barrier | rebar | lane_line so the girder reader ignores it; the barrier’s real contribution to the analysis model is its dc2 line load, carried in the tags exactly as the deck’s parapet is. rhino3dm is an optional dependency imported lazily.

civilpy.structural.rhino_barrier.BAR_DIAMETERS_IN: dict[int, float] = {3: 0.375, 4: 0.5, 5: 0.625, 6: 0.75, 7: 0.875, 8: 1.0, 9: 1.128, 10: 1.27, 11: 1.41, 14: 1.693, 18: 2.257}

Standard reinforcing-bar nominal diameters (in), keyed by bar number.

class civilpy.structural.rhino_barrier.BarrierModel(designation: str, shape_family: str, height_in: float, length_ft: float, n_placements: int, dc2_klf_each: float, n_barrier: int, n_steel: int, n_rebar: int, material: str = '')[source]

Bases: object

Summary of a generated barrier run: what was placed and the dead load it hands the analysis model. Lengths in the units named; loads in klf.

dc2_klf_each: float
designation: str
height_in: float
length_ft: float
material: str = ''
n_barrier: int
n_placements: int
n_rebar: int
n_steel: int
shape_family: str
property total_dc2_klf: float

Combined dead load of every barrier run placed (klf).

civilpy.structural.rhino_barrier.CONCRETE_PCF = 150.0

Reinforced-concrete unit weight (pcf) for barrier dead load.

civilpy.structural.rhino_barrier.DEFAULT_BARRIER = 'BR-1 (36 in)'

Default barrier designation (36 in New Jersey, TL-4).

civilpy.structural.rhino_barrier.DEFAULT_COVER_IN = 2.0

Default clear cover to the reinforcing, inches.

civilpy.structural.rhino_barrier.DEFAULT_OVERHANG_FT = 3.5

Default deck overhang beyond each exterior girder line (ft) – where the edge barriers land when placements are not given explicitly.

class civilpy.structural.rhino_barrier.LaneLineModel(n_lanes: int, lane_width_ft: float, usable_width_ft: float, n_edge_lines: int, n_divider_lines: int, n_objects: int)[source]

Bases: object

Summary of generated lane markings.

lane_width_ft: float
n_divider_lines: int
n_edge_lines: int
n_lanes: int
n_objects: int
usable_width_ft: float
civilpy.structural.rhino_barrier.bar_diameter_in(bar_no: int) float[source]

Nominal diameter (in) of a standard #bar_no reinforcing bar.

civilpy.structural.rhino_barrier.barrier_dc2_klf(designation: str, *, concrete_pcf: float = 150.0) float[source]

Dead load of one barrier run (klf): the gross concrete section_area x unit weight for a concrete section, else the steel weight_per_ft.

civilpy.structural.rhino_barrier.barrier_profile(r, height_ft: float, side: int) list[tuple[float, float]][source]

Cross-section of a concrete barrier as (offset, z) vertices (ft), offset measured transversely from the placement line and z from the deck top. side is +1 / -1 for an edge barrier whose back face is on the line and body toward +Y / -Y, or 0 for a freestanding barrier built symmetric about the line (a PCB or median section).

The vertex order winds around the section; the extruder fans its end caps from the centroid, so mild non-convexity (the NJ / F-shape break) is fine.

civilpy.structural.rhino_barrier.barrier_rebar_curves(prof, y_ref, x0, x1, z0, *, cover_ft, spacing_ft, bar_size, long_spacing_ft)[source]

Bar centerlines for a concrete barrier’s cage: a vertical face bar every spacing_ft along X tracing the section inset by cover_ft, plus longitudinal bars every long_spacing_ft up the inset face. Returns (vertical_pts, longitudinal_pts) – each a list of 3D point lists.

civilpy.structural.rhino_barrier.build_barriers(source, *, out_path, designation: str = 'BR-1 (36 in)', placements=None, overhang_ft: float = 3.5, deck_top_z_ft: float = 0.0, height_in: float | None = None, rebar: bool = True, cover_in: float = 2.0, long_spacing_in: float = 12.0, concrete_pcf: float = 150.0, unit_system=None) BarrierModel[source]

Render the ODOT standard section for designation and write it to out_path (a .3dm the DeckBarrier command imports). source is a GirderBridge or a path to a girder .3dm.

placements selects where the barrier goes:

  • None – one on each deck edge (y_extent +/- overhang_ft), back face outward;

  • "median" – one freestanding barrier on the bridge centerline;

  • a list of (y_ft, side) – explicit transverse offsets, side being +1 / -1 for an edge barrier facing inward / outward or 0 for a freestanding (symmetric) section such as a PCB.

height_in overrides the catalog height. When rebar is true a bar cage is drawn on the Rebar layer for each concrete barrier (skipped for a steel railing, whose reinforcement lives in its curb detail). Returns a BarrierModel.

civilpy.structural.rhino_barrier.build_lane_lines(source, *, out_path, n_lanes: int | None = None, lane_width_ft: float = 12.0, line_width_in: float = 4.0, edge_offset_ft: float = 1.0, overhang_ft: float = 3.5, barrier_base_in: float = 18.0, deck_top_z_ft: float = 0.0, dash_len_ft: float = 10.0, gap_len_ft: float = 30.0, unit_system=None) LaneLineModel[source]

Paint traffic lane markings on the deck top and write them to out_path (a .3dm the DeckLaneLines command imports): a solid edge line edge_offset_ft inside each barrier face and dashed lane dividers every lane_width_ft between them. n_lanes defaults to the usable width divided by lane_width_ft (rounded). Returns a LaneLineModel.

civilpy.structural.rhino_barrier.deck_rebar_curves(x0, x1, y0, y1, z_bot, z_top, *, cover_ft, transverse_spacing_ft, longitudinal_spacing_ft)[source]

Top and bottom reinforcing mats for a deck slab spanning [y0, y1] over [x0, x1] between z_bot and z_top. Returns a list of (form, points) bar centerlines: transverse bars (run in Y at each X station) and longitudinal bars (run in X at each Y station), in both the top and bottom mat, inset by cover_ft.

civilpy.structural.rhino_barrier.read_barrier_model(path)[source]

Read the gdr.kind=barrier | rebar | lane_line objects back from a barrier/markings .3dm: a list of dicts with kind, id, and an attrs map of the kind’s gdr.<kind>.* tags (numeric values as float). Round-trips build_barriers() / build_lane_lines().

civilpy.structural.rhino_barrier.shape_family(r) str[source]

Classify a catalog railing into a geometry family: "new jersey", "single slope", "portable" (PCB F-shape), "combination" (full-height concrete barrier + steel tube pedestrian rail on top, e.g. BR-2-15), "steel tube" (post-and-beam on a low curb, e.g. TST-1/TST-2), or "trapezoid" (generic concrete).

civilpy.structural.rhino_bchw module

BrIM emit for ODOT concrete headwalls on precast box culverts (BCHW).

Builds the complete culvert-end assembly the Design Data sheets (1/6-6/6) tabulate — resolved through design_headwall():

  • both wingwalls, placed per headwall type (Type A: both at 45 deg from the culvert centerline; Type B: wall #1 at 45 deg + wall #2 straight along the roadway line, per tabulated skew; Type C: both parallel to the roadway), tops on the sheet’s 2:1 backslope (level for Type C) down to the 2 ft minimum tip;

  • the foreslope wall — the 6 in / 1’-6” panel that sits on top of the box, leaving the barrel opening clear;

  • an optional precast box stub (display only — the box itself is a separate ASTM C1433 design) so the opening reads as a culvert;

  • footings under each wingwall (with the 4’-0” extension past the tip) and the culvert footing strip across the opening, each with its cutoff wall at the stream edge;

  • the sheet’s reinforcing series as bar centerlines: “X” verticals and “Y” footing dowels at the tabulated size/spacing on the stream face, the #5 @ 18 near-face/horizontal mats, “V”/”W” footing mats and “Z” cutoff bars per the footing design number, and the foreslope-wall bars.

Every object lands on a Culvert::* layer with bim.* / pay.* / mat.* user text. Quantities come from the sheet tables, not from the drawn geometry: concrete solids carry their tabulated cy (whole-assembly values split across the parts) and the tabulated reinforcing lbs ride on per-part schedule markers, so pay_item_quantities() reproduces the sheet’s estimated quantities exactly. Individual bars are visual and carry rebar.* metadata but no pay block (they would double-count the schedule markers).

Frame: culvert axis = +x (the barrel runs into +x), headwall face through the origin, z = 0 at the top of footing, y = 0 on the culvert centerline. A Type B/C roadway skew rotates the face line by theta about z.

The record draws through the BCHW Grasshopper component, Notebooks/Rhino Components/draw_bim_emit.py, and emit_to_3dm().

class civilpy.structural.rhino_bchw.BchwEmit(design: ~civilpy.structural.odot.box_culvert_headwall.HeadwallDesign, objects: tuple[~civilpy.structural.rhino_bim.EmitObject, ...], doc_tags: dict[str, str] = <factory>)[source]

Bases: object

One culvert-end headwall assembly — duck-compatible with emit_to_json() and emit_to_3dm().

design: HeadwallDesign
doc_tags: dict[str, str]
objects: tuple[EmitObject, ...]
civilpy.structural.rhino_bchw.bchw_emit(inp: HeadwallInput, *, box_stub_ft: float = 4.0, rebar: bool = True) BchwEmit[source]

Tagged BrIM geometry for one culvert-end headwall assembly, resolved from the Design Data tables (see the module docstring). box_stub_ft draws that much display-only precast box behind the headwall (0 skips it); rebar=False skips the bar centerlines (the tabulated schedule markers, and therefore the quantities, stay either way).

civilpy.structural.rhino_bim module

BrIM emit layer for the parametric steel-girder bridge.

Turns a BridgeLayout into tagged, transport-neutral geometry records — the full “source of truth” model the Rhino document carries: girder solids with true k-fillets, the crowned deck solid, haunches, welded shear studs, both deck rebar mats, parapets, bearings, and load plates, each stamped with its civilpy.structural.bim attribute set (bim.type/bim.id, bim.scd/bim.scd_year where the part is a standard detail, a pay.* pay-item block, and a mat.* material block).

The point of the neutral record (same architecture as rhino_slab) is that the engineering content is described exactly once; a backend only decides how to draw a prism or a cylinder and how to stamp a user string, never where a stud goes. Known backends:

  • Notebooks/Rhino Components/draw_bim_emit.py — live-document driver (run inside Rhino 8, from the ScriptEditor or over MCP) that consumes emit_to_json().

  • The odot_bridge_generator_ghpython.py Grasshopper component shares the same layout_bridge() layout for its preview geometry.

Coordinates are feet (the hub convention): X = stations along the layout centerline, Y = transverse (girder 1 at y = 0), Z = 0 at the top of deck at the crown. Section dimensions in the tags stay in inches (_in suffixes).

Geometry record kinds

prism

A closed planar loop (points, unrepeated) extruded along vector. Every solid here is a prism: the bridge is prismatic along its length, so the deck (skew-cut ends), girders (square-cut ends), haunches, parapets, bearings, and plates all emit this way.

polyline

Open polyline through points (rebar; girder centerlines).

cylinder

points = (base, tip) plus radius_ft (shear studs).

point

A marker. The bim.type = bridge marker carries the bridge-wide parameters, because standalone rhino3dm cannot write or read the RhinoDoc string table (the same contract rhino_slab follows); a live-document backend additionally mirrors them into doc.Strings for the rhino_gdr reader.

The gdr.* analysis contract is preserved untouched: girder centerlines and bearing points still carry their gdr.* tags (plus a bim.id back-reference), so a document drawn from this emit remains readable by civilpy.structural.rhino_gdr and the MIDAS pipeline.

civilpy.structural.rhino_bim.BEARING_PLY_IN = 0.6

internal elastomer ply thickness

civilpy.structural.rhino_bim.BEARING_SIDE_IN = 20.0

square elastomeric pad plan side

class civilpy.structural.rhino_bim.BridgeEmit(inputs: BridgeInput, layout: BridgeLayout, objects: tuple[EmitObject, ...], doc_tags: dict[str, str])[source]

Bases: object

Everything a backend needs to draw one steel-girder BrIM model.

doc_tags: dict[str, str]
inputs: BridgeInput
layout: BridgeLayout
objects: tuple[EmitObject, ...]
of_type(bim_type: str) tuple[EmitObject, ...][source]

The emitted objects whose bim.type matches.

class civilpy.structural.rhino_bim.EmitObject(kind: str, layer: str, points: tuple[tuple[float, float, float], ...], tags: dict[str, str] = <factory>, vector: tuple[float, float, float] | None = None, radius_ft: float | None = None)[source]

Bases: object

One drawable object, independent of any Rhino API (see the module docstring for the kind vocabulary).

KINDS = ('prism', 'polyline', 'cylinder', 'point')
kind: str
layer: str
points: tuple[tuple[float, float, float], ...]
radius_ft: float | None = None
tags: dict[str, str]
vector: tuple[float, float, float] | None = None
civilpy.structural.rhino_bim.PLATE_SIDE_IN = 21.0

load plate plan side

civilpy.structural.rhino_bim.SBR1_EMBED_REDUCTION_IN = 1.5

embedment = deck thickness - this

civilpy.structural.rhino_bim.SBR1_LEG_IN = 12.0

horizontal deck-lap leg of Y601/Y602

civilpy.structural.rhino_bim.SBR1_TOP_BAR_DROP_IN = 3.0

X401 pair below the rail top

civilpy.structural.rhino_bim.SBR1_Y601_TOP_BEND_IN = 5.25

Y601 top bend toward the back face

civilpy.structural.rhino_bim.SBR1_Y602_HOOK_OVER_IN = 7.0

over toward traffic …

Type:

top hairpin

civilpy.structural.rhino_bim.STEEL_UNIT_WT_PCF = 490.0

structural steel unit weight, lb/ft^3

civilpy.structural.rhino_bim.STUDS_PER_ROW = 3

studs across the top flange per row

civilpy.structural.rhino_bim.STUD_DIA_IN = 0.875

welded stud diameter, 7/8 in typical

civilpy.structural.rhino_bim.STUD_GAUGE_IN = 3.0

transverse c/c between studs in a row

civilpy.structural.rhino_bim.STUD_LENGTH_IN = 6.0

stud length — embeds ~4 in into an 8.5 in deck

civilpy.structural.rhino_bim.STUD_PITCH_IN = 24.0

longitudinal row pitch (preliminary)

civilpy.structural.rhino_bim.SUB_FC_PSI = 4000.0

Class QC1 substructure concrete

class civilpy.structural.rhino_bim.SubRebarSpec(stirrup_size: int = 5, stirrup_spacing_in: float = 12.0, column_bar_size: int = 9, column_tie_size: int = 4, column_tie_spacing_in: float = 12.0, wall_bar_size: int = 5, wall_bar_spacing_in: float = 12.0, cover_in: float = 3.0, wall_cover_in: float = 2.0, coating: str = 'epoxy')[source]

Bases: object

Substructure reinforcing parameters that the executed designs do not size themselves. The cap’s main (tie) steel comes from the STM bar schedule on the placed CapBeam and the column verticals from the bent’s RebarLayer area — everything here is the detailing around them: the stirrups the cap shear check was run with, the bar size the column steel area is broken into, its ties, and the nominal two-face wall mats (temperature/shrinkage-level; final wall design replaces them).

coating: str = 'epoxy'
column_bar_size: int = 9
column_tie_size: int = 4
column_tie_spacing_in: float = 12.0
cover_in: float = 3.0

caps / columns / footings

stirrup_size: int = 5
stirrup_spacing_in: float = 12.0
wall_bar_size: int = 5
wall_bar_spacing_in: float = 12.0
wall_cover_in: float = 2.0
civilpy.structural.rhino_bim.add_girder_details(emit: BridgeEmit, record, *, apply_to=None) BridgeEmit[source]

A new BridgeEmit with one SteelGirderRecord’s §3a customizations (cross-frames, transverse/bearing/longitudinal stiffeners, and a field splice) appended as tagged geometry — mirroring add_substructure(), so the merged emit still round-trips through emit_to_json(), read_bim_tags(), and the quantity rollup.

Placement uses the emit’s solved girder lines; apply_to restricts it to those line_no (default: every line). The girder depth comes from the record’s controlling section (catalog or plate). Only the details present on the record are emitted.

civilpy.structural.rhino_bim.add_substructure(emit: BridgeEmit, sub, *, fc_psi: float = 4000.0, rebar: SubRebarSpec | None = SubRebarSpec(stirrup_size=5, stirrup_spacing_in=12.0, column_bar_size=9, column_tie_size=4, column_tie_spacing_in=12.0, wall_bar_size=5, wall_bar_spacing_in=12.0, cover_in=3.0, wall_cover_in=2.0, coating='epoxy')) BridgeEmit[source]

A new BridgeEmit with the substructure appended to the superstructure emit — same doc tags, so the merged record still round-trips through emit_to_json(), read_bim_tags(), and the quantity rollup.

civilpy.structural.rhino_bim.emit_to_3dm(emit: BridgeEmit, path, *, version: int = 7, mesh: bool = False) dict[str, int][source]

Bake the emit straight into a .3dm with standalone rhino3dm — no Rhino session, no MCP server, no network: the notebook-only path to a real Rhino file. Prisms become capped extrusion breps, studs cylinder breps, rebar/centerlines polyline curves, markers points, on the same colored layer taxonomy as the live-document driver, each object stamped with its full user-text tags. The bridge-wide record rides on the bim.type = bridge marker (standalone rhino3dm has no document string table — see the point record kind in the module docstring), so read_bim_tags() / read_bim_quantities() round-trip from the saved file alone. Returns per-layer object counts.

mesh=True routes every solid (prisms and stud cylinders) through closed-mesh construction instead of breps — the web-viewer flavor: headless rhino3dm cannot tessellate breps, so three’s 3DMLoader renders a brep file empty, while meshes arrive shaded. Tag stamping and read-back are identical in both flavors.

Notebooks/Rhino Components/draw_bim_emit.py remains the live-document twin (same JSON payload via emit_to_json()) when a running Rhino 8 should host the model instead of a file.

civilpy.structural.rhino_bim.emit_to_json(emit: BridgeEmit) str[source]

Serialize the emit for draw_bim_emit.py (a pure-Rhino script — no civilpy import needed inside Rhino, so a stale civilpy in Rhino’s site environment cannot skew the geometry). Layer colors ride along so every backend paints the shared taxonomy identically.

civilpy.structural.rhino_bim.girder_bridge_emit(inp: BridgeInput, *, scd_year: int | str = 2020, stud_pitch_in: float = 24.0, studs_per_row: int = 3, stud_dia_in: float = 0.875, stud_length_in: float = 6.0, side_cover_in: float = 2.0, rebar_coating: str = 'epoxy', integral_supports: tuple[int, ...] = ()) BridgeEmit[source]

Build the tagged BrIM geometry for one steel-girder bridge.

Shear studs are emitted only for a composite layout — they are the physical composite connection, mirroring the toggle the analysis models use. integral_supports lists support-line indices with integral abutments: no load plate / bearing pad is drawn there (the girder ends are cast into the end diaphragm) while the tagged gdr.* support point stays for the analysis reader. Raises whatever layout_bridge() raises for inputs that violate the ODOT standard-design assumptions.

civilpy.structural.rhino_bim.i_profile_wh(section, arc_pts: int = 4) list[tuple[float, float]][source]

Closed W-shape outline as (w, h) pairs in inches, w across the flange (0 at the web centerline), h up from the bottom face. The web-to-flange k-fillets (no square re-entrant corners) are tessellated with arc_pts points each; a section without a cataloged fillet_k falls back to square corners.

civilpy.structural.rhino_bim.pay_item_quantities(emit: BridgeEmit) dict[str, dict][source]

Quantity rollup straight from an emit (see _tag_quantities()).

civilpy.structural.rhino_bim.read_bim_estimate(path, prices: dict[str, float] | None = None, *, include_hidden: bool = False)[source]

Priced pay-item rollup straight from a saved BrIM .3dm — the quantity read-back run through civilpy.structural.bim.cost_estimate(). Unit prices default to the planning-level DEFAULT_UNIT_PRICES book; pass prices to override with project numbers.

civilpy.structural.rhino_bim.read_bim_quantities(path, *, include_hidden: bool = False) dict[str, dict][source]

Pay-item rollup for a saved BrIM .3dm (read-back + estimate).

civilpy.structural.rhino_bim.read_bim_tags(path, *, include_hidden: bool = False) dict[source]

Read a BrIM-tagged .3dm back: every object carrying bim.type returns its full user-text dict, and the bim.type = bridge marker’s tags come back as the bridge-wide record.

Components on hidden layers (directly, or under a hidden parent — e.g. a Legacy tree kept for reference) are skipped, so the rollup counts exactly what the Rhino document displays; pass include_hidden=True to read everything. The bridge marker is document metadata and comes back regardless.

This is the round-trip half of the source-of-truth contract: the saved Rhino document alone carries enough attributes to regenerate the estimate (and, through the preserved gdr.* tags, the analysis model) without the session that drew it. Returns {"bridge": {...}, "components": [tags, ...]}.

civilpy.structural.rhino_bim.stm_overlay_emit(model, geom, layout) tuple[EmitObject, ...][source]

The solved cap strut-and-tie model drawn in place on the Substructure::STM layers (ties red, struts blue) — the analysis overlay merged into the main document instead of a separate .3dm.

model is the solved StrutAndTieModel from the cap’s PierCapDesign (its 2D frame: x along the cap in the girder-1-origin coordinates the reactions were given in, y up from the cap bottom); geom the placed pier/abutment geometry it belongs to. Members carry stm.kind / stm.force_kip and no pay item, so every estimate rollup ignores them.

civilpy.structural.rhino_bim.substructure_emit(sub, *, fc_psi: float = 4000.0, rebar: SubRebarSpec | None = SubRebarSpec(stirrup_size=5, stirrup_spacing_in=12.0, column_bar_size=9, column_tie_size=4, column_tie_spacing_in=12.0, wall_bar_size=5, wall_bar_spacing_in=12.0, cover_in=3.0, wall_cover_in=2.0, coating='epoxy')) tuple[EmitObject, ...][source]

Tagged BrIM geometry for a placed SubstructureLayout: pier caps / columns / footings and abutment caps / piles / backwalls / wingwalls on the Substructure::* layers, with the stepped beam seats under every bearing stack. All cast-in-place concrete measures into the Class QC1 substructure item; piles into the steel-pile item (ft).

rebar adds the reinforcing cage: cap main steel from the placed STM tie schedule, column verticals from the bent’s steel area, and the detailing in SubRebarSpec (pass None to skip).

civilpy.structural.rhino_box_beam module

Prestressed concrete adjacent box-beam bridges in Rhino (stage G9, the precast-girder companion to the steel line-girder pipeline in civilpy.structural.rhino_gdr).

Box beams are a different structural system from the steel-girder pipeline: full-length precast members that sit side by side (spacing = beam width, no gap) rather than field-spliced curves needing shape transitions. This module lays out a straight, zero-skew box-beam bridge from three inputs – a standard box designation (BOX_DESIGNATIONS, e.g. "CB27-48"), a span, and a beam count – and writes the geometry a BoxBeamLines Rhino command imports:

  • one girder-line curve per beam (gdr.kind=girder, gdr.family=box) so the line-girder-style downstream tooling can still find the framing;

  • the true box-beam solid, built as four wall meshes (top/bottom flange, two webs) using the wall thickness read off PSBD-1-25 sheet 3 (BOX_WALL_THICKNESS_IN) – a hollow tube, not a solid rectangle, though the void corners are drawn square rather than filleted (cosmetic simplification, same category as the steel pipeline’s display W-shapes);

  • a schematic prestressing tendon centerline per active strand row (2 in / 4 in / 6 in above the soffit, from the PSBDD-1-25 strand pattern) run the full beam length – straight, since box beams debond rather than harp; a full per-strand model is a possible future refinement;

  • transverse diaphragms at the PSBD-1-25 sheet 4/6 count and (for multiples) evenly-spaced stations between the end offsets (diaphragm_stations_ft());

  • tie rods at each diaphragm station, at the PSBD-1-25 tie-rod height (TIE_ROD);

  • a bearing pad footprint at each beam end, sized from the design’s standard pad (BEARING_PADS);

  • an optional composite topping slab, when the box’s design table calls for one, at the standard structural thickness plus wearing surface.

Skewed layouts are not yet supported (skew_deg stays 0); the PSBD-1-25 skew rules for diaphragm/tie-rod offset (sheet 2/6) are a follow-up. This is display geometry plus engineering tags, same as the deck/barrier modules; it does not yet wire a box-beam bridge into the MIDAS payload builder (a different structural idealization from the line-girder model).

rhino3dm is an optional dependency imported lazily.

class civilpy.structural.rhino_box_beam.BoxBeamBridgeModel(box: str, beam_type: str, depth_in: float, width_in: float, span_ft: float, n_beams: int, bridge_width_ft: float, n_strands: int, bearing_type: str, composite: bool, self_weight_klf_per_beam: float, n_diaphragms: int, n_beam_solids: int, n_tendons: int, n_tie_rods: int, n_bearing_pads: int, n_diaphragm_objects: int, n_slab: int = 0)[source]

Bases: object

Summary of a generated box-beam bridge: what was placed and its engineering payload. Lengths in the units named; loads in klf.

beam_type: str
bearing_type: str
box: str
bridge_width_ft: float
composite: bool
depth_in: float
n_beam_solids: int
n_beams: int
n_bearing_pads: int
n_diaphragm_objects: int
n_diaphragms: int

diaphragm_count – the INTERMEDIATE diaphragms only. The bridge also has two end diaphragms, so n_diaphragm_objects is this + 2.

n_slab: int = 0
n_strands: int
n_tendons: int
n_tie_rods: int
self_weight_klf_per_beam: float
span_ft: float
width_in: float
civilpy.structural.rhino_box_beam.CONCRETE_PCF = 150.0

Concrete unit weight (pcf) for the box-beam and topping-slab self-weight.

civilpy.structural.rhino_box_beam.DIAPHRAGM_THICKNESS_IN = 8.0

Cosmetic diaphragm thickness – PSBD-1-25 gives the count and end offset but not a thickness in the section carried here; this is a display value only, not sourced from the drawing.

civilpy.structural.rhino_box_beam.build_box_beams(*, out_path, box: str, span_ft: float, n_beams: int, x0_ft: float = 0.0, y0_ft: float = 0.0, concrete_pcf: float = 150.0, unit_system=None) BoxBeamBridgeModel[source]

Lay out a straight, zero-skew adjacent box-beam bridge and write it to out_path (a .3dm the BoxBeamLines command imports).

box is a standard designation from BOX_DESIGNATIONS (e.g. "CB27-48", composite; or "B27-48", non-composite); span_ft must be one of that box’s cataloged spans (box_beam_design() raises a KeyError naming the valid spans otherwise). Beams are placed edge to edge starting at (x0_ft, y0_ft), each spanning the full width (no gap, per “adjacent box beam”). Returns a BoxBeamBridgeModel.

civilpy.structural.rhino_box_beam.read_box_beam_model(path)[source]

Read the gdr.kind=girder | box_beam | tendon | diaphragm | tie_rod | bearing_pad | deck objects back from a box-beam .3dm: a list of dicts with kind, id, and an attrs map of every gdr.* tag on the object (numeric values as float where they parse). Round-trips build_box_beams() and mirrors what the BoxBeamLines importer carries.

civilpy.structural.rhino_box_bim module

BrIM emit layer for prestressed adjacent box-beam bridges.

The rhino_box_beam legacy writer ported to the transport-neutral emit architecture: the same PSBD-1-25 / PSBDD-1-25 standard-design content —

  • box members as four wall prisms each (hollow tube, square-drawn void corners), every part tagged bim.type = box_beam with the beam id; one part per beam carries the 515 member pay item (strands, tie rods, and precast diaphragms are included in the member);

  • a schematic strand-row polyline per active row of the PSBDD-1-25 pattern (2 / 4 / 6 in above the soffit — straight, boxes debond rather than harp);

  • transverse diaphragms and tie rods at the standard stations;

  • standard bearing pads under each beam end (516 item);

  • the composite topping slab when the design line calls for one (511 superstructure concrete);

  • the gdr.* girder centerline per beam (gdr.family = box) so the BoxBeamLines importer and line-girder tooling still work.

Drawn by the same draw_bim_emit.py driver and read back by the same read_bim_quantities() as the steel slice. Coordinates are feet: X spans 0..span, Y across the beams (beam 1 edge at y = 0), Z = 0 at the box soffit.

Skewed layouts are not yet supported (the PSBD-1-25 sheet 2/6 skew rules for diaphragm/tie-rod offsets are the follow-up), and no railing is emitted — the barrier family on boxes differs from the deck-girder SBR standard.

class civilpy.structural.rhino_box_bim.BoxBridgeInput(box: str, span_ft: float, n_beams: int, skew_deg: float = 0.0, fc_psi: float = 6000.0)[source]

Bases: object

One adjacent box-beam bridge on the ODOT standard designs.

box is a PSBDD-1-25 designation ("CB27-48" composite / "B27-48" non-composite); span_ft must be one of that box’s cataloged spans. Beams sit edge to edge (adjacent), beam 1 at y = 0.

box: str

"CB27-48" composite / "B27-48" non-composite – a key of the PSBD span tables in civilpy.structural.odot.box_beam_design.

Type:

PSBDD-1-25 box designation

fc_psi: float = 6000.0
n_beams: int
skew_deg: float = 0.0
span_ft: float
civilpy.structural.rhino_box_bim.DIAPHRAGM_THICKNESS_IN = 8.0

Display thickness for the precast diaphragms (PSBD-1-25 gives count and stations but no thickness in the carried section) — cosmetic only.

civilpy.structural.rhino_box_bim.box_beam_bridge_emit(inp: BoxBridgeInput, *, concrete_pcf: float = 150.0) BridgeEmit[source]

Build the tagged BrIM geometry for one adjacent box-beam bridge.

Raises KeyError (naming the valid spans) when span_ft is not a cataloged design for box, and ValueError for a skewed layout (not yet supported).

civilpy.structural.rhino_deck module

Deck slab, parapets, and railing geometry for the girder-line model (stage G6, the deck companion to civilpy.structural.rhino_gdr).

Given a girder-line bridge (the tagged .3dm the GirderLines / GirderShape commands author, read by read_girder_model()), this builds the riding surface the girders carry:

  • a deck slab spanning the full bridge length across all girder lines plus an overhang on each side;

  • a parapet swept along each deck edge, its cross-section taken from the ODOT bridge-railing catalog (civilpy.structural.odot.bridge_railing); and

  • an optional railing (a steel top rail) atop each edge.

The geometry is written to a companion .3dm the GirderDeck command imports (the same write-back handoff the splice pipeline uses), tagged gdr.kind=deck | parapet | railing so the girder reader — which only consumes girder and support — ignores it, while the deck’s engineering payload (deck thickness, the per-girder deck DC1 and per-parapet DC2 line loads) rides along in the tags for the analysis model to pick up.

The slab and parapet shapes are display geometry (like the cosmetic girder sections); the loads they carry are the real contribution to the MIDAS model. rhino3dm is an optional dependency imported lazily.

civilpy.structural.rhino_deck.CONCRETE_PCF = 150.0

Reinforced-concrete unit weight (pcf) for deck and parapet dead load.

civilpy.structural.rhino_deck.DEFAULT_DECK_T_IN = 8.5

Default deck slab thickness (in) when the model carries no gdr.deck_t.

civilpy.structural.rhino_deck.DEFAULT_OVERHANG_FT = 3.5

Default deck overhang beyond each exterior girder line (ft).

civilpy.structural.rhino_deck.DEFAULT_PARAPET = 'BR-1 (36 in)'

Default parapet designation (36 in New Jersey, TL-4).

class civilpy.structural.rhino_deck.DeckModel(deck_t_in: float, width_ft: float, length_ft: float, overhang_ft: float, girder_spacing_ft: float, n_girder_lines: int, deck_dc1_klf_interior: float, parapet: str, parapet_height_in: float, parapet_dc2_klf_each: float, railing: str | None, railing_dc2_klf_each: float, n_deck: int, n_parapet: int, n_railing: int, n_rebar: int = 0)[source]

Bases: object

Summary of a generated deck: geometry counts plus the dead-load quantities the analysis model needs. Lengths in the units named; loads in kips/ft (klf).

deck_dc1_klf_interior: float
deck_t_in: float
girder_spacing_ft: float
length_ft: float
n_deck: int
n_girder_lines: int
n_parapet: int
n_railing: int
n_rebar: int = 0
overhang_ft: float
parapet: str
parapet_dc2_klf_each: float
parapet_height_in: float
railing: str | None
railing_dc2_klf_each: float
property total_dc2_klf: float

Combined parapet + railing dead load from both edges (klf), the value the two exterior girders share as DC2.

width_ft: float
civilpy.structural.rhino_deck.build_deck(source, *, out_path, deck_t_in: float | None = None, overhang_ft: float = 3.5, haunch_in: float = 0.0, deck_bottom_z_ft: float = 0.0, parapet: str = 'BR-1 (36 in)', railing: str | None = None, railing_height_in: float = 42.0, rebar: bool = False, rebar_cover_in: float = 2.5, rebar_transverse_spacing_in: float = 6.0, rebar_longitudinal_spacing_in: float = 12.0, concrete_pcf: float = 150.0, unit_system=None) DeckModel[source]

Generate the deck slab, edge parapets, and optional railing for a girder model and write them to out_path (a .3dm the GirderDeck command imports). source is a GirderBridge or a path to a girder .3dm.

deck_t_in defaults to the model’s gdr.deck_t and then DEFAULT_DECK_T_IN. The deck spans the full girder length and the full transverse girder spread plus overhang_ft on each side; its bottom sits at deck_bottom_z_ft + haunch_in above the girder-line plane (cosmetic — the girder line is the analysis reference). Returns a DeckModel with the geometry counts and the DC1/DC2 dead loads.

civilpy.structural.rhino_deck.parapet_dc2_klf(designation: str, *, concrete_pcf: float = 150.0)[source]

Dead load of one parapet/railing run (klf) from the catalog: the gross concrete section_area × unit weight for a concrete barrier, else the steel weight_per_ft. Returns 0.0 when the catalog states neither.

civilpy.structural.rhino_deck.read_deck_model(path)[source]

Read the gdr.kind=deck | parapet | railing objects back from a deck .3dm: a list of dicts with kind, id, and an attrs map of the kind’s gdr.<kind>.* tags (numeric values as float). Round-trips build_deck() and mirrors what the GirderDeck importer carries.

civilpy.structural.rhino_gdr module

Rhino 3D -> girder-line model (the gdr.* reader, stage G4).

Parses a tagged .3dm authored by the C# GirderLines / GirderShape / GirderBearing commands into the canonical StructuralModel hub, following the girder / field-splice contract in docs/Rhino Design Philosophy.md:

Geometry carries what is spatial; tags carry what is scalar.

  • Girder lines are curves tagged gdr.kind=girder with gdr.shape (AISC label, e.g. W24X104), gdr.grade (default Grade 50), and gdr.line (girder number). A polyline yields one Element per segment (a continuous-span chain); each element carries the resolved AISC section label and the grade.

  • Bearings are points tagged gdr.kind=support with gdr.fixity=fixed|expansion and gdr.line; they become 6-DOF Restraints on the nearest node of their girder line.

  • Bridge parameters live in document user text (gdr.deck_t, gdr.deck_weff, gdr.deck_fc, gdr.ship_max, gdr.bolt_*) and are returned as a GirderBridge alongside the hub.

Plane convention: PLAN – X = stations along the bridge, Y = transverse, Z = up; lengths in feet (ModelUnitSystem honored on read, mirroring rhino_stm). rhino3dm is an optional dependency imported lazily.

class civilpy.structural.rhino_gdr.GirderBridge(model: StructuralModel, deck_t: float | None = None, deck_weff: float | None = None, deck_fc: float = 4.0, ship_max: float = 100.0, bolt_dia: float = 0.875, bolt_spec: str = 'A325', bolt_hole: str = 'oversize', bolt_class: str = 'C', girder_lines: dict[str, list[str]]=<factory>)[source]

Bases: object

The canonical hub plus the document-level bridge parameters the girder / splice pipeline needs (deck composite section + bolt standard). Missing deck_t / deck_weff are None (a loud warning is issued on read); the rest fall back to the ODOT BDM 308.2.2.1.j defaults.

bolt_class: str = 'C'
bolt_dia: float = 0.875
bolt_hole: str = 'oversize'
bolt_spec: str = 'A325'
deck_fc: float = 4.0
deck_t: float | None = None
deck_weff: float | None = None
girder_lines: dict[str, list[str]]
model: StructuralModel
ship_max: float = 100.0
class civilpy.structural.rhino_gdr.SpliceMarker(point: tuple, design: object, line: str = '', id: str = '')[source]

Bases: object

A designed splice to write back: its station point (feet, PLAN), the SpliceDesign, the gdr.line it belongs to, and an optional persistent gdr.id (minted if empty, same identity rule as the C# authored tags).

design: object
id: str = ''
line: str = ''
point: tuple
civilpy.structural.rhino_gdr.grade_fy_fu(grade: str) tuple[float, float][source]

(Fy, Fu) ksi for a gdr.grade name; warns + defaults to Grade 50.

civilpy.structural.rhino_gdr.read_girder_model(path, *, tol=0.5) GirderBridge[source]

Read a tagged Rhino .3dm into a GirderBridge (the canonical hub + bridge parameters). One Element chain per gdr.line; Element.section is the resolved AISC label, Element.material the grade name. Bearings become 6-DOF restraints on the nearest node of their girder line.

Parameters:
  • path (str) – Path to the .3dm file.

  • tol (float) – Snap tolerance (feet) for attaching a bearing point to a girder node.

civilpy.structural.rhino_gdr.read_splice_results(path)[source]

Read gdr.kind=splice markers back from a .3dm: a list of dicts with point (feet), line, id, status, summary, checks (parsed into [article, check, actual, allowable, verdict] records), and attrs – the gdr.splice.* smart-node attribute set with the prefix stripped and numeric values converted to float (e.g. attrs["tf.bolts"], attrs["bolt_dia"]). Round-trips write_splice_results() and mirrors what C# reads; display geometry (no gdr.kind) is ignored by contract.

civilpy.structural.rhino_gdr.resolve_shape(label: str)[source]

Look up an AISC W-shape by (already-normalized) label via steel.W; returns the section or None (with a warning) if it does not resolve. Import is local so the reader works without the steel db warmed.

civilpy.structural.rhino_gdr.splice_attribute_tags(design, inp=None) dict[source]

Build the gdr.splice.* smart-node attribute tags from a SpliceDesign.

These ride on the gdr.kind=splice marker point next to the status/summary/checks write-back, so the marker carries the designed splice the way a bearing carries its fixity – readable in Rhino’s object properties and by the C# GirderSplice dialog. All lengths are inches; bolt counts are per side of the joint.

Marker-level keys: bolt_spec, bolt_dia, hole_type, hole_dia, layout (inline; staggered reserved – the designer lays out straight rows), gap (girder end gap), method.

Per-component keys under tf. / bf. / web.: bolts, rows x cols (the per-side grid), pitch (along the load path), gage (across it), edge, end, and the plate stack – plate_t / plate_w / plate_l where plate_l always runs along the girder axis and plate_w across it (transverse for flange plates, vertical for the paired web plates). Flanges add the two inner plates (inner_t / inner_w) and the joint-straddling spacings pitch_joint (flange, longitudinal) / gage_web (across the web); the web adds gage_joint (its across-the-gap spacing).

inp is the SpliceInput; it defaults to design.spec (attached by design_splice).

civilpy.structural.rhino_gdr.splice_writeback_tags(design) dict[source]

Build the gdr.status / gdr.summary / gdr.checks write-back tags (stage G8) from a SpliceDesign.

gdr.checks is newline-separated article|check|actual|allowable|verdict records – one row per limit state – the format the C# GirderSplice command renders (NG rows red).

civilpy.structural.rhino_gdr.write_splice_results(out_path, markers, *, unit_system=None, display=True)[source]

Author gdr.kind=splice smart-node marker points into a new .3dm for the C# GirderSplice command.

Each marker carries the G8 check write-back (gdr.status / gdr.summary / gdr.checks), a persistent gdr.id, and – when the design retains its SpliceInput (design.spec, set by design_splice) – the full gdr.splice.* attribute set: bolt spec and hole size, per-component bolt grids and spacings, and the three plate stacks (see splice_attribute_tags()). With display=True true-scale plate boxes and bolt-axis lines are baked next to each marker on the Splice Display layer; they carry no gdr.kind so they stay invisible to Python readers, and the C# importer carries them across for viewing only.

markers is a list of SpliceMarker; geometry is written in unit_system (default feet). Returns the number of markers written.

civilpy.structural.rhino_layers module

Shared Rhino layer taxonomy for civilpy’s .3dm writers.

Every civilpy module that writes a .3dm (rhino_gdr, rhino_deck, rhino_barrier, rhino_box_beam, and the ODOT SCD Grasshopper components) should bake into these layers instead of inventing its own flat names, so a file civilpy writes and the C# RhinoODOTExtension plugin reads (or vice versa) resolve to the same nested layer instead of silently duplicating it under a differently-spelled one.

Deck / Superstructure / Substructure are authoritative and pinned to the C# side – transcribed byte-for-byte from ODOT Rhino Plugin/Core/Gdr.cs (RhinoODOTExtension repo, commit 34f7051, “GirderDeck command + Deck/Superstructure/Substructure layer groups”). This mirrors NBIS/SNBI’s three inspection groups (deck, superstructure, substructure condition ratings), so the layer tree reads the way a bridge is actually inspected and the same grouping can carry straight through to Midas/BrR/AssetWise/IFC element groups later – that alignment is the point of a shared layer taxonomy, not an afterthought.

Culvert and Site are civilpy-side PROPOSED additions (not yet in Gdr.cs) for the ODOT SCD standard-drawing components that are neither deck, superstructure, nor substructure: headwalls/box culverts (Culvert) and off-structure roadway items like approach slabs, portable barrier, and drainage strips (Site). See the dated “PROPOSED” section in docs/Rhino Design Philosophy.md for the reconciliation request; do not treat these two as frozen until the C# side signs off, same as every other gdr.* contract addition.

civilpy.structural.rhino_layers.DEFAULT_COLORS: dict[str, tuple[int, int, int, int]] = {'Culvert': (130, 150, 150, 255), 'Culvert::Box': (170, 170, 175, 255), 'Culvert::Footings': (110, 110, 115, 255), 'Culvert::Foreslope Walls': (150, 150, 155, 255), 'Culvert::Rebar': (60, 120, 200, 255), 'Culvert::Wingwalls': (145, 135, 120, 255), 'Deck': (170, 170, 175, 255), 'Deck::Approach Slab': (173, 216, 230, 255), 'Deck::Approach Slab::Rebar': (255, 140, 0, 255), 'Deck::Bridge Deck': (170, 170, 175, 255), 'Deck::Lane Markings': (245, 225, 70, 255), 'Deck::Rebar': (60, 120, 200, 255), 'Deck::Traffic Barriers': (150, 150, 155, 255), 'Site': (150, 140, 120, 255), 'Substructure': (120, 90, 70, 255), 'Substructure::Backwalls': (150, 140, 125, 255), 'Substructure::Beam Seats': (170, 160, 145, 255), 'Substructure::Caps': (155, 145, 130, 255), 'Substructure::Columns': (140, 130, 115, 255), 'Substructure::Footings': (120, 110, 95, 255), 'Substructure::Piles': (70, 70, 80, 255), 'Substructure::Rebar': (60, 120, 200, 255), 'Substructure::STM': (150, 150, 150, 255), 'Substructure::STM::Struts': (40, 80, 200, 255), 'Substructure::STM::Ties': (200, 40, 40, 255), 'Substructure::Wingwalls': (145, 135, 120, 255), 'Superstructure': (40, 40, 40, 255), 'Superstructure::Bearings': (0, 110, 200, 255), 'Superstructure::Box Beams': (140, 140, 145, 255), 'Superstructure::Diaphragms': (100, 100, 180, 255), 'Superstructure::Display': (110, 125, 140, 255), 'Superstructure::Girders': (40, 40, 40, 255), 'Superstructure::Haunches': (200, 200, 190, 255), 'Superstructure::Load Plates': (90, 90, 100, 255), 'Superstructure::Shear Studs': (200, 120, 40, 255), 'Superstructure::Splices': (200, 30, 30, 255), 'Superstructure::Tendons': (200, 160, 20, 255), 'Superstructure::Tie Rods': (180, 60, 60, 255)}

Default RGBA colors, keyed by full layer path – kept alongside the path constants so every writer paints the same layer the same color.

civilpy.structural.rhino_layers.LAYER_APPROACH_SLAB = 'Deck::Approach Slab'

PROPOSED leaves for the AS-1-15 approach slab emit (rhino_approach_slab): human-readable hierarchy under Deck (the slab rides with the roadway surface), rebar on its own toggleable/estimable sub-layer.

civilpy.structural.rhino_layers.LAYER_CULVERT_BOX = 'Culvert::Box'

PROPOSED leaves for the BCHW headwall/wingwall emit (rhino_bchw).

civilpy.structural.rhino_layers.LAYER_HAUNCHES = 'Superstructure::Haunches'

concrete haunches, welded shear studs, and bearing load plates get their own layers under Superstructure so the composite-connection and bearing hardware can be toggled/estimated independently of the girders.

Type:

PROPOSED leaves for the BrIM source-of-truth model (rhino_bim)

civilpy.structural.rhino_layers.LAYER_SUBSTRUCTURE = 'Substructure'

Reserved – no elements are generated yet (matches Gdr.cs’s own comment).

civilpy.structural.rhino_layers.LAYER_SUB_CAPS = 'Substructure::Caps'

PROPOSED leaves under the (reserved) Substructure group, mirroring the Superstructure taxonomy one component per layer: cap beams, the stepped beam seats on top of them, columns, footings, driven piles, abutment backwalls and wingwalls, the substructure reinforcing cage, and an STM layer holding the strut-and-tie analysis overlay (non-pay — excluded from every estimate rollup).

civilpy.structural.rhino_layers.ensure_layer(f, full_path: str, color: tuple = None) int[source]

Ensure a nested "Group::Leaf" layer exists in an offline rhino3dm.File3dm, creating any missing parents – the Python-side mirror of the plugin’s StmDocument.EnsureLayer (same “walk the ::-separated path, create what’s missing” logic, since standalone rhino3dm has no FindByFullPath, only parent-scoped FindName). Returns the leaf layer’s index.

color defaults to DEFAULT_COLORS[full_path] if not given and the path is one of the constants above.

civilpy.structural.rhino_ps_i_bim module

BrIM emit layer for prestressed I-beam bridges (PSID-1-13).

The prestressed-I counterpart of rhino_box_bim: the executed design line from ps_i_beam_line_checks() drives the drawn geometry — the strand rows are the designed pattern, not a free parameter.

  • each beam as one true-profile I-prism (ps_i_beam_profile() — tapered flanges, no fillet radii), tagged bim.type = ps_i_beam carrying the 515 member pay item (strands, embedded sole plates, and anchorage steel are included in the member per sheet 10);

  • a schematic strand-row polyline per occupied row of the designed pattern, with the row’s end-debonded count in its tags;

  • 2 in design haunches over each top flange (BDM 309.3.5) and the CIP deck slab (511 superstructure concrete);

  • cast-in-place intermediate diaphragms at the sheet 5 stations (midspan up to 80 ft, quarter points beyond), one 515 intermediate- diaphragm count per station-bay;

  • elastomeric bearing pads under each beam end (516 item);

  • the gdr.* girder centerline per beam (gdr.family = ps_i) so the line-girder tooling reads the model back.

Drawn by the same draw_bim_emit.py driver and read back by the same read_bim_quantities() as the steel and box slices. Coordinates are feet: X spans 0..span, Y across the beams (beam 1 at y = overhang), Z = 0 at the beam soffit.

Like the box slice: skew is not yet supported, the deck is drawn flat (no crown), and no railing is emitted here — place one with the barrier tooling. End/pier diaphragms belong to the substructure phase.

civilpy.structural.rhino_ps_i_bim.DIAPHRAGM_THICKNESS_IN = 10.0

Display thickness for the CIP intermediate diaphragms (the project plans size them; sheet 9 details the connection) — cosmetic only.

class civilpy.structural.rhino_ps_i_bim.PSIBridgeInput(section: str, span_ft: float, n_beams: int, spacing_ft: float, overhang_ft: float = 2.5, deck_t_in: float = 8.5, haunch_in: float = 2.0, skew_deg: float = 0.0, fci_ksi: float = 4.0, fc_ksi: float = 5.5, barrier_klf: float = 0.0, n_strands: int | None = None)[source]

Bases: object

One prestressed I-beam bridge on the PSID-1-13 standard.

section is a PSID-1-13 section name ("WF48-49", "AASHTO Type 3", …). Beam 1’s centerline sits at y = overhang_ft; the deck edges run overhang_ft beyond each exterior beam centerline.

barrier_klf: float = 0.0
deck_t_in: float = 8.5
fc_ksi: float = 5.5
fci_ksi: float = 4.0
haunch_in: float = 2.0
n_beams: int
n_strands: int | None = None
overhang_ft: float = 2.5
section: str
skew_deg: float = 0.0
spacing_ft: float
span_ft: float
civilpy.structural.rhino_ps_i_bim.ps_i_bridge_emit(inp: PSIBridgeInput, *, checks: PSIBeamLineChecks | None = None, concrete_pcf: float = 150.0) BridgeEmit[source]

Build the tagged BrIM geometry for one prestressed I-beam bridge.

checks short-circuits the design step with an already-executed ps_i_beam_line_checks() result (it must match the input’s section/span/spacing); otherwise the line is designed here. Raises ValueError for a skewed layout (not yet supported) or when no passing pattern exists.

civilpy.structural.rhino_slab module

Rhino emit layer for ODOT single span slab bridges (SB-1-24).

Turns the pure layout in civilpy.structural.odot.slab_bridge into tagged, transport-neutral geometry records (EmitObject), then bakes them through whichever backend the caller has:

  • write_slab_bridge() – offline rhino3dm.File3dm, no Rhino needed.

  • The SB-1-24 Grasshopper component – builds Rhino.Geometry from the same records inside Rhino 8.

  • Any live-document driver (an MCP agent, a plugin command) – consumes the same records.

The point of the neutral record is that SB-1-24’s engineering content is described exactly once. A backend only decides how to draw a closed polyline and how to stamp a user-string; it never decides where a bar goes.

Coordinates in the records are feet, matching the hub’s Units and the gdr./stm. tag contracts. Backends scale to the document’s unit system.

Winding contract

EmitObject.points for a solid is a closed plan outline wound counter-clockwise viewed from +Z, so its plane normal is +Z and extrude_ft (negative) drives the solid down from the top of slab at z = 0. This is load-bearing, not cosmetic: layout_slab_bridge returns its outline wound clockwise, whose plane normal is -Z, and feeding that straight into Extrusion.Create(crv, -thickness, True) extrudes the slab upward into z > 0 while the bar mats stay at z < 0 – the “rebar appears below the concrete deck” defect noted in the component’s own TODO block. _ccw() normalizes the winding here so no backend can reintroduce it.

User-text contract (slab. namespace)

Mirrors gdr./stm.: a slab.kind discriminator marks structural objects, and geometry without it is cosmetic display that readers ignore by contract. See read_slab_bridge() for the round trip.

tag

meaning

slab.kind

slab | rebar (absent => display only)

slab.scd

source standard drawing, SB-1-24

slab.id

stable GUID, survives a Rhino -> hub -> Rhino trip

slab.mark

bar mark A / B / M / N

slab.size

bar size (imperial eighths, e.g. 7 => #7)

slab.diameter_in

nominal bar diameter, in (ASTM A615)

slab.area_in2

nominal bar area, in^2 (ASTM A615)

slab.mat

top | bottom reinforcing mat

slab.length_ft

bar length, ft (quantity take-off)

slab.epoxy

true – SB-1-24 note requires epoxy coating

slab.thickness_in

slab thickness, in

slab.fc_psi

concrete strength, psi

slab.pay_item

ODOT CMS pay item, only when supplied by caller

Warning

slab.pay_item is never guessed. ODOT CMS item numbers are contractual and civilpy does not ship a table of them; pass pay_items= to stamp them, or the tag is simply omitted.

civilpy.structural.rhino_slab.BAR_AREA_IN2: dict[int, float] = {3: 0.11, 4: 0.2, 5: 0.31, 6: 0.44, 7: 0.6, 8: 0.79, 9: 1.0, 10: 1.27, 11: 1.56}

Nominal bar area (in^2) by imperial bar size, ASTM A615.

civilpy.structural.rhino_slab.BAR_DIAMETER_IN: dict[int, float] = {3: 0.375, 4: 0.5, 5: 0.625, 6: 0.75, 7: 0.875, 8: 1.0, 9: 1.128, 10: 1.27, 11: 1.41}

Nominal bar diameter (in) by imperial bar size, ASTM A615.

civilpy.structural.rhino_slab.BAR_MAT: dict[str, str] = {'A': 'bottom', 'B': 'top', 'M': 'bottom', 'N': 'top'}

Which mat each SB-1-24 longitudinal bar mark belongs to (sheet 1).

class civilpy.structural.rhino_slab.EmitObject(kind: str, layer: str, points: tuple[tuple[float, float, float], ...], tags: dict[str, str] = <factory>, extrude_ft: float | None = None, closed: bool = False)[source]

Bases: object

One drawable object, independent of any Rhino API.

kind is "solid" (extrude points along extrude_ft) or "curve" (polyline through points). tags are the user strings to stamp verbatim, already prefixed with TAG.

KINDS = ('solid', 'curve', 'point')

"solid" | "curve" | "point"

closed: bool = False
extrude_ft: float | None = None
kind: str
layer: str
points: tuple[tuple[float, float, float], ...]
tags: dict[str, str]
class civilpy.structural.rhino_slab.SlabEmit(inputs: SlabBridgeInput, layout: SlabBridgeLayout, objects: tuple[EmitObject, ...], doc_tags: dict[str, str])[source]

Bases: object

Everything a backend needs to draw one SB-1-24 slab bridge.

doc_tags: dict[str, str]
inputs: SlabBridgeInput
layout: SlabBridgeLayout
objects: tuple[EmitObject, ...]
of_kind(kind: str) tuple[EmitObject, ...][source]

The emitted objects whose slab.kind matches ("" = display).

civilpy.structural.rhino_slab.push_slab_to_midas(inp: SlabBridgeInput, *, level: str = 'L1', midas=None, **client_kwargs) dict[source]

Push an SB-1-24 slab to a live MIDAS Civil NX session and report per table.

Builds slab_midas_payloads(), then PUTs each table in send order, continuing past individual table errors (same report shape as push_midas()). Pass a MidasCivil, or let it build one from ~/secrets.json.

civilpy.structural.rhino_slab.read_slab_bridge(path) dict[source]

Read a slab.-tagged .3dm back into {"doc", "slab", "rebar"}.

doc is the document-level tag dict; slab and rebar are lists of {"points", "tags"} with numeric tag values cast to float and points converted to feet. Untagged (cosmetic) geometry is skipped by contract, the same rule read_splice_results() follows.

civilpy.structural.rhino_slab.rebar_quantities(emit: SlabEmit) dict[str, dict][source]

Bar count, total length (ft), and weight (lb) per mark – the take-off the slab.diameter_in / slab.length_ft tags exist to support.

Steel unit weight is 490 lb/ft^3, so a bar’s weight is area_in2 / 144 * 490 * length_ft.

civilpy.structural.rhino_slab.slab_emit(inp: SlabBridgeInput, *, pay_items: dict[str, str] | None = None) SlabEmit[source]

Build the tagged, transport-neutral geometry for one slab bridge.

pay_items optionally maps "concrete" and "rebar" to ODOT CMS item strings; absent keys leave slab.pay_item unstamped rather than guessed.

Raises whatever layout_slab_bridge() raises for an untabulated span, bad edge condition, or excessive skew.

civilpy.structural.rhino_slab.slab_input_from_doc_tags(doc_tags: dict) SlabBridgeInput[source]

Rebuild a SlabBridgeInput from the slab. document tags read off a .3dm (or authored by the GH component).

This is the Rhino -> analysis handoff: the user-text attributes carry enough to regenerate the design, so an engineer who traced/authored a slab in Rhino can drive MIDAS from those tags alone. Values may arrive as floats (from read_slab_bridge()) or strings (raw user text); both work.

civilpy.structural.rhino_slab.slab_midas_payloads(inp: SlabBridgeInput, *, level: str = 'L1') dict[source]

MIDAS /db/* payloads for an SB-1-24 slab, with concrete material and a solid-rectangle strip section.

Starts from the generic hub serialization (midas_payloads()) for NODE/ELEM/CONS/BEAM, then overrides the steel-centric MATL/SECT the hub emits by default: the material becomes concrete with Ec from f'c (concrete_material_block()) and the section becomes the equivalent-strip rectangle – 1 ft wide (the L1 loads are already scaled to a 1 ft strip by the 1/E distribution factor) by the slab thickness (solid_rect_section_block()).

civilpy.structural.rhino_slab.write_slab_bridge(out_path, inp: SlabBridgeInput, *, pay_items: dict[str, str] | None = None, unit_system=None) SlabEmit[source]

Bake one SB-1-24 slab bridge into a new .3dm and return its SlabEmit.

Geometry is written in unit_system (default feet, so points round-trip 1:1 through read_slab_bridge()). Objects land on the shared Deck::Bridge Deck / Deck::Rebar layers from civilpy.structural.rhino_layersnot a per-drawing Deck::SB-1-24 layer – so a file civilpy writes and the C# plugin reads resolve to the same nested layer.

civilpy.structural.rhino_stm module

Rhino 3D <-> strut-and-tie interchange.

Bridges a Rhino .3dm model and StrutAndTieModel using the convention documented in docs/Rhino Design Philosophy.md:

Geometry carries what is spatial; tags carry what is scalar.

  • Members are curves tagged stm.kind=member (an untagged curve falls back to a member, so quick sketches still import).

  • Supports are block instances (STM_Pin / STM_Roller_V / STM_Roller_H / STM_Fixed) at a node; the block name sets the common fixity and stm.fix_* user text overrides it. Full 6-DOF is recorded for future frame/Midas use; the 2D STM solver consumes only the in-plane translations.

  • Loads are STM_Load arrow blocks (or plain tagged lines). The arrow’s orientation is the force direction; stm.kips carries the magnitude.

Nodes are not authored explicitly – they are derived by snapping member endpoints together within a tolerance and auto-labeling A, B, C… ordered bottom-to-top then left-to-right (so labels read like an elevation).

The model is drawn flat in a world plane (the FHWA examples use XZ, a front elevation). plane="auto" detects it; X/Z map to the analysis x/y with gravity along -Z.

rhino3dm is an optional dependency (pip install civilpy[rhino]); it is imported lazily so the rest of civilpy.structural works without it.

civilpy.structural.rhino_stm.build_template(path, *, version=7)[source]

Write a starter .3dm (units feet) holding the STM layers, ready to draw into.

The symbol block definitions (pin / rollers / fixed / load arrow) are intentionally not baked in here: rhino3dm 8.x’s InstanceDefinitions.Add corrupts memory probabilistically, so block creation is left to the in-Rhino authoring tools (src/civilpy/structural/rhino_scripts/ stm_authoring.py), where RhinoCommon creates them reliably on first use. The reader (model_from_3dm()) understands those Rhino-authored block symbols as well as the tagged points/lines this package writes.

civilpy.structural.rhino_stm.model_from_3dm(path, *, plane='auto', tol=0.05, as_model=False)[source]

Read a tagged Rhino .3dm file into a structural model.

By default returns a 2D StrutAndTieModel (backward compatible) – now produced as a thin projection of the canonical hub rather than via a second parser. Pass as_model=True to get the richer 3D / 6-DOF StructuralModel hub instead (equivalent to read_structural_model()).

Parameters:
  • path (str) – Path to the .3dm file.

  • plane ({"auto", "XY", "XZ", "YZ"}) – World plane the model is drawn in. "auto" infers it from the geometry (the near-constant axis is the normal).

  • tol (float) – Snapping tolerance (model units) for collapsing coincident endpoints into nodes and matching supports/loads to them.

  • as_model (bool) – When True return the full StructuralModel hub instead of the projected 2D StrutAndTieModel.

civilpy.structural.rhino_stm.model_to_3dm(model, path, *, plane='XZ', arrow_len=1.0, version=7)[source]

Author a StrutAndTieModel as a tagged .3dm file.

Members are tagged lines, supports are tagged points (stm.support plus the stm.fix_* flags), and loads are tagged arrow lines drawn from the node in the force direction with the magnitude in stm.kips. This keeps the writer to the rock-solid AddLine/AddPoint paths; the richer symbol blocks live in the template (build_template()) and are inserted by the in-Rhino authoring tools, and the reader understands both representations.

arrow_len is the drawn length of load arrows in model units (purely visual; magnitude comes from the tag).

civilpy.structural.rhino_stm.problem_from_3dm(path, *, plane='auto', nu=0.2)[source]

Read a tagged Rhino .3dm authored with the region workflow into a DRegionProblem.

The file holds one closed stm.kind=region curve (the concrete D-region) carrying stm.thickness (ft) and stm.fc (ksi), optionally stm.E/stm.nu/stm.vol_frac; optional stm.kind=void / stm.kind=solid inner curves; and the same supports and loads as the drawn-truss workflow, optionally with an stm.bearing (ft) width. See docs/Rhino Design Philosophy.md (§”Two front ends”).

civilpy.structural.rhino_stm.read_structural_model(path, *, plane='auto', tol=0.05)[source]

Read a tagged Rhino .3dm into the canonical StructuralModel hub.

Where model_from_3dm() returns the lossy 2D StrutAndTieModel, this preserves everything the MIDAS and IFC adapters need: full 3D node coordinates, the complete 6-DOF restraint (stm.fix_* – so fix_z/fix_rx/fix_ry/fix_rz survive, not just the in-plane pair the truss solver uses), the support preset, stable object ids, and the full 3D load vectors. This is stage S2 of the package-coherence track in docs/Rhino Design Philosophy.md.

Parameters:
  • path (str) – Path to the .3dm file.

  • plane ({"auto", "XY", "XZ", "YZ"}) – World plane the model is drawn in, used only to cluster coincident endpoints into nodes; "auto" infers it from the geometry. The hub keeps the genuine 3D coordinates regardless.

  • tol (float) – Snapping tolerance (model units) for collapsing coincident endpoints into nodes and matching supports/loads to them.

civilpy.structural.rhino_stm.results_to_3dm(model, path, *, plane='XZ', version=7)[source]

Write a solved model for review in Rhino: ties red, struts blue, with a text dot of the member force at each midpoint and reactions at supports.

civilpy.structural.section_properties module

Built-up plate cross-section property calculator.

CrossSection accumulates rectangular plates (bottom to top) and computes area, centroid, moments of inertia, and section moduli for the composite shape — useful for plate girders and other built-up members.

class civilpy.structural.section_properties.CrossSection(label, dimensions=None, shape=None, y=None, axis='strong')[source]

Bases: object

A class for defining a built-up section by repeatedly adding plate dimensions.

This class allows users to define a built-up cross-section by calling the instance and passing in the dimensions of each plate making up the section. The plates are assumed to be entered in order from the bottom to the top, and all shapes are assumed to be rectangles that do not overlap.

labels

A list of labels identifying each plate in the cross-section.

Type:

list of str

dimensions

A list of tuples (width, height) for each plate.

Type:

list

ys

The centroids of each plate along the y-axis.

Type:

list of float

areas

The areas of each plate.

Type:

list of float

moments

The first moments of area for each plate.

Type:

list of float

I_ys

The second moments of area of each plate about its own centroid.

Type:

list of float

I_gs

The second moments of area of each plate about the global centroid.

Type:

list of float

area

The total area of the cross-section.

Type:

float

moment

The first moment of area of the cross-section.

Type:

float

I_y

The total second moment of area about the local centroid.

Type:

float

I_g

The total second moment of area about the global centroid.

Type:

float

I_n

The net inertia value, adjusted for global centroid.

Type:

int

n

The location of the neutral axis.

Type:

float

Parameters:
  • label (str) – A label to identify the plate in the cross-section (e.g., “A”, “B”).

  • dimensions (tuple) – A tuple representing the plate dimensions (width, height).

  • shape (str, optional) – The shape of the plate (default is None for rectangles).

Returns:

An instance of the CrossSection object.

Return type:

CrossSection

__init__(label, dimensions=None, shape=None, y=None, axis='strong')[source]

Initialise a built-up cross-section with its first plate or shape.

Parameters:
  • label (str) – Identifier for this plate (e.g. "bottom flange").

  • dimensions (tuple[float, float], optional) – (width, height) of a rectangular plate in consistent units. Required when shape is None.

  • shape (W, optional) – A steel wide-flange shape object from civilpy.structural.steel. When supplied, dimensions is derived from the shape’s flange width and depth.

  • y (float, optional) – Distance from the datum to the centroid of this plate. Defaults to half the plate height (i.e. the plate sits with its bottom at y = 0).

  • axis (str) – 'strong' (default) to use I_x and depth, or 'weak' to use I_y and flange width for rolled shapes.

append_value(dimensions=None, y=None, shape=None, axis=None)[source]

Add a plate or rolled shape to the cross-section and update properties.

Four dispatch paths are supported:

  1. Rectangular plate stacked on top (y=None, shape=None): plate sits directly above the current top of section.

  2. Rolled shape at explicit y (shape provided, y provided): uses I_x or I_y depending on axis.

  3. Rolled shape stacked on top (shape provided, y=None): depth/flange width derived from shape; stacked on current top.

  4. Rectangular plate at explicit y (shape=None, y provided): plate placed with centroid at y.

Parameters:
  • dimensions (tuple[float, float], optional) – (width, height) of a rectangular plate.

  • y (float, optional) – Centroid position from datum.

  • shape (W, optional) – Rolled steel shape object.

  • axis (str, optional) – 'strong' or 'weak' for rolled shapes.

Note

_calc_gen_properties() is called automatically after each plate is added.

check_negative_y_values()[source]

Check whether any plate centroid lies below the datum (y < 0).

Returns:

True if at least one y-coordinate key in self.plate_dims is negative, False otherwise.

Return type:

bool

civilpy.structural.section_properties.get_bar_section_properties(d)[source]

Return (I, S, r) for a solid circular bar about its centroidal axis.

Parameters:

d – Diameter of the bar.

Returns:

(I, S, r) — moment of inertia, section modulus, radius of gyration.

Return type:

tuple

civilpy.structural.section_properties.get_hollow_oval_section_properties(a, b, c, d)[source]

Return (I, S, r) for a hollow ellipse (elliptical tube) about its centroidal axis.

Parameters:
  • a – Outer semi-axis height.

  • b – Outer semi-axis width.

  • c – Inner semi-axis height.

  • d – Inner semi-axis width.

Returns:

(I, S, r) — moment of inertia, section modulus, radius of gyration.

Return type:

tuple

civilpy.structural.section_properties.get_oval_section_properties(b, a)[source]

Return (I, S, r) for a solid ellipse about its centroidal axis.

Parameters:
  • b – Semi-axis width (horizontal half-width).

  • a – Semi-axis height (vertical half-height).

Returns:

(I, S, r) — moment of inertia, section modulus, radius of gyration.

Return type:

tuple

civilpy.structural.section_properties.get_pipe_section_properties(D, d)[source]

Return (I, S, r) for a hollow circular pipe about its centroidal axis.

Parameters:
  • D – Outer diameter.

  • d – Inner diameter.

Returns:

(I, S, r) — moment of inertia, section modulus, radius of gyration.

Return type:

tuple

civilpy.structural.section_properties.get_rectangular_section_properties(b, d)[source]

Return (I, S, r) for a rectangle about its neutral axis (centroid).

Parameters:
  • b – Width of the rectangle.

  • d – Height of the rectangle.

Returns:

(I, S, r) — moment of inertia, section modulus, radius of gyration.

Return type:

tuple

civilpy.structural.section_properties.get_rectangular_section_properties_baseline(b, d)[source]

Return (I, S, r) for a rectangle about its baseline (bottom edge).

Parameters:
  • b – Width of the rectangle.

  • d – Height of the rectangle.

Returns:

(I, S, r) — moment of inertia, section modulus, radius of gyration.

Return type:

tuple

civilpy.structural.section_properties.get_triangular_section_properties(b, d)[source]

Return (I, S, r) for a triangle about its neutral axis.

Parameters:
  • b – Base width of the triangle.

  • d – Height of the triangle.

Returns:

(I, S, r) — moment of inertia, section modulus, radius of gyration.

Return type:

tuple

civilpy.structural.section_properties.get_triangular_section_properties_baseline(b, d)[source]

Return (I, S, r) for a triangle about its baseline.

Parameters:
  • b – Base width of the triangle.

  • d – Height of the triangle.

Returns:

(I, S, r) — moment of inertia, section modulus, radius of gyration.

Return type:

tuple

civilpy.structural.shear_flow module

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
class civilpy.structural.shear_flow.Plate(width, height, y_bottom)[source]

Bases: Plate

A rectangle in the cross-section: width x height with its bottom edge at y_bottom (in).

class civilpy.structural.shear_flow.ShearSection(plates: list[Plate])[source]

Bases: object

Stacked-rectangle section for VQ/(I*b) shear stress.

plot(v: float, ax=None, n: int = 400)[source]

Plot tau(y) beside the section outline; flange/web jumps from the width change show as the textbook discontinuities.

q_at(y: float) float[source]

First moment of the area above y about the neutral axis.

tau(v: float, y: float) float[source]

Shear stress at elevation y for shear force v (ksi for kips and inches).

width_at(y: float) float[source]

Total section width crossing elevation y.

civilpy.structural.steel module

AISC steel-shape database lookup and section classes.

Loads the AISC shapes database (current and historic editions, bundled as CSV under structural/res) and exposes SteelSection-style classes whose attributes are Pint quantities (SteelSection("W36X150").I_x, .S_x, .weight, …), plus bolt-weight reference data for detailing.

class civilpy.structural.steel.BoltMaterial(designation: str, f_y: float, f_u: float, f_v: float = 48)[source]

Bases: object

Structural bolt material specification.

Parameters:
  • designation (str) – ASTM bolt designation, e.g. 'A325', 'A490', 'F3125GrA325'.

  • f_y (float) – Proof load / yield strength, ksi.

  • f_u (float) – Minimum tensile strength, ksi.

  • f_v (float) – Nominal shear strength (ASD allowable or LRFD nominal), ksi. Defaults to 48 (A325 threads excluded from shear plane).

Example

>>> b = BoltMaterial('A325', f_y=92, f_u=120, f_v=48)
>>> b.f_u
120 kip/inch²
class civilpy.structural.steel.C(label)[source]

Bases: SteelSection

Class to provide more specific attributes and functions related to designing with steel C s. Splitting values into multiple classes allows dropping of empty values in the database.

>>> t = C("C15X50")
>>> t.weight
50.0 pound/foot
class civilpy.structural.steel.HP(label)[source]

Bases: SteelSection

Class to provide more specific attributes and functions related to designing with steel M s. Splitting values into multiple classes allows dropping of empty values in the database.

>>> t = HP("HP18X204")
>>> t.weight
204.0 pound/foot
class civilpy.structural.steel.HSS(label)[source]

Bases: SteelSection

Class to provide more specific attributes and functions related to designing with steel HSS sections. Splitting values into multiple classes allows dropping of empty values in the database.

>>> t = HSS("HSS20X20X.500")
>>> t.weight
130.52 pound/foot
class civilpy.structural.steel.HistoricSteelSection(label, designation=None)[source]

Bases: object

Main Steel Section Class, the goal is to make the attributes of various steel sections easily accessible in various python scripts.

__init__(label, designation=None)[source]

Initialize a historic steel section from the AISC historic shapes database.

Parameters:
  • label (str) – Shape label, e.g. "18WF96" or "WF36X150". Case and spaces are normalized automatically.

  • designation (str, optional) – AISC historic edition designation used to disambiguate shapes that were published under multiple editions. Example: '18WF_B18b'. If None, returns the first match.

clean_user_input(user_input)[source]

Eliminates value not found errors by removing lower case letters and spaces

>>> t = SteelSection("W 44X335")
>>> print(t.clean_user_input('W 44X335'))
W44X335
>>> print(t.clean_user_input('w40x294'))
W40X294
>>> t.id
'W40X294'
Returns:

cleaned input

get_historical_shape(designation=None)[source]

Searches AISC Steel database table for member label passed to it, returns values from table if it finds a match, or prints an error if it doesn’t

>>> t = SteelSection("W36X150")
>>> t.aisc_value['Type'].values[0]
'W'
>>> t = SteelSection("2L8X4X5/8LLBB")
>>> t.aisc_value['W'].values[0]
48.4
Parameters:

self

Returns:

dataframe of raw values from AISC Shape Table

class civilpy.structural.steel.L(label)[source]

Bases: SteelSection

Class to provide more specific attributes and functions related to designing with steel L s. Splitting values into multiple classes allows dropping of empty vain the database.

>>> t = L("L12x12x1-3/8")
>>> t.weight
105.0 pound/foot
class civilpy.structural.steel.M(label)[source]

Bases: SteelSection

Class to provide more specific attributes and functions related to designing with steel M s. Splitting values into multiple classes allows dropping of empty values in the database.

>>> t = M("M10X9")
>>> t.weight
9.0 pound/foot
class civilpy.structural.steel.MC(label)[source]

Bases: C

Class to provide more specific attributes and functions related to designing with steel MC s. Splitting values into multiple classes allows dropping of empty values in the database.

>>> t = MC("MC18X58")
>>> t.weight
58.0 pound/foot
class civilpy.structural.steel.MT(label)[source]

Bases: SteelSection

Class to provide more specific attributes and functions related to designing with steel WT s. Splitting values into multiple classes allows dropping of empty values in the database.

>>> t = MT("MT5x4")
>>> t.weight
4.0 pound/foot
class civilpy.structural.steel.Pipe(label)[source]

Bases: SteelSection

Class to provide more specific attributes and functions related to designing with steel Pipe Sections. Splitting values into multiple classes allows dropping of empty values in the database.

>>> t = Pipe("Pipe10SCH140")
>>> t.weight
104.0 pound/foot
class civilpy.structural.steel.Rebar(bar_number: int, grade: int = 60)[source]

Bases: object

Standard deformed reinforcing bar per ASTM A615 / A706.

Provides nominal diameter, cross-sectional area, and unit weight for standard US rebar sizes #2 through #18. All dimensional properties carry Pint units.

Parameters:
  • bar_number (int) – Standard bar designator (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 14, or 18).

  • grade (int) – Yield strength grade in ksi. Common values are 40, 60, 75, or 80 per ASTM A615; 60 or 80 per ASTM A706. Defaults to 60.

Raises:

ValueError – If bar_number is not a standard ASTM size.

Example

>>> bar = Rebar(5)
>>> float(bar.area.magnitude)
0.31
>>> float(bar.weight.magnitude)
1.043
>>> bar.diameter
0.625 inch
>>> bar = Rebar(8, grade=60)
>>> bar.bar_number
8
class civilpy.structural.steel.S(label)[source]

Bases: SteelSection

Class to provide more specific attributes and functions related to designing with steel M s. Splitting values into multiple classes allows dropping of empty values in the database.

>>> t = S("S24X121")
>>> t.weight
121.0 pound/foot
class civilpy.structural.steel.ST(label)[source]

Bases: WT

Class to provide more specific attributes and functions related to designing with steel ST s. Splitting values into multiple classes allows dropping of empty values in the database.

>>> t = ST("ST10x48")
>>> t.weight
48.0 pound/foot
class civilpy.structural.steel.SteelMaterial(designation: str, f_y: float, f_u: float, e_s: float = 29000)[source]

Bases: object

Structural steel material specification.

Stores yield strength, tensile strength, and modulus of elasticity for common ASTM structural steel grades. Properties carry Pint units.

Parameters:
  • designation (str) – ASTM material designation, e.g. 'A36', 'A572Gr50', 'A992'.

  • f_y (float) – Minimum yield strength, ksi.

  • f_u (float) – Minimum tensile strength, ksi.

  • e_s (float) – Modulus of elasticity, ksi. Defaults to 29000.

Example

>>> m = SteelMaterial('A36', f_y=36, f_u=58)
>>> m.f_y
36 kip/inch²
class civilpy.structural.steel.SteelSection(label)[source]

Bases: object

Main Steel Section Class, the goal is to make the attributes of various steel sections easily accessible in various python scripts.

__init__(label)[source]

Initialize a SteelSection from an AISC shape label.

Looks up the label in the AISC Steel Construction Manual shape table and populates standard section properties with Pint units attached.

Parameters:

label (str) – AISC standard nomenclature, e.g. "W36X150", "w40x294", "2L8X4X5/8LLBB". Case and spaces are normalized automatically.

Raises:

KeyError – If the label is not found in the AISC shape database.

Example

>>> s = SteelSection("W36X150")
>>> float(s.I_x.magnitude)
9040.0
clean_user_input(user_input)[source]

Eliminates value not found errors by removing lower case letters and spaces

>>> t = SteelSection("W 44X335")
>>> print(t.clean_user_input('W 44X335'))
W44X335
>>> print(t.clean_user_input('w40x294'))
W40X294
>>> t.id
'W40X294'
Returns:

cleaned input

get_shape()[source]

Searches AISC Steel database table for member label passed to it, returns values from table if it finds a match, or prints an error if it doesn’t

>>> t = SteelSection("W36X150")
>>> t.aisc_value['Type'].values[0]
'W'
>>> t = SteelSection("2L8X4X5/8LLBB")
>>> t.aisc_value['W'].values[0]
48.4
Parameters:

self

Returns:

dataframe of raw values from AISC Shape Table

class civilpy.structural.steel.TwoL(label)[source]

Bases: SteelSection

Class to provide more specific attributes and functions related to designing with steel 2L s. Splitting values into multiple classes allows dropping of empty values in the database.

>>> t = TwoL("2L10X10X1-1/4")
>>> t.weight
48.0 pound/foot
class civilpy.structural.steel.W(label)[source]

Bases: SteelSection

Class to provide more specific attributes and functions related to designing with steel W s. Splitting values into multiple classes allows dropping of empty values in the database.

>>> t = W("W36X150")
>>> t.weight
150.0 pound/foot
class civilpy.structural.steel.WF(label, designation=None)[source]

Bases: HistoricSteelSection

Historic wide-flange section (pre-AISC W-shape standardization).

Used for evaluating existing bridges and structures built with pre-1970 steel sections. Extends HistoricSteelSection with dimensional attributes (depth, flange width, web thickness, etc.) from the AISC historic shapes table.

Example

>>> t = WF("18WF96", '18WF_B18b')
>>> t.weight
96.0 force_pound/foot
__init__(label, designation=None)[source]

Initialize a historic WF section with full dimensional properties.

Parameters:
  • label (str) – Historic shape label, e.g. "18WF96".

  • designation (str, optional) – AISC edition designation to disambiguate shapes with the same label across multiple editions. Example: '18WF_B18b'.

class civilpy.structural.steel.WT(label)[source]

Bases: SteelSection

Class to provide more specific attributes and functions related to designing with steel WT s. Splitting values into multiple classes allows dropping of empty values in the database.

>>> t = WT("WT22x145")
>>> t.weight
145.0 pound/foot
civilpy.structural.steel.conv_frac_str(fraction_string: str) float[source]
civilpy.structural.steel.get_bolt_weights(length: float, diameter: float, no_of_washers: int) float[source]

Function to get the bolt weights from the A325_bolt_weights dictionary and calculate the weight per bolt if it’s not contained in the table.

Original table from: https://www.portlandbolt.com/print/?table=7587

The A325_bolt_weights dictionary reports the weight indexed by bolt length and diameter, and reports the value in lbs/100 bolts. This function simplifies that by reducing the value to that of a single bolt.

If bolt weights are not contained in the dictionary, the function will utilize the following formula to calculate the weight per bolt, if no_of_washers is greater than 0, it will include the weight of the washers in the calculation with the following values:

Per inch Adder (dia):

0.5: 5.5 0.625: 8.6 0.75: 12.4 0.875: 16.9 1.0: 22.1 1.125: 28 1.25: 34.4 1.375: 42.5 1.5: 49.7

F436 Round Washers

0.5: 2.1 0.625: 3.6 0.75: 4.8 0.875: 7 1.0: 9.4 1.125: 11.3 1.25: 13.8 1.375: 16.8 1.5: 20

Weight in table includes the weight of nuts.

Parameters:
Returns:

Weight

Return type:

float - the weight of the individual bolt

civilpy.structural.structural_model module

The canonical structural model: a pure, IFC-aligned interchange hub.

It is the in-memory analogue of a tagged .3dm / an IFC file: a discretized structure (nodes, elements, restraints, loads, results) carrying 3D geometry, full 6-DOF restraints, multiple load cases, and stable ids – everything the lossy 2D StrutAndTieModel throws away.

It holds no solver. By design the package’s three analysis paradigms (pin-jointed method of joints, continuous-beam/frame relaxation, external FEM) solve different math and stay separate; what they share is this data. The model is the hub; rhino_stm, the MIDAS serializer, .3dm write-back, the future IFC adapter, and the capacity calculators are all spokes off it (composition, not inheritance).

The vocabulary mirrors the IFC 4.3 IfcStructuralAnalysisDomain so a future from_ifc / to_ifc adapter is a direct mapping (see the entity table in the design doc):

hub type

IFC 4.3 entity

Node

IfcStructuralPointConnection

Restraint

IfcBoundaryNodeCondition

Element

IfcStructuralCurveMember

Load

IfcStructuralLoadSingleForce (point action)

LoadCase

IfcStructuralLoadCase

Result

IfcStructuralResultGroup (reactions / forces)

StructuralModel

IfcStructuralAnalysisModel

Units are carried as labels (default kips / ft, matching the rest of civilpy.structural and the Rhino tag contract); actual unit conversion is an adapter concern (e.g. midas.convert_node_units already uses the civilpy.general.units pint registry), so the hub stays dependency-light.

Examples

>>> m = StructuralModel()
>>> a = m.add_node(0, 0, 0, label="A")
>>> b = m.add_node(27, 0, 0, label="B")
>>> _ = m.add_element(a.id, b.id)
>>> m.add_restraint(a.id, preset="pin").to_constraint_string()
'1100000'
>>> m.add_restraint(b.id, preset="roller-v").to_2d()
(False, True)
>>> _ = m.add_load(b.id, fy=-600)
>>> len(m.nodes), len(m.elements), len(m.loads)
(2, 1, 1)
>>> m.check()           # no structural errors in this little model
[]
class civilpy.structural.structural_model.BeamLoad(element_id: str, w_start: float, w_end: float | None = None, case: str = 'default', direction: Literal['GX', 'GY', 'GZ']='GZ', id: str = <factory>)[source]

Bases: object

A distributed load on a beam element.

case: str = 'default'
direction: Literal['GX', 'GY', 'GZ'] = 'GZ'

"GX", "GY", or "GZ".

Type:

MIDAS load direction

element_id: str
id: str
w_end: float | None = None
w_start: float
civilpy.structural.structural_model.DOF_NAMES = ('x', 'y', 'z', 'rx', 'ry', 'rz')

Translational + rotational degrees of freedom, in the order used everywhere in the package – it matches the stm.fix_* tag suffixes and the first six characters of the MIDAS CONS constraint string DX DY DZ RX RY RZ RW.

class civilpy.structural.structural_model.Element(node_a: str, node_b: str, role: str = 'member', member_type: str = 'auto', midas_type: str = 'TRUSS', section: str | None = None, material: str | None = None, id: str = <factory>, nodes: list[str] = <factory>, metadata: dict = <factory>)[source]

Bases: object

A member between nodes – IFC IfcStructuralCurveMember or IfcStructuralSurfaceMember.

role is the typed-component taxonomy (e.g. member, top_chord, diagonal) that later drives capacity-check routing and the MIDAS element type. member_type is the stm.member hint: auto (default; the solver classifies by sign) or a forced tie / strut. midas_type is the export element type (TRUSS / BEAM / TENS / PLATE).

id: str
material: str | None = None
member_type: str = 'auto'
metadata: dict
midas_type: str = 'TRUSS'
node_a: str
node_b: str
nodes: list[str]
role: str = 'member'
section: str | None = None
class civilpy.structural.structural_model.Load(node_id: str, fx: float = 0.0, fy: float = 0.0, fz: float = 0.0, mx: float = 0.0, my: float = 0.0, mz: float = 0.0, case: str = 'default', id: str = <factory>)[source]

Bases: object

A nodal force/moment – IFC IfcStructuralLoadSingleForce applied via a point action. Full 3D force (and optional moment) vector; case names the LoadCase it belongs to (default "default").

case: str = 'default'
property force: tuple[float, float, float]
fx: float = 0.0
fy: float = 0.0
fz: float = 0.0
id: str
mx: float = 0.0
my: float = 0.0
mz: float = 0.0
node_id: str
class civilpy.structural.structural_model.LoadCase(name: str, description: str = '', factor: float = 1.0)[source]

Bases: object

A named load case – IFC IfcStructuralLoadCase.

description: str = ''
factor: float = 1.0
name: str
civilpy.structural.structural_model.MEMBER_TYPES = ('auto', 'tie', 'strut', 'beam')

auto is the never-written default; tie / strut are optional author overrides).

Type:

stm.member hint values (contract

class civilpy.structural.structural_model.Node(x: float, y: float, z: float = 0.0, label: str | None = None, id: str = <factory>)[source]

Bases: object

A connection point – IFC IfcStructuralPointConnection.

id is stable (defaults to a uuid); label is the human name an importer derives (A, B, C…). Coordinates are full 3D.

property coords: tuple[float, float, float]
id: str
label: str | None = None
x: float
y: float
z: float = 0.0
class civilpy.structural.structural_model.Restraint(node_id: str, fix_x: bool = False, fix_y: bool = False, fix_z: bool = False, fix_rx: bool = False, fix_ry: bool = False, fix_rz: bool = False, preset: str | None = None)[source]

Bases: object

A nodal boundary condition – IFC IfcBoundaryNodeCondition.

Full 6-DOF booleans (True = fixed). Built directly or from a SUPPORT_PRESETS name via from_preset().

fix_rx: bool = False
fix_ry: bool = False
fix_rz: bool = False
fix_x: bool = False
fix_y: bool = False
fix_z: bool = False
flags() tuple[bool, bool, bool, bool, bool, bool][source]

The six DOF flags in DOF_NAMES order.

classmethod from_preset(node_id: str, preset: str) Restraint[source]

Build a restraint from a friendly preset (pin/roller-v/ roller-h/fixed/custom).

node_id: str
preset: str | None = None
to_2d() tuple[bool, bool][source]

In-plane translational fixity (fix_x, fix_y) – what the 2D StrutAndTieModel / Truss solver consumes.

to_constraint_string() str[source]

The MIDAS CONS 7-char flag string DX DY DZ RX RY RZ RW ('1' = fixed). Warping (RW) is always 0 here.

>>> Restraint.from_preset("n", "pin").to_constraint_string()
'1100000'
>>> Restraint.from_preset("n", "fixed").to_constraint_string()
'1100010'
class civilpy.structural.structural_model.Result(case: str = 'default', element_forces: dict[str, float]=<factory>, reactions: dict[str, tuple[float, float, float, float, float, float]]=<factory>)[source]

Bases: object

Solved results for one load case – IFC IfcStructuralResultGroup.

element_forces maps element id -> axial force (tension positive, the package convention). reactions maps node id -> a 6-tuple in DOF_NAMES order.

case: str = 'default'
element_forces: dict[str, float]
reactions: dict[str, tuple[float, float, float, float, float, float]]

Bases: object

A rigid (master-slave) constraint – IFC IfcRelConnectsStructuralMember with an infinitely stiff connection.

master is the retained node; every node in slaves is tied to it in the DOFs flagged in dof (a 6-char DX DY DZ RX RY RZ string, '1' = constrained). This is the deck-to-girder tie of a refined grillage: a fully rigid link ("111111") makes the slab act compositely with the girder (studs/rebar into the deck); constraining vertical/transverse only and freeing longitudinal slip models a non-composite interface. Exports to the MIDAS /db/RIGD table.

dof: str = '111111'
id: str
master: str
slaves: list[str]
civilpy.structural.structural_model.SUPPORT_PRESETS = {'custom': {}, 'fixed': {'fix_rz': True, 'fix_x': True, 'fix_y': True}, 'pin': {'fix_x': True, 'fix_y': True}, 'roller-h': {'fix_x': True}, 'roller-v': {'fix_y': True}}

Friendly support presets, expressed in the 2D analysis-plane axes the stm.support contract uses (x = in-plane horizontal, y = in-plane vertical, z = out-of-plane). An adapter that knows the drawing plane maps these to global 3D DOF; the 2D STM solver consumes fix_x / fix_y directly. custom leaves everything free for explicit fix_* flags.

class civilpy.structural.structural_model.StructuralModel(nodes: dict[str, ~civilpy.structural.structural_model.Node]=<factory>, elements: dict[str, ~civilpy.structural.structural_model.Element]=<factory>, restraints: dict[str, ~civilpy.structural.structural_model.Restraint]=<factory>, loads: list[Load] = <factory>, beam_loads: list[BeamLoad] = <factory>, rigid_links: list[RigidLink] = <factory>, load_cases: dict[str, ~civilpy.structural.structural_model.LoadCase]=<factory>, results: dict[str, ~civilpy.structural.structural_model.Result]=<factory>, units: Units = <factory>, metadata: dict = <factory>)[source]

Bases: object

Canonical structural model – IFC IfcStructuralAnalysisModel.

A pure data container plus light add/lookup helpers. No analysis lives here; engines and adapters consume or produce it. Built incrementally:

>>> m = StructuralModel()
>>> n = m.add_node(0, 0, label="A")
>>> m.add_restraint(n.id, preset="pin").to_2d()
(True, True)
add_beam_load(element_id: str, w_start: float, w_end: float | None = None, *, case: str = 'default', direction: Literal['GX', 'GY', 'GZ'] = 'GZ', id: str | None = None) BeamLoad[source]
add_element(node_a: str, node_b: str, *, role: str = 'member', member_type: str = 'auto', midas_type: str = 'TRUSS', section: str | None = None, material: str | None = None, nodes: list[str] | None = None, id: str | None = None) Element[source]
add_load(node_id: str, *, fx: float = 0.0, fy: float = 0.0, fz: float = 0.0, mx: float = 0.0, my: float = 0.0, mz: float = 0.0, case: str = 'default', id: str | None = None) Load[source]
add_node(x: float, y: float, z: float = 0.0, *, label: str | None = None, id: str | None = None) Node[source]
add_restraint(node_id: str, *, preset: str | None = None, **flags: bool) Restraint[source]

Restrain a node, by preset and/or explicit fix_* flags. Explicit flags override the preset (matching the tag contract).

Tie slaves to master rigidly in the DOFs flagged in dof (6-char DX DY DZ RX RY RZ). Used for the deck-to-girder composite connection in a refined grillage (see RigidLink).

beam_loads: list[BeamLoad]
beam_loads_in_case(case: str = 'default') list[BeamLoad][source]
cases() list[str][source]

Load-case names actually used by loads (plus any registered).

check() list[str][source]

Return a list of structural-integrity problems (empty = clean).

Pure, geometry-level checks an importer can run before handing the model to a solver – they name the offending object, unlike the generic determinacy error the solver raises later. Flags: elements pointing at missing nodes, restraints/loads on missing nodes, zero-length elements, a model with no restraints, and nodes that no element touches.

elements: dict[str, Element]
load_cases: dict[str, LoadCase]
loads: list[Load]
loads_in_case(case: str = 'default') list[Load][source]
metadata: dict

free-form model-level tags a builder wants to hand downstream (traffic lane names and offsets, the dead-load breakdown, …) – the model-level counterpart of Element.metadata.

node_by_label(label: str) Node[source]
nodes: dict[str, Node]
restraints: dict[str, Restraint]
results: dict[str, Result]
units: Units
class civilpy.structural.structural_model.Units(force: str = 'kips', length: str = 'ft')[source]

Bases: object

Unit-system labels carried with a model (not a converter).

Defaults match the package convention and the Rhino tag contract (kips / feet). Conversion between systems is an adapter responsibility – keep the hub free of a numeric-units dependency.

force: str = 'kips'
length: str = 'ft'

civilpy.structural.strut_and_tie module

Strut-and-tie modeling: build a truss idealization of a D-region, solve member forces by the method of joints, and draw the classic STM diagram (struts dashed, ties solid, forces labeled).

Conventions match civilpy.structural.beam_bending: kips and feet (or any consistent unit pair), plot methods take an optional ax and return the figure for Jupyter use. Tension is positive, so positive member forces are ties and negative are struts.

The AASHTO LRFD capacity side (tie strength 5.8.2.4, node crushing 5.8.2.5, crack-control reinforcement 5.8.2.6) lives in civilpy.structural.aashto.lrfd.stm and takes the solved forces directly.

Examples

Deep beam idealized as a triangular truss — supports 8 ft apart, load at midspan top, 4 ft deep:

>>> stm = StrutAndTieModel()
>>> stm.add_node("A", 0, 0)
>>> stm.add_node("B", 8, 0)
>>> stm.add_node("C", 4, 4)
>>> stm.add_member("A", "C")
>>> stm.add_member("B", "C")
>>> stm.add_member("A", "B")
>>> stm.add_support("A", fix_x=True, fix_y=True)
>>> stm.add_support("B", fix_y=True)
>>> stm.add_load("C", fy=-100)
>>> forces = stm.solve()
>>> round(forces[("A", "B")], 2)  # bottom chord is a tie (+)
50.0
>>> round(forces[("A", "C")], 2)  # diagonals are struts (-)
-70.71
class civilpy.structural.strut_and_tie.StrutAndTieModel(E: float = 1.0)[source]

Bases: object

A pin-jointed truss idealization of a disturbed region.

add_load(node: str, fx: float = 0.0, fy: float = 0.0)[source]

Applied joint load (kips); fy negative = downward.

add_member(node_a: str, node_b: str, area: float = 1.0, member_type: str = 'auto')[source]

Add a truss member.

area (cross-sectional area) only affects the indeterminate direct-stiffness solver — it sets the relative member stiffness that picks the load path; the determinate method of joints ignores it. member_type is the stm.member hint: auto (classified by the solved sign) or a forced tie / strut.

add_node(label: str, x: float, y: float)[source]
add_support(node: str, fix_x: bool = False, fix_y: bool = False)[source]
classify(member: tuple[str, str]) str | None[source]

Classify a member as "tie" (tension) or "strut" (compression). A forced stm.member override wins; otherwise the solved sign decides (None until solve() has run).

degree_of_indeterminacy() int[source]

members + reactions - 2*nodes. Zero = statically determinate, positive = indeterminate (needs the direct-stiffness solver), negative = a mechanism (unstable).

diagnose() list[str][source]

Actionable pre-solve checks that name the offending node/member.

These checks run before a solve and point at the fixable cause – a dangling node, a load with no load path, a support on an undefined node, missing supports, and an under-constrained (mechanism) model. A statically indeterminate model is reported informationally only: it is solved by the direct-stiffness method (solve()), not an error. Returns an empty list for a clean, determinate model.

classmethod from_3dm(path, **kwargs)[source]

Build a model from a tagged Rhino .3dm file. Thin wrapper over civilpy.structural.rhino_stm.model_from_3dm() (needs the optional rhino3dm dependency).

classmethod from_structural_model(hub, *, plane='auto')[source]

Project a canonical StructuralModel hub down to this 2D model (default elevation maps X->x, Z->y). The inverse of to_structural_model().

The hub’s full 6-DOF restraints collapse to the in-plane (fix_x, fix_y) the truss solver consumes, and 3D load vectors are projected onto the plane. plane="auto" infers the drawing plane from the node coordinates. This lets from_3dm parse once into the hub and project, instead of carrying a second parser.

plot(ax=None, force_units: str = 'kips', show_reactions: bool = True)[source]

Draw the strut-and-tie diagram: ties solid red, struts dashed blue with width scaled to force magnitude, joint loads as green arrows, supports as triangles. Solves first if needed. Returns the figure.

solve(method: Literal['auto', 'joints', 'stiffness'] = 'auto') dict[tuple[str, str], float][source]

Solve member forces and reactions.

method="auto" uses the method of joints when the model is statically determinate and the direct-stiffness method (DSM) when it is indeterminate; pass "joints" or "stiffness" to force one. Returns {(a, b): force} with tension positive (tie) and compression negative (strut); reactions land in self.reactions.

The DSM result depends on the member areas (relative stiffness picks the load path among the redundant members). Use solve_fully_stressed() to converge areas onto the forces.

solve_fully_stressed(iterations: int = 25, tol: float = 0.0001, min_ratio: float = 0.001) dict[tuple[str, str], float][source]

Resize members in proportion to their force and re-solve until the load path stabilizes (fully-stressed design).

For an indeterminate truss the force distribution depends on the relative member areas; setting each area proportional to |force| and iterating drives the truss toward a uniform-stress load path — the natural strut-and-tie idealization. min_ratio floors the area (relative to the largest) so lightly loaded members keep the truss stable instead of vanishing. Determinate models are returned unchanged after a single solve.

to_3dm(path, **kwargs)[source]

Write this model to a tagged .3dm file (members as lines, supports as symbol blocks, loads as arrows).

to_structural_model(*, plane='XZ')[source]

Lift this 2D model into the canonical StructuralModel hub (the inverse of from_structural_model()).

In-plane coordinates and forces are placed in the chosen world plane; the 2D (fix_x, fix_y) fixity widens to a 6-DOF restraint with the remaining DOF free. Solved member forces and reactions, if present, are carried across into a Result.

civilpy.structural.substructure module

Substructure reactions from a solved bridge grillage – the hand-off from superstructure analysis to pier/abutment and foundation design.

A refined-grillage superstructure (from civilpy.structural.bridge_layout.grillage_model_from_layout()) sits on bearings at every support line. Those bearing reactions, summed across the girders of each support line and factored into the AASHTO load combinations, are exactly the loads a substructure unit (abutment or pier) and its foundation are designed for. This module pulls the per-node reactions MIDAS solved, bins them onto their substructure unit by station, and applies the combinations.

Sign/units: reactions in the model’s units (kips, ft) as MIDAS reports them – FZ up-positive vertical, FX longitudinal (along stations), FY transverse, MX/MY/MZ the moments. The vertical FZ and the horizontals size the bearings and the pier; the total FZ per unit sizes the foundation.

civilpy.structural.substructure.DEFAULT_COMBOS = {'Service I': {'DC': 1.0, 'DW': 1.0, 'LL': 1.0}, 'Strength I': {'DC': 1.25, 'DW': 1.5, 'LL': 1.75}}

Vertical AASHTO LRFD strength/service combinations (max load factors).

civilpy.structural.substructure.LOAD_TYPE = {'CLOSURE': 'DC', 'DC1': 'DC', 'DC2': 'DC', 'DW': 'DW', 'LL-LANE': 'LL', 'MOT-LANE': 'LL', 'PCB': 'DC'}

Map a load-case name to its AASHTO load type (for the combinations).

class civilpy.structural.substructure.SubstructureUnit(index: int, name: str, station_ft: float)[source]

Bases: object

One support line: an abutment or pier the superstructure bears on.

index: int
name: str
station_ft: float
civilpy.structural.substructure.design_reactions(grouped: dict[str, dict[str, tuple]], *, combos: dict[str, dict[str, float]] | None = None) dict[str, dict[str, tuple]][source]

Apply the AASHTO load combinations to grouped per-unit reactions.

Returns {unit_name: {combo_name: (FX..MZ)}} – the factored reactions each substructure unit and foundation is designed for. Cases are mapped to DC/DW/LL by LOAD_TYPE; a case with an unknown name is treated as DC.

civilpy.structural.substructure.fetch_support_reactions(midas, model, cases: list[str], *, suffix: str = '(ST)') dict[str, dict[int, tuple]][source]

Pull the support reactions for each static case from a solved MIDAS model: {case: {midas_node_id: (FX, FY, FZ, MX, MY, MZ)}}.

suffix is the result-case suffix MIDAS appends ("(ST)" static, "(CB)" combination). Call MidasCivil.analyze() first.

civilpy.structural.substructure.group_support_reactions(model, layout, reactions_by_case: dict[str, dict[int, tuple]]) dict[str, dict[str, tuple]][source]

Sum per-node reactions onto their substructure unit, per case.

reactions_by_case is {case: {midas_node_id: 6-tuple}} (from fetch_support_reactions()). Each node is binned to the nearest support line by its station (X). Returns {unit_name: {case: (FX..MZ) sum}}.

civilpy.structural.substructure.restrained_node_midas_ids(model) list[int][source]

MIDAS ids of the restrained (support) nodes.

civilpy.structural.substructure.substructure_reaction_report(midas, model, layout, cases: list[str], *, combos: dict[str, dict[str, float]] | None = None) dict[source]

Solve-side convenience: fetch, group, and combine in one call on a solved MIDAS model. Returns {"by_case": grouped, "design": design_reactions, "units": [...]}.

civilpy.structural.substructure.substructure_units(layout) list[SubstructureUnit][source]

The abutments and piers of layout, one per support line.

civilpy.structural.substructure_layout module

Substructure geometry placement: the executed design becomes the model.

substructure_from_layout() closes the loop the substructure design notebook opens. The superstructure hands its factored reactions to optimize_pier_cap() (pier and abutment caps), MultiColumnBent (columns), and 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 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; civilpy.structural .rhino_bim turns it into tagged emit objects on the Substructure::* layers.

class civilpy.structural.substructure_layout.AbutmentGeometry(unit: SubstructureUnit, cap: CapBeam, seats: tuple[BeamSeat, ...], piles: tuple[PileGeometry, ...], backwall: WallPanel | None = None, wingwalls: tuple[WallPanel, ...] = (), kind: Literal['seat', 'semi-integral', 'integral'] = 'seat', diaphragm: WallPanel | None = None)[source]

Bases: object

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).

backwall: WallPanel | None = None
cap: CapBeam
diaphragm: WallPanel | None = None
kind: Literal['seat', 'semi-integral', 'integral'] = 'seat'

Abutment configuration this geometry represents.

piles: tuple[PileGeometry, ...]
seats: tuple[BeamSeat, ...]
unit: SubstructureUnit
wingwalls: tuple[WallPanel, ...] = ()
class civilpy.structural.substructure_layout.AbutmentSpec(pile_xs_ft: tuple[float, ...], pile_shape: str = 'HP10X42', pile_length_ft: float = 40.0, backwall_thickness_in: float = 18.0, wingwall: object | None = None, wingwall_length_ft: float = 0.0)[source]

Bases: object

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 RetainingWall whose stem/footing dimensions the wingwall panels are read from; wingwall_length_ft its run along the roadway.

backwall_thickness_in: float = 18.0
pile_length_ft: float = 40.0
pile_shape: str = 'HP10X42'

AISC HP label from civilpy.structural.steel, e.g. "HP10X42".

pile_xs_ft: tuple[float, ...]
wingwall: object | None = None
wingwall_length_ft: float = 0.0
class civilpy.structural.substructure_layout.BeamSeat(girder_line: int, center: tuple[float, float, float], side_in: float, height_in: float)[source]

Bases: object

One stepped bearing seat: a square pedestal from the cap top up to the bottom of that girder’s bearing stack.

center: tuple[float, float, float]
girder_line: int
height_in: float
side_in: float
class civilpy.structural.substructure_layout.BentPierSpec(cap_design: object, bent: object, footing: FootingSpec | None = None)[source]

Bases: object

Multi-column bent: cap from cap_design, columns from bent (see pier_geometry()).

bent: object
build(layout, unit, **frame_kw) PierGeometry[source]
cap_design: object
footing: FootingSpec | None = None
class civilpy.structural.substructure_layout.CapBeam(origin: tuple[float, float, float], axis: tuple[float, float, float], 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)[source]

Bases: object

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.

axis: tuple[float, float, float]
depth_ft: float
length_ft: float
origin: tuple[float, float, float]
soffit_profile: tuple[tuple[float, float], ...] | None = None
tie_bar_count: int | None = None
tie_bar_size: int | None = None
tie_z_frac: float | None = None
property volume_cy: float
width_ft: float
class civilpy.structural.substructure_layout.ColumnGeometry(center: tuple[float, float], 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)[source]

Bases: object

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).

b_in: float | None = None
bars_area_in2: float = 0.0
center: tuple[float, float]
diameter_in: float | None = None
h_in: float | None = None
property height_ft: float
property volume_cy: float
z_bot: float
z_top: float
civilpy.structural.substructure_layout.DEFAULT_BEARING_STACK_IN = 4.5

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).

class civilpy.structural.substructure_layout.FootingGeometry(center: tuple[float, float], z_top: float, length_ft: float, width_ft: float, thickness_ft: float, axis: tuple[float, float, float])[source]

Bases: object

Spread/pile-cap footing under one column, aligned with the cap axes (length_ft along the cap).

axis: tuple[float, float, float]
center: tuple[float, float]
length_ft: float
thickness_ft: float
property volume_cy: float
width_ft: float
z_top: float
class civilpy.structural.substructure_layout.FootingSpec(length_ft: float, width_ft: float, thickness_ft: float)[source]

Bases: object

Per-column footing plan dims (a geotech deliverable — no civilpy footing designer exists yet, so these stay explicit inputs).

length_ft: float
thickness_ft: float
width_ft: float
class civilpy.structural.substructure_layout.HammerheadSpec(cap_design: object, column: object, tip_depth_ft: float | None = None, footing: FootingSpec | None = None)[source]

Bases: object

Single-column hammerhead pier (see hammerhead_geometry()). column is the executed PierColumn.

build(layout, unit, **frame_kw) PierGeometry[source]
cap_design: object
column: object
footing: FootingSpec | None = None
tip_depth_ft: float | None = None
class civilpy.structural.substructure_layout.IntegralAbutmentSpec(pile_xs_ft: tuple[float, ...], 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, wingwall_length_ft: float = 0.0)[source]

Bases: object

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.

build(layout, unit, **_frame_kw) AbutmentGeometry[source]
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
pile_length_ft: float = 40.0
pile_shape: str = 'HP10X42'

AISC HP label from civilpy.structural.steel, e.g. "HP10X42".

pile_xs_ft: tuple[float, ...]
wingwall: object | None = None
wingwall_length_ft: float = 0.0
civilpy.structural.substructure_layout.PILE_EMBED_IN = 12.0

pile head embedment into a capped-pile cap

class civilpy.structural.substructure_layout.PierGeometry(unit: SubstructureUnit, cap: CapBeam, seats: tuple[BeamSeat, ...], columns: tuple[ColumnGeometry, ...] = (), footings: tuple[FootingGeometry, ...] = (), piles: tuple[PileGeometry, ...] = ())[source]

Bases: object

One pier: a multi-column bent carries columns (+ optional footings); a capped-pile bent carries piles instead.

cap: CapBeam
columns: tuple[ColumnGeometry, ...] = ()
footings: tuple[FootingGeometry, ...] = ()
piles: tuple[PileGeometry, ...] = ()
seats: tuple[BeamSeat, ...]
unit: SubstructureUnit
class civilpy.structural.substructure_layout.PileBentSpec(cap_design: object, pile_xs_ft: tuple[float, ...], pile_shape: str = 'HP12X53', pile_length_ft: float = 40.0)[source]

Bases: object

Capped-pile pier (see pile_bent_geometry()).

build(layout, unit, **frame_kw) PierGeometry[source]
cap_design: object
pile_length_ft: float = 40.0
pile_shape: str = 'HP12X53'

AISC HP label from civilpy.structural.steel (CPP-1-08 default "HP12X53").

pile_xs_ft: tuple[float, ...]
class civilpy.structural.substructure_layout.PileGeometry(head: tuple[float, float, float], shape: str, length_ft: float)[source]

Bases: object

One driven HP pile. head is the butt at the embedment plane inside the cap; the pay length runs below the cutoff.

head: tuple[float, float, float]
length_ft: float
shape: str
civilpy.structural.substructure_layout.SEAT_MIN_IN = 3.0

minimum beam-seat (pedestal) height

civilpy.structural.substructure_layout.SEAT_SIDE_IN = 27.0

21 in load plate + 3 in edges

Type:

seat plan side

class civilpy.structural.substructure_layout.SeatAbutmentSpec(cap_design: object, spec: AbutmentSpec)[source]

Bases: object

Conventional seat abutment: the Phase-4 AbutmentSpec plus its cap design, buildable per unit.

build(layout, unit, **frame_kw) AbutmentGeometry[source]
cap_design: object
spec: AbutmentSpec
class civilpy.structural.substructure_layout.SemiIntegralAbutmentSpec(cap_design: object, spec: AbutmentSpec, diaphragm_thickness_in: float = 30.0)[source]

Bases: object

Seat abutment plus the superstructure-borne end diaphragm (see semi_integral_abutment_geometry()).

build(layout, unit, **frame_kw) AbutmentGeometry[source]
cap_design: object
diaphragm_thickness_in: float = 30.0
spec: AbutmentSpec
class civilpy.structural.substructure_layout.SubstructureLayout(layout: object, abutments: tuple[AbutmentGeometry, ...], piers: tuple[PierGeometry, ...])[source]

Bases: object

Every substructure unit of one bridge, placed under its layout.

abutments: tuple[AbutmentGeometry, ...]
layout: object
piers: tuple[PierGeometry, ...]
property units: tuple
class civilpy.structural.substructure_layout.WallPanel(origin: tuple[float, float, float], axis: tuple[float, float, float], length_ft: float, thickness_ft: float, height_ft: float)[source]

Bases: object

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.

axis: tuple[float, float, float]
height_ft: float
length_ft: float
origin: tuple[float, float, float]
thickness_ft: float
property volume_cy: float
civilpy.structural.substructure_layout.abutment_geometry(layout, unit: SubstructureUnit, cap_design, spec: AbutmentSpec, *, bearing_stack_in: float = 4.5, seat_min_in: float = 3.0, seat_side_in: float = 27.0, pile_embed_in: float = 12.0) AbutmentGeometry[source]

Place one capped-pile abutment: the cap from its cap_design (an 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 RetainingWall.

civilpy.structural.substructure_layout.assemble_substructure(layout, assignments: dict, *, bearing_stack_in: float = 4.5, seat_min_in: float = 3.0, seat_side_in: float = 27.0) SubstructureLayout[source]

Place a substructure that mixes unit types.

assignments maps a support-line index (0 at the start abutment) to its typed spec (BentPierSpec, PileBentSpec, SeatAbutmentSpec, …); the string keys "pier" and "abutment" supply defaults for unassigned units of that role.

civilpy.structural.substructure_layout.hammerhead_geometry(layout, unit: SubstructureUnit, cap_design, column, *, tip_depth_ft: float | None = None, footing: FootingSpec | None = None, bearing_stack_in: float = 4.5, seat_min_in: float = 3.0, seat_side_in: float = 27.0) PierGeometry[source]

Place one hammerhead pier: the cap from cap_design (an 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 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.

civilpy.structural.substructure_layout.integral_abutment_geometry(layout, unit: SubstructureUnit, spec: IntegralAbutmentSpec) AbutmentGeometry[source]

Place one integral abutment (see 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.

civilpy.structural.substructure_layout.pier_geometry(layout, unit: SubstructureUnit, cap_design, bent, *, footing: FootingSpec | None = None, bearing_stack_in: float = 4.5, seat_min_in: float = 3.0, seat_side_in: float = 27.0) PierGeometry[source]

Place one pier from its executed designs: the cap from cap_design (PierCapDesign), the columns from bent (MultiColumnBent, whose cap.column_positions are inches from the left end of the cap).

civilpy.structural.substructure_layout.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 = 4.5, seat_min_in: float = 3.0, seat_side_in: float = 27.0, pile_embed_in: float = 12.0) PierGeometry[source]

Place one capped-pile pier (pile bent): the cap from cap_design (an 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 (civilpy.structural.odot.capped_pile_pier carries the SCD’s own limits for the standard-drawing case).

civilpy.structural.substructure_layout.semi_integral_abutment_geometry(layout, unit: SubstructureUnit, cap_design, spec: AbutmentSpec, *, diaphragm_thickness_in: float = 30.0, bearing_stack_in: float = 4.5, seat_min_in: float = 3.0, seat_side_in: float = 27.0, pile_embed_in: float = 12.0) AbutmentGeometry[source]

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).

civilpy.structural.substructure_layout.substructure_from_layout(layout, *, pier_cap, pier_bent, abutment_cap, abutment: AbutmentSpec, footing: FootingSpec | None = None, bearing_stack_in: float = 4.5, seat_min_in: float = 3.0, seat_side_in: float = 27.0) SubstructureLayout[source]

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).

civilpy.structural.truss module

Plane truss analysis by the method of joints, with the force diagram.

This is the general-statics face of civilpy.structural.strut_and_tie.StrutAndTieModel — identical solver and plotting (tension red/solid, compression blue/dashed), plus member-length and axial-stress helpers for sizing.

Examples

>>> t = Truss()
>>> t.add_node("A", 0, 0)
>>> t.add_node("B", 12, 0)
>>> t.add_node("C", 6, 8)
>>> t.add_member("A", "B")
>>> t.add_member("A", "C")
>>> t.add_member("B", "C")
>>> t.add_support("A", fix_x=True, fix_y=True)
>>> t.add_support("B", fix_y=True)
>>> t.add_load("C", fx=10)
>>> f = t.solve()
>>> round(t.member_lengths()[("A", "C")], 0)
10.0
class civilpy.structural.truss.Truss(E: float = 1.0)[source]

Bases: StrutAndTieModel

Pin-jointed plane truss; see the base class for the full API.

member_lengths() dict[tuple[str, str], float][source]
member_stresses(areas) dict[tuple[str, str], float][source]

Axial stress per member (force units / area units). areas is a single area applied to every member or a {(a, b): area} mapping.

plot(ax=None, force_units: str = 'kips', show_reactions: bool = True)[source]

Draw the strut-and-tie diagram: ties solid red, struts dashed blue with width scaled to force magnitude, joint loads as green arrows, supports as triangles. Solves first if needed. Returns the figure.

civilpy.structural.truss_builder module

Object-oriented truss bridge builder: typed components, semantic deck and lane definitions, plane-truss analysis, AASHTO capacity checks, and direct MIDAS Civil NX API export.

The builder generates a complete truss bridge from panel geometry that may be fully non-uniform — every panel length and every panel-point height is independent, so real (often asymmetric) bridges can be modeled exactly. Components are typed (TopChord, Diagonal, Floorbeam, Stringer, …) so loads, capacity checks, and the exported analysis model all know what each piece is.

The load path is resolved semantically: a Deck (with an optional centerline offset for asymmetric roadways) sheds area load to the stringer lines by tributary width, stringers deliver reactions to the floorbeams, and each floorbeam distributes to the two truss planes by statics — so an off-center deck correctly loads one truss harder than the other. LaneLine objects place vehicle lanes by transverse offset; on export they become MIDAS traffic line lanes riding the nearest stringer elements with the exact eccentricity.

Conventions match civilpy.structural.beam_bending: kips and feet, plot methods take an optional ax and return the figure.

Examples

A 100-ft through Pratt truss with non-uniform panels and a polygonal (Parker-style) top chord:

>>> bridge = TrussBridge(panel_lengths_ft=[22, 28, 28, 22],
...                      heights_ft=[None, 24, 27, 24, None],
...                      width_ft=18.0)
>>> bridge.nodes["U2"]
(50.0, 27.0)
>>> _ = bridge.set_deck(Deck(width_ft=16.0, thickness_in=8.0))
>>> forces = bridge.solve()
>>> forces[("L1", "L2")] > 0          # bottom chord in tension
True
>>> forces[("U1", "U2")] < 0          # top chord in compression
True
>>> r = bridge.reactions()
>>> total = bridge.deck_area_load_ksf() * 16.0 * 100.0
>>> abs(sum(v[1] for v in r.values()) - total / 2.0) < 1e-6
True
class civilpy.structural.truss_builder.BottomChord(start: str, end: str, section: TrussSection | None = None, midas_type: str | None = None)[source]

Bases: Member

expected: str | None = 'tension'
midas_type = 'BEAM'
role = 'Bottom Chord'
class civilpy.structural.truss_builder.Counter(start: str, end: str, section: TrussSection | None = None, midas_type: str | None = None)[source]

Bases: Diagonal

Tension-only counter diagonal (TENS element on export). Counters make the plane truss statically indeterminate, so add them to MIDAS-bound models only.

midas_type = 'TENS'
role = 'Counter'
class civilpy.structural.truss_builder.Deck(width_ft: float, thickness_in: float = 8.0, unit_weight_pcf: float = 150.0, wearing_surface_psf: float = 0.0, offset_ft: float = 0.0)[source]

Bases: object

Semantic deck definition: geometry plus the loads it generates.

offset_ft shifts the deck centerline transversely (positive toward the +y truss plane) so asymmetric roadways shed more load to one truss. Deck self weight is DC; wearing_surface_psf is DW.

property dc_ksf: float
property dw_ksf: float
property edges_ft: tuple[float, float]
offset_ft: float = 0.0
thickness_in: float = 8.0
unit_weight_pcf: float = 150.0
wearing_surface_psf: float = 0.0
width_ft: float
class civilpy.structural.truss_builder.Diagonal(start: str, end: str, section: TrussSection | None = None, midas_type: str | None = None)[source]

Bases: Member

expected: str | None = 'tension'
role = 'Diagonal'
class civilpy.structural.truss_builder.EndPost(start: str, end: str, section: TrussSection | None = None, midas_type: str | None = None)[source]

Bases: Member

expected: str | None = 'compression'
midas_type = 'BEAM'
role = 'End Post'
class civilpy.structural.truss_builder.Floorbeam(point: str, section: TrussSection | None = None)[source]

Bases: Member

Transverse floor member at a panel point, spanning between the two truss planes. start/end name the deck-level panel-point node; the export expands it across the bridge width.

expected: str | None = 'flexure'
midas_type = 'BEAM'
property name: str
role = 'Floorbeam'
class civilpy.structural.truss_builder.Hanger(start: str, end: str, section: TrussSection | None = None, midas_type: str | None = None)[source]

Bases: Vertical

expected: str | None = 'tension'
role = 'Hanger'
class civilpy.structural.truss_builder.LaneLine(name: str, offset_ft: float, width_ft: float = 12.0, wheel_space_ft: float = 6.0)[source]

Bases: object

A vehicle lane line at a transverse offset from the bridge centerline (feet, positive toward the +y truss plane).

name: str
offset_ft: float
wheel_space_ft: float = 6.0
width_ft: float = 12.0
class civilpy.structural.truss_builder.LateralBrace(start: str, end: str, section: TrussSection | None = None, midas_type: str | None = None)[source]

Bases: Member

Cross-frame/lateral member added manually between any two 3-D node names (use the prime suffix for far-plane nodes, e.g. LateralBrace("U1", "U2'")).

role = 'Lateral Brace'
class civilpy.structural.truss_builder.Member(start: str, end: str, section: TrussSection | None = None, midas_type: str | None = None)[source]

Bases: object

A typed truss-bridge component between two named nodes.

expected records the gravity-load behavior the type implies (tension/compression/flexure) and routes TrussBridge. capacity_checks() to the matching AASHTO check. midas_type is the element type used on API export (BEAM, TRUSS, or TENS for tension-only members such as counters).

expected: str | None = None
property key: tuple[str, str]
midas_type = 'TRUSS'
property name: str
role = 'Member'
civilpy.structural.truss_builder.Post

alias of Vertical

class civilpy.structural.truss_builder.Stringer(offset_ft: float, section: TrussSection | None = None)[source]

Bases: Member

A longitudinal stringer line at a transverse offset, bearing on every floorbeam. One object represents the whole line; the export breaks it into one element per panel.

expected: str | None = 'flexure'
midas_type = 'BEAM'
property name: str
role = 'Stringer'
class civilpy.structural.truss_builder.Strut(point: str, section: TrussSection | None = None)[source]

Bases: Member

Transverse strut tying the two truss planes at a panel point away from the deck (e.g. top struts on a through truss).

expected: str | None = 'compression'
midas_type = 'BEAM'
property name: str
role = 'Strut'
class civilpy.structural.truss_builder.TopChord(start: str, end: str, section: TrussSection | None = None, midas_type: str | None = None)[source]

Bases: Member

expected: str | None = 'compression'
midas_type = 'BEAM'
role = 'Top Chord'
class civilpy.structural.truss_builder.TrussBridge(panel_lengths_ft, height_ft: float | None = None, *, heights_ft=None, width_ft: float = 20.0, pattern: str | None = 'pratt', deck_level: str = 'bottom')[source]

Bases: object

Build an analysis-ready truss bridge from panel geometry.

Parameters:
  • panel_lengths_ft (list of float) – Length of every panel — non-uniform spacing is fully supported.

  • height_ft (float, optional) – Uniform truss depth. For a through truss (deck_level= "bottom") the end panel points get no top node, producing inclined end posts; for a deck truss the top chord runs full length.

  • heights_ft (list of float or None, optional) – Explicit depth at every panel point (len(panel_lengths_ft) + 1 entries) for polygonal and asymmetric chords; None or 0 means no top node there. Overrides height_ft.

  • width_ft (float) – Center-to-center spacing of the two truss planes.

  • pattern ({"pratt", "howe", "warren", None}) – Web layout generated automatically. None builds chords and end posts only, for fully custom webs via add(). "warren" (with verticals) needs an even panel count.

  • deck_level ({"bottom", "top"}) – Chord carrying the floor system — through truss or deck truss.

add(member: Member)[source]

Add a custom typed member. Plane members (chords, web) use 2-D node names; LateralBrace may cross planes with the prime suffix.

add_lane(lane: LaneLine)[source]
add_panel_load(node: str, p_kips: float)[source]

Extra vertical load (positive down, kips) applied to a panel point of each truss plane — e.g. utilities or sidewalk.

assign_section(member_type: type, section: TrussSection)[source]

Assign a section to every member of a typed class (subclasses included): bridge.assign_section(Diagonal, TrussSection(...)).

capacity_checks(case: str = 'total', k_factor: float = 1.0, design_year: int | None = None) dict[str, object][source]

AASHTO LRFD axial checks for every plane member with a section: tension members through 6.8.2.1 (yield/rupture) and compression members through 6.9.4.1.1 (column buckling with KL/r from the member’s own length). Returns {member.name: CheckResult}; members without sections are skipped.

deck: Deck | None
deck_area_load_ksf(case: str = 'total') float[source]

Deck area load in ksf: "dc" (self weight), "dw" (wearing surface), or "total".

extra_panel_loads: dict[str, float]
floorbeam_point_loads(index: int, case: str = 'total') dict[float, float][source]

Concentrated loads (kips, positive down) the stringers drop on floorbeam index, keyed by transverse offset. Each stringer span is simply supported between floorbeams, so a floorbeam takes half of each adjacent panel.

floorbeams: list[Floorbeam]
lanes: list[LaneLine]
laterals: list[LateralBrace]
member(start: str, end: str) Member[source]
member_forces(case: str = 'total') dict[Member, float][source]

Governing (largest-magnitude) force per typed member across both truss planes.

member_length_ft(m: Member) float[source]
members: list[Member]
midas_payloads(node_start: int = 1, elem_start: int = 1) dict[str, dict][source]

Build every PUT /db/* Assign body for the full 3-D model.

Returns {table: assign} in send order: UNIT, MATL, SECT, NODE, ELEM, CONS, GRUP (structure groups named by component type), STLD (DC/DW cases), BMLD (deck load on the stringers), and LLAN (traffic line lanes riding the nearest stringer line).

Load paths stay semantic in the export: deck weight lands on stringer elements as beam loads, lanes reference stringer elements with their true eccentricity, and every element belongs to a group named for its component type. Field layouts for GRUP/STLD/BMLD follow the MIDAS API manual but are unverified against a live Civil NX session — check the send report’s errors first when debugging.

nodes: dict[str, tuple[float, float]]
panel_point_loads(plane: str = 'near', case: str = 'total') dict[str, float][source]

Vertical load (kips, positive down) at each deck-level panel point of one truss plane, from floorbeam statics. plane is "near" (y = -width/2) or "far" (+width/2); an off-center deck loads the planes unequally.

plane_truss(plane: str = 'near', case: str = 'total') Truss[source]

The 2-D Truss for one plane, loaded at the deck-level panel points; pin at L0, roller at the far bottom chord end.

plot_3d(ax=None, show_lanes: bool = True, elev: float = 22.0, azim: float = -60.0)[source]

Isometric wireframe of the complete 3-D model, drawn from the same node/element tables midas_payloads() exports — so the picture is exactly what a to_midas() call will build: both truss planes, floorbeams (split at stringer crossings), stringers, struts, and any laterals, colored by component type, with lane lines and the deck outline overlaid. Returns the figure.

plot_cross_section(ax=None)[source]

Transverse section at a floorbeam: truss planes, floorbeam, stringers, deck, and lane positions. Returns the figure.

plot_elevation(ax=None)[source]

Elevation of the truss colored by member type, with the deck level and panel points marked. Returns the figure.

plot_forces(plane: str = 'near', case: str = 'total', ax=None)[source]

Solved member-force diagram (tension red, compression blue) for one truss plane; see Truss.plot().

reactions(plane: str = 'near', case: str = 'total') dict[str, list[float]][source]
remove(start: str, end: str)[source]

Remove the plane member between two nodes (either order).

set_deck(deck: Deck, stringer_offsets_ft=None, max_stringer_spacing_ft: float = 8.0)[source]

Attach the deck and lay out stringer lines.

Without explicit offsets, stringers are spaced evenly across the deck width (edge lines at the deck edges) at no more than max_stringer_spacing_ft.

solve(plane: str = 'near', case: str = 'total') dict[tuple[str, str], float][source]

Member forces (kips, tension positive) for one truss plane.

stringer_line_loads(case: str = 'total') dict[float, float][source]

Uniform load on each stringer line (klf, positive down) from the deck by tributary width, keyed by stringer offset.

stringers: list[Stringer]
struts: list[Strut]
to_midas(midas=None, **client_kwargs) dict[source]

Send the model to a live Civil NX session through civilpy.structural.midas.MidasCivil (created from ~/secrets.json when not given). Sends each table in order and keeps going on errors; returns {table: {"sent": n} | {"error": message}} so a single bad table is visible without losing the rest of the model.

class civilpy.structural.truss_builder.TrussSection(name: str, area_in2: float, fy_ksi: float = 50.0, fu_ksi: float = 65.0, r_in: float | None = None, net_area_in2: float | None = None, shear_lag_u: float = 1.0)[source]

Bases: object

Axial-member section for capacity checks and export.

r_in is the governing radius of gyration (needed only for compression checks); net_area_in2 enables the rupture check on tension members.

area_in2: float
fu_ksi: float = 65.0
fy_ksi: float = 50.0
name: str
net_area_in2: float | None = None
r_in: float | None = None
shear_lag_u: float = 1.0
class civilpy.structural.truss_builder.Vertical(start: str, end: str, section: TrussSection | None = None, midas_type: str | None = None)[source]

Bases: Member

expected: str | None = 'compression'
role = 'Vertical'

civilpy.structural.wood module

Wood structural design per NDS (National Design Specification) 2024.

Provides section property lookups, NDS adjustment factor calculations, member design checks (bending, shear, compression, tension, deflection), timber bridge design per AASHTO LRFD, and bolted connection design.

References

  • AWC, National Design Specification (NDS) for Wood Construction, 2024.

  • Breyer, D. et al., Design of Wood Structures – ASD/LRFD, 8th Ed.

  • AASHTO LRFD Bridge Design Specifications, 9th Ed., Section 8 & 9.

  • USDA Forest Service, Timber Bridge Design, Construction, Inspection and Maintenance (LTRC/LTAP).

class civilpy.structural.wood.AdjustmentFactors(section, method='ASD')[source]

Bases: object

NDS adjustment factor calculator.

Computes all applicable adjustment factors for a wood member and returns adjusted design values. Works with both sawn lumber and glulam sections.

Parameters:
  • section – A LumberSection or GlulamSection instance.

  • method – “ASD” or “LRFD”. Default “ASD”.

CD(load_duration='normal')[source]

Load duration factor per NDS Table 2.3.2.

CF(prop='Fb')[source]

Size factor for sawn lumber per NDS Table 4.3.1.

Returns 1.0 for glulam or Beams & Stringers / Posts & Timbers.

CL(lu, load_type='uniform')[source]

Beam stability factor per NDS 3.3.3.

Parameters:
  • lu – Unbraced length (Pint length quantity). Use 0 if fully braced.

  • load_type – Loading condition for effective length calculation.

CM(prop, wet_service=False)[source]

Wet service factor per NDS Table 4.3.3 / 5.3.3.

Parameters:
  • prop – Property name (“Fb”, “Ft”, “Fv”, “Fc_perp”, “Fc”, “E”, “Emin”).

  • wet_service – True if in-service moisture content > 19%.

CV(L=None)[source]

Volume factor for glulam per NDS 5.3.6.

CV = (21/L)^(1/x) * (12/d)^(1/x) * (5.125/b)^(1/x) where x = 10 for Western Species, x = 10 for Southern Pine.

Parameters:

L – Span length in feet (Pint length). Required for glulam.

Cb(bearing_length)[source]

Bearing area factor per NDS 3.10.4.

Parameters:

bearing_length – Length of bearing along grain (Pint length).

Cfu(flat_use=False)[source]

Flat use factor per NDS Table 4.3.7.

Ci(prop, incised=False)[source]

Incising factor per NDS Table 4.3.8.

Cp(le, axis='strong')[source]

Column stability factor per NDS 3.7.1.

Parameters:
  • le – Effective column length (Pint length quantity).

  • axis – “strong” or “weak” buckling axis.

Cr(repetitive=False)[source]

Repetitive member factor. 1.15 if 3+ members <= 12 in. deep, spaced <= 24 in. o.c.

Ct(temperature_f, prop, wet_service=False)[source]

Temperature factor per NDS Table 2.3.3.

Parameters:
  • temperature_f – Maximum sustained temperature in deg F.

  • prop – Property name.

  • wet_service – True if wet service conditions.

KF(prop)[source]

Format conversion factor (LRFD) per NDS Table N1.

adjusted_value(prop, load_duration='normal', wet_service=False, temperature_f=100.0, incised=False, repetitive=False, lu=None, le_col=None, bearing_length=None, flat_use=False, span_length=None, load_type='uniform')[source]

Compute the fully adjusted design value F’ for a given property.

Parameters:
  • prop – Property name (“Fb”, “Ft”, “Fv”, “Fc_perp”, “Fc”, “E”, “Emin”).

  • load_duration – Load duration case name.

  • wet_service – True if MC > 19%.

  • temperature_f – Max sustained temperature (deg F).

  • incised – True if incised for treatment.

  • repetitive – True if repetitive member.

  • lu – Unbraced length for CL (bending only).

  • le_col – Effective column length for Cp (compression only).

  • bearing_length – Bearing length for Cb (Fc_perp only).

  • flat_use – True if loaded on flat face.

  • span_length – Span length for CV (glulam bending only).

  • load_type – Loading type for CL calculation.

Returns:

{“value”: adjusted Pint quantity, “factors”: dict of factor names->values}

Return type:

dict

lam(load_duration='normal')[source]

Time effect factor (LRFD) per NDS Table N3.

phi(prop)[source]

Resistance factor (LRFD) per NDS Table N2.

class civilpy.structural.wood.BoltConnection(bolt_diameter, main_thickness, side_thickness, main_species_gravity, side_species_gravity=None, side_material='wood', loading='single_shear', angle_to_grain=0)[source]

Bases: object

Bolted connection design per NDS Chapter 12.

Computes reference lateral design values using the yield mode equations and applies NDS adjustment factors.

Parameters:
  • bolt_diameter – Bolt diameter (Pint length, e.g. 0.75*units(“in”)).

  • main_thickness – Main member thickness (Pint length).

  • side_thickness – Side member thickness (Pint length).

  • main_species_gravity – Specific gravity of main member.

  • side_species_gravity – Specific gravity of side member (use 1.0 for steel).

  • side_material – “wood” or “steel”.

  • loading – “single_shear” or “double_shear”.

  • angle_to_grain – Angle between force and grain direction (degrees). 0 = parallel, 90 = perpendicular.

C_delta(end_distance, edge_distance, bolt_spacing, row_spacing=None)[source]

Geometry factor per NDS 12.5.

Checks minimum end distance, edge distance, and spacing requirements. Returns 1.0 if all minimums met, reduced value if distances are between minimum and full.

Parameters:
  • end_distance – Distance from bolt center to end of member (Pint length).

  • edge_distance – Distance from bolt center to edge (Pint length).

  • bolt_spacing – Center-to-center spacing between bolts in a row (Pint length).

  • row_spacing – Spacing between rows (Pint length). Optional.

Returns:

Geometry factor (0 to 1.0).

Return type:

float

Cg(num_bolts_in_row, Am=None, As=None)[source]

Group action factor per NDS 10.3.6 Table 10.3.6A.

Simplified approach: for 2 bolts in a row, Cg ~ 1.0. For more bolts, Cg decreases.

Parameters:
  • num_bolts_in_row – Number of fasteners in a row.

  • Am – Area of main member (in^2). Defaults from section.

  • As – Area of side member (in^2). Defaults to Am/2.

Returns:

Group action factor.

Return type:

float

Z_prime(num_bolts=1, num_bolts_in_row=1, load_duration='normal', wet_service=False, temperature_f=100.0, end_distance=None, edge_distance=None, bolt_spacing=None)[source]

Adjusted total connection design value Z’.

Z’ = Z * n * CD * CM * Ct * Cg * C_delta

Parameters:
  • num_bolts – Total number of bolts.

  • num_bolts_in_row – Bolts per row (for Cg).

  • load_duration – Load duration case.

  • wet_service – True if wet service.

  • temperature_f – Max sustained temperature.

  • end_distance – End distance (Pint length).

  • edge_distance – Edge distance (Pint length).

  • bolt_spacing – In-row spacing (Pint length).

Returns:

dict with Z_prime, Z_ref, factors.

Z_reference()[source]

Reference single-bolt lateral design value Z per NDS 12.3.1.

Uses the European Yield Model (EYM) equations for the six yield modes (Im, Is, II, IIIm, IIIs, IV).

Returns:

{“Z”: value in lbs, “governing_mode”: mode name, “all_modes”: dict}

Return type:

dict

class civilpy.structural.wood.GlulamSection(width, depth, combination=None, species=None)[source]

Bases: object

Glued laminated timber section with NDS reference design values.

Can be constructed either from a size label or separate width/depth:

>>> beam = GlulamSection("6.75x30", "24F-V4", "Douglas Fir")
>>> beam = GlulamSection(6.75, 30, "24F-V4", "Douglas Fir")
Parameters:
  • width – Actual width in inches (e.g. 5.125, 6.75, 8.75, 10.75), or a size label string such as “6.75x30” (in which case the remaining arguments shift left).

  • depth – Actual depth in inches.

  • combination – Glulam combination symbol (e.g. “24F-V4”, “20F-V7”).

  • species – Species group (e.g. “Douglas Fir”, “Southern Pine”).

Example

>>> beam = GlulamSection(6.75, 30, "24F-V4", "Douglas Fir")
>>> float(beam.Fb_pos.magnitude)
2400.0
class civilpy.structural.wood.LumberSection(nominal_width, nominal_depth, species=None, grade=None)[source]

Bases: object

Sawn lumber section with NDS reference design values.

Looks up actual dimensions from the lumber sizes table and reference design values from the NDS Supplement tables.

Can be constructed either from a size label (like steel sections, e.g. W("W36X150")) or from separate width/depth values:

>>> beam = LumberSection("6x12", "Douglas Fir-Larch", "No. 1")
>>> beam = LumberSection(6, 12, "Douglas Fir-Larch", "No. 1")
Parameters:
  • nominal_width – Nominal width in inches (e.g. 2, 4, 6, 8), or a size label string such as “6x12” (in which case the remaining arguments shift left: LumberSection("6x12", species, grade)).

  • nominal_depth – Nominal depth in inches (e.g. 4, 6, 8, 10, 12).

  • species – Species group (e.g. “Douglas Fir-Larch”, “Southern Pine”).

  • grade – Lumber grade (e.g. “Select Structural”, “No. 1”, “No. 2”).

Example

>>> beam = LumberSection(6, 12, "Douglas Fir-Larch", "No. 1")
>>> float(beam.b.magnitude)
5.5
>>> float(beam.d.magnitude)
11.25
>>> joist = LumberSection("2x10", "Douglas Fir-Larch", "No. 2")
>>> float(joist.d.magnitude)
9.25
class civilpy.structural.wood.TimberBridgeDeck(deck_type, span, thickness, species, grade, wearing_surface='none')[source]

Bases: object

Timber bridge deck design per AASHTO LRFD Section 9.9.

Parameters:
  • deck_type – “nail_laminated”, “spike_laminated”, “glulam”, “stress_laminated”, or “plank”.

  • span – Clear span of deck panel (Pint length).

  • thickness – Deck lamination depth (Pint length).

  • species – Wood species group.

  • grade – Lumber grade.

  • wearing_surface – Key from WEARING_SURFACE_WEIGHTS or “none”.

check_deck()[source]

Full deck adequacy check under AASHTO HL-93 wheel load.

Uses a 16-kip wheel load (half of 32-kip axle) distributed over the effective width.

Returns:

dict with bending, shear, and deflection check results.

dead_load_per_ft()[source]

Dead load on a 1-ft-wide strip (lbf/ft).

Returns:

Pint force/length quantity.

wheel_load_distribution_width(tire_contact_width=None)[source]

Effective width for wheel load distribution per AASHTO 4.6.2.1.3.

For transverse nail-laminated decks:

E = 2.0 * t + 40 (in.) (approximate, perpendicular to span)

Parameters:

tire_contact_width – Tire contact length (Pint length). Defaults to 20 in. per AASHTO 3.6.1.2.5.

Returns:

Effective distribution width.

Return type:

Pint length quantity

class civilpy.structural.wood.TimberStringer(section, span, spacing, num_lanes=1, deck_type='nail_laminated')[source]

Bases: object

Timber bridge stringer design per AASHTO LRFD.

Parameters:
  • section – LumberSection or GlulamSection for the stringer.

  • span – Stringer span (Pint length).

  • spacing – Stringer center-to-center spacing (Pint length).

  • num_lanes – Number of design lanes.

  • deck_type – Type of deck supported.

dynamic_load_allowance()[source]

Dynamic load allowance (IM) per AASHTO 3.6.2.3.

Returns 0.33 for timber bridges (same as other materials). Note: Some agencies reduce IM for timber. AASHTO does not distinguish by material.

live_load_distribution_factor(num_stringers=5)[source]

Live load distribution factor for moment per AASHTO Table 4.6.2.2.2a-1.

Simplified method for timber beam/slab bridges. For one design lane loaded: g = S/D where S = spacing (ft), D depends on bridge type.

Parameters:

num_stringers – Total number of stringers.

Returns:

Distribution factor (lanes per beam).

Return type:

float

live_load_distribution_factor_shear(num_stringers=5)[source]

Live load distribution factor for shear.

Conservatively uses the moment distribution factor (AASHTO allows this simplification for timber bridges).

rating_factor(DC_moment, DW_moment, LL_moment, capacity_moment, gamma_DC=1.25, gamma_DW=1.5, gamma_LL=1.75, phi_c=1.0, phi_s=1.0)[source]

Load rating factor per AASHTO MBE Eq. 6A.4.2.1-1.

RF = (phi_c * phi_s * C - gamma_DC * DC - gamma_DW * DW) /

(gamma_LL * LL * (1 + IM))

Parameters:
  • DC_moment – Dead load moment (Pint moment).

  • DW_moment – Wearing surface moment (Pint moment).

  • LL_moment – Live load moment per stringer (Pint moment, without IM).

  • capacity_moment – Member capacity M = Fb’ * Sx (Pint moment).

  • gamma_DC – DC load factor.

  • gamma_DW – DW load factor.

  • gamma_LL – LL load factor.

  • phi_c – Condition factor (1.0, 0.85, or 0.65).

  • phi_s – System factor (1.0 for most timber).

Returns:

dict with RF, numerator, denominator, and status.

class civilpy.structural.wood.WoodMemberCheck(section, method='ASD', wet_service=False, temperature_f=100.0, incised=False, repetitive=False, load_duration='normal')[source]

Bases: object

Complete NDS member design check with calc-sheet output.

Orchestrates adjustment factor computation and demand/capacity checks for all stress types. Each check returns a dict of intermediate values suitable for display in a Jupyter notebook.

Parameters:
  • section – LumberSection or GlulamSection.

  • method – “ASD” or “LRFD”.

  • wet_service – True if in-service MC > 19%.

  • temperature_f – Maximum sustained temperature (deg F).

  • incised – True if incised for preservative treatment.

  • repetitive – True if repetitive member conditions apply.

  • load_duration – Governing load duration case.

check_bearing(R, bearing_length, bearing_width=None)[source]

Compression perpendicular to grain (bearing) check.

Parameters:
  • R – Applied bearing reaction (Pint force).

  • bearing_length – Length of bearing along grain (Pint length).

  • bearing_width – Width of bearing (Pint length). Defaults to section width.

Returns:

dict with fc_perp, Fc_perp_prime, ratio, status, factors.

check_bending(M, lu=None, load_type='uniform', span_length=None)[source]

Bending stress check.

Parameters:
  • M – Applied moment (Pint moment quantity, e.g. ft*lbf or in*lbf).

  • lu – Unbraced length (Pint length). 0 or None = fully braced.

  • load_type – Loading type for effective length.

  • span_length – Span for CV (glulam only).

Returns:

dict with fb, Fb_prime, ratio, status, factors.

check_combined_bending_compression(P, M, le_strong, le_weak=None, lu=None, load_type='uniform')[source]

Combined bending and axial compression per NDS Eq. 3.9-3.

(fc/Fc’)^2 + fb / (Fb’ * (1 - fc/FcE)) <= 1.0

Parameters:
  • P – Axial compression (Pint force).

  • M – Bending moment (Pint moment).

  • le_strong – Effective column length, strong axis.

  • le_weak – Effective column length, weak axis.

  • lu – Unbraced length for bending.

  • load_type – Loading type for CL.

Returns:

dict with interaction ratio and components.

check_compression(P, le_strong, le_weak=None)[source]

Compression parallel to grain check.

Parameters:
  • P – Applied axial compression (Pint force).

  • le_strong – Effective length about strong axis (Pint length).

  • le_weak – Effective length about weak axis (defaults to le_strong).

Returns:

dict with fc, Fc_prime, ratio, status, Cp, le_d, factors.

check_deflection(w, L, limit='L/360', I_eff=None)[source]

Deflection check for uniform load on simple span:

delta = 5*w*L^4 / (384*E'*I)
Parameters:
  • w – Uniform load (Pint force/length, e.g. lbf/ft).

  • L – Span length (Pint length).

  • limit – Deflection limit as string (e.g. “L/360”, “L/240”, “L/425”).

  • I_eff – Override moment of inertia. Defaults to section Ix.

Returns:

dict with delta, delta_limit, ratio, status.

check_shear(V)[source]

Shear stress check (fv = 3V/2A per NDS 3.4.2).

Parameters:

V – Applied shear force (Pint force quantity).

Returns:

dict with fv, Fv_prime, ratio, status, factors.

check_tension(T)[source]

Tension parallel to grain check.

Parameters:

T – Applied tension force (Pint force).

Returns:

dict with ft, Ft_prime, ratio, status, factors.

summary()[source]

Return a DataFrame summarizing all checks performed.

Returns:

Check, Demand, Capacity, Ratio, Status.

Return type:

pd.DataFrame with columns

civilpy.structural.wood.beam_stability_factor(Emin_prime, Fb_star, le, d, b)[source]

CL per NDS 3.3.3 with Pint quantities.

Parameters:
  • Emin_prime – Adjusted E’min (Pint stress).

  • Fb_star – Fb * (all factors except CL and CV) (Pint stress).

  • le – Effective unbraced length (Pint length).

  • d – Depth (Pint length).

  • b – Width (Pint length).

Returns:

Return type:

float

civilpy.structural.wood.beam_stability_factor_from_ratio(FbE, Fb_star)[source]

CL per NDS 3.3.3.8.

Same form as Cp but with c=0.95 for beams.

Parameters:
  • FbE – Critical buckling stress (numeric, psi).

  • Fb_star – Fb times all factors except CL and CV (numeric, psi).

Returns:

Beam stability factor CL.

Return type:

float

civilpy.structural.wood.column_stability_factor(FcE, Fc_star, c=0.8)[source]

Cp per NDS Eq. 3.7-1.

Parameters:
  • FcE – Euler critical buckling stress (numeric, psi).

  • Fc_star – Fc multiplied by all adjustment factors except Cp (numeric, psi).

  • c – 0.8 for sawn lumber, 0.9 for glulam, 0.85 for round timber.

Returns:

Column stability factor Cp.

Return type:

float

civilpy.structural.wood.effective_beam_length(lu, load_type='uniform')[source]

Effective unbraced length le for lateral-torsional buckling.

Per NDS Table 3.3.3.

Parameters:
  • lu – Unbraced length (Pint length quantity).

  • load_type – One of “uniform”, “concentrated_center”, “concentrated_quarter”, “equal_end_moments”, “cantilever_uniform”, “cantilever_point”.

Returns:

Pint length quantity.

civilpy.structural.wood.euler_critical_buckling_stress(Emin_prime, le, d)[source]

FcE per NDS 3.7.1.5 with Pint quantities.

Parameters:
  • Emin_prime – Adjusted minimum modulus of elasticity (Pint stress).

  • le – Effective column length (Pint length).

  • d – Member dimension in buckling plane (Pint length).

Returns:

Pint stress quantity.

civilpy.structural.wood.euler_critical_buckling_stress_val(Emin_prime, le_in, d_in)[source]

FcE = 0.822 * E’min / (le/d)^2 per NDS 3.7.1.5.

All arguments are raw numeric values in psi / inches.

civilpy.structural.wood.list_available_glulam()[source]

Return available glulam combinations.

civilpy.structural.wood.list_available_grades(species=None)[source]

Return available grades, optionally filtered by species.

civilpy.structural.wood.list_available_species()[source]

Return a list of available wood species in the reference database.

civilpy.structural.wood.list_lumber_sizes()[source]

Return the lumber sizes table as a DataFrame.

Module contents

Structural engineering package — the most developed part of CivilPy.

Highlights: