civilpy.structural.stm_topology package

Strut-and-tie topology optimization: turns a D-region description into an optimized truss (SIMP density optimization on a structured quad mesh, truss extraction, member design, and cost estimation).

Submodules

civilpy.structural.stm_topology.cost module

Reinforcement sizing, node checks, material take-off, and an ODOT cost estimate for a solved strut-and-tie truss.

Forces come from the solved StrutAndTieModel (tension +, kips); ties are sized by inverting the AASHTO 5.8.2.4 tie resistance, struts/nodes are checked with 5.8.2.5. Lengths are in feet, areas in in^2; the cost model uses ODOT-style unit prices.

class civilpy.structural.stm_topology.cost.CostEstimate(concrete_cy: 'float', steel_lb: 'float', concrete_cost: 'float', steel_cost: 'float', total: 'float', unit_prices: 'dict')[source]

Bases: object

concrete_cost: float
concrete_cy: float
steel_cost: float
steel_lb: float
total: float
unit_prices: dict
class civilpy.structural.stm_topology.cost.DesignReport(ties: 'list' = <factory>, node_checks: 'list' = <factory>, concrete_volume_cf: 'float' = 0.0, steel_volume_cf: 'float' = 0.0, cost: 'CostEstimate | None' = None)[source]

Bases: object

concrete_volume_cf: float = 0.0
cost: CostEstimate | None = None
node_checks: list
steel_volume_cf: float = 0.0
ties: list
class civilpy.structural.stm_topology.cost.TieDesign(member: 'tuple', force: 'float', length: 'float', a_st_required: 'float', bar_size: 'int', bar_count: 'int', a_st_provided: 'float', check: 'object')[source]

Bases: object

a_st_provided: float
a_st_required: float
bar_count: int
bar_size: int
check: object
force: float
length: float
member: tuple
civilpy.structural.stm_topology.cost.check_nodes(model, problem) list[source]

Bearing/nodal-zone check (AASHTO 5.8.2.5) at each supported and loaded node where a bearing width is given. Classifies the node CCC/CCT/CTT from the solved member signs meeting it.

civilpy.structural.stm_topology.cost.design_report(model, problem, density_result, *, threshold: float = 0.3, **price_kwargs) DesignReport[source]
civilpy.structural.stm_topology.cost.estimate_cost(concrete_cf, steel_cf, *, price_concrete_cy: float | None = None, price_steel_lb: float | None = None, prices=None) CostEstimate[source]

ODOT-style cost: concrete by the cubic yard, reinforcing by the pound.

Unit prices come from (highest priority first) the explicit price_* arguments, then the prices mapping, then the registered price provider, then DEFAULT_PRICES (see set_price_provider()).

civilpy.structural.stm_topology.cost.material_takeoff(density_result, problem, ties, *, threshold: float = 0.3)[source]

Concrete volume from the optimized density field and steel volume from the sized ties (cubic feet).

civilpy.structural.stm_topology.cost.resolve_prices(prices=None) dict[source]

Resolve unit prices in priority order: explicit prices mapping > the registered provider > DEFAULT_PRICES.

civilpy.structural.stm_topology.cost.set_price_provider(provider)[source]

Register a zero-argument callable that returns a unit-price mapping with keys concrete_cy ($/cy) and steel_lb ($/lb) — and optionally source. It is consulted whenever a take-off is requested without an explicit prices argument. civilpy falls back to DEFAULT_PRICES if the provider is unset, returns nothing, or raises. Returns the previous provider (pass None to clear).

civilpy.structural.stm_topology.cost.size_ties(model, *, f_y: float = 60.0, bar_size: int = 10) list[TieDesign][source]

Size reinforcement for every tension member (tie).

Required steel inverts AASHTO 5.8.2.4: Ast = Pu / (phi * fy); bars are rounded up to whole bar_size bars.

civilpy.structural.stm_topology.design module

Pier-cap dimension design driven by girder reactions.

The everyday workflow this targets: an engineer pulls factored girder reactions and their bearing locations out of a line-girder run (AASHTOWare BrR, Merlin DASH, …), and wants the most economical cap depth that still produces a valid, code-checkable strut-and-tie model for that specific layout.

optimize_pier_cap() sweeps the cap depth, runs the topology-optimization pipeline (optimize_to_stm()) at each depth, and returns the shallowest depth — i.e. the least concrete, which dominates cost — that satisfies the design gates:

  • the strut-and-tie model is a complete, solved load path (every girder and column present, global equilibrium satisfied);

  • the flattest primary strut meets the AASHTO 5.8.2.5 / ACI 318 Ch. 23 nodal angle limit (default 25 deg), equivalently the deep-beam a/d limit; and

  • every nodal zone has capacity.

The cap length is fixed by the girder layout plus an edge distance, and the width by the columns/bearings, so depth is the free dimension.

class civilpy.structural.stm_topology.design.DepthCandidate(depth: float, cost: float, concrete_cost: float, steel_lb: float, strut_angle: float, node_ratio: float, max_tie: float, complete: bool, feasible: bool, result: object)[source]

Bases: object

One depth evaluated in the sweep.

complete: bool
concrete_cost: float
cost: float
depth: float
feasible: bool
max_tie: float
node_ratio: float
result: object
steel_lb: float
strut_angle: float
class civilpy.structural.stm_topology.design.PierCapDesign(optimal: DepthCandidate | None, candidates: list[DepthCandidate] = <factory>, span: float = 0.0, thickness: float = 0.0, min_strut_angle: float = 25.0)[source]

Bases: object

Result of optimize_pier_cap().

candidates: list[DepthCandidate]
min_strut_angle: float = 25.0
property model
optimal: DepthCandidate | None
plot(ax=None)[source]

Draw the optimal strut-and-tie model over its density field.

plot_tradeoff(ax=None)[source]

Cost and strut angle vs depth, with the infeasible band shaded and the optimum marked.

property report
span: float = 0.0
summary() str[source]
thickness: float = 0.0
civilpy.structural.stm_topology.design.governing_strut_angle(load_xs, column_xs, depth) float[source]

Inclination (deg) of the flattest primary strut: the largest shear span a (girder-to-nearest-column distance) with theta = atan(depth / a).

This geometric a/d measure is robust and monotonic in depth, unlike the angle read off whichever discrete truss the extractor produced at a given depth, so it is what gates the sweep.

civilpy.structural.stm_topology.design.optimize_pier_cap(loads, load_xs, column_xs, *, f_c: float = 5.0, f_y: float = 60.0, thickness: float = 4.0, depth_bounds: tuple[float, float] = (4.0, 12.0), n_depths: int = 9, edge: float = 2.5, vol_frac: float = 0.3, nelx: int = 72, column_bearing: float = 2.0, load_bearing: float = 1.0, min_strut_angle: float = 25.0, price_concrete_cy: float | None = None, price_steel_lb: float | None = None, prices=None, pin_column: int | None = None) PierCapDesign[source]

Find the most economical pier-cap depth for a set of girder reactions.

Parameters:
  • loads – Factored girder reactions (kip, magnitudes) and their bearing locations (ft) along the cap — typically straight from a BrR / DASH run.

  • load_xs – Factored girder reactions (kip, magnitudes) and their bearing locations (ft) along the cap — typically straight from a BrR / DASH run.

  • column_xs – Column centreline locations (ft).

  • thickness – Out-of-plane cap width b (ft).

  • depth_bounds – Range and sampling of cap depths to evaluate.

  • n_depths – Range and sampling of cap depths to evaluate.

  • edge – Cap overhang beyond the outermost girder/column (ft); sets the length.

  • column_bearing – In-plane bearing widths (ft) for the nodal-zone checks.

  • load_bearing – In-plane bearing widths (ft) for the nodal-zone checks.

  • min_strut_angle – AASHTO 5.8.2.5 nodal angle limit (deg); raise it to force a stockier, more clearly deep-beam cap.

  • pin_column – Index of the laterally-restrained (pinned) column; defaults to the one nearest mid-length, the rest acting as vertical rollers.

Return type:

PierCapDesign whose optimal is the shallowest feasible depth.

civilpy.structural.stm_topology.extract module

Truss extraction: turn a SIMP density field into a strut-and-tie truss.

Pipeline: threshold the density to a binary load path, thin it to a one-pixel skeleton (Zhang–Suen), walk the skeleton into a node/edge graph, force a node at every support and load, simplify each traced path to straight members (Douglas–Peucker), collapse near-coincident nodes, and emit a StrutAndTieModel with the supports and loads attached. Pure NumPy — the thinning is hand-rolled so no scikit-image dependency is required.

civilpy.structural.stm_topology.extract.extract_truss(result, *, threshold: float = 0.3, merge: float | None = None) StrutAndTieModel[source]

Extract a StrutAndTieModel from a DensityResult.

Parameters:
  • threshold – Density above which a cell is part of the load path.

  • merge – Node-merge tolerance in pixels (default scales with the mesh). Each traced skeleton path becomes a single straight member between its real graph nodes, so truss joints land only at supports, loads, and genuine junctions.

civilpy.structural.stm_topology.extract.is_stable(model, tol: float = 1e-08) bool[source]

True when the truss has no mechanism — its free-DOF stiffness matrix (unit member stiffness, supports applied) is non-singular.

This is the honest stability test the pipeline needs: the member/reaction count (StrutAndTieModel.degree_of_indeterminacy()) is necessary but not sufficient, and np.linalg.solve can return garbage for a near- mechanism instead of raising. We compare the smallest singular value of the free-free stiffness block against the largest.

civilpy.structural.stm_topology.extract.layout_optimize_truss(model, density_result, *, keep: float = 0.02, min_density: float = 0.12, search: float | None = None, betweenness: float | None = None, cluster_tol: float | None = None, symmetric: bool = True, boundary_chords: bool = True, boundary_discount: float = 0.3, max_crossing_iter: int = 3)[source]

Extract a strut-and-tie model by LP plastic layout optimization.

Uses model’s joints (cleaned: clustered, and mirrored when the problem is symmetric) as the node set, lays a density-filtered ground structure between them, and solves a single LP for the minimum-volume equilibrium load path. Returns a new StrutAndTieModel with member forces and reactions already populated, or None if the program is infeasible (caller falls back to the FSD refinement).

Two geometric rules give an ACI 318-detailable result:

  • boundary_chords adds a continuous chord along each loaded edge (adjacent supports along the bottom, adjacent loads along the top) and keeps it even when lightly loaded — the conventional flexural tie / top compression chord an engineer expects for detailing and crack control.

  • intersecting members are never left un-noded: after each solve, a joint is inserted at every crossing of two load-carrying members and the layout is re-optimized (up to max_crossing_iter times), so every crossing is a real nodal zone (ACI 318 Ch. 23) rather than two members sharing a point in space.

civilpy.structural.stm_topology.extract.refine_truss(model, density_result, *, method: str = 'lp', **kwargs)[source]

Turn a raw skeleton truss into a clean, solved strut-and-tie model.

method="lp" (default) runs layout_optimize_truss() — a single global plastic truss layout optimization that is inherently symmetric for a symmetric problem. It returns a new model. method="fsd" (or an lp failure) falls back to the in-place ground-structure fully-stressed-design refinement _refine_fsd().

Returns the model to use (possibly a new object), so callers must use the return value rather than assuming in-place mutation.

civilpy.structural.stm_topology.extract.zhang_suen(binary: ndarray) ndarray[source]

Thin a binary image to a 1-pixel skeleton (Zhang–Suen 1984).

civilpy.structural.stm_topology.mesh module

Structured-quad meshing for the topology optimizer.

A fixed grid of square Q4 plane-stress elements covers the region’s bounding box; elements whose centroid falls outside the boundary polygon (or inside a void) are passive empty, elements inside a solid keep-in polygon are passive full, and the rest are active (optimized). This “fixed-grid FEM” handles arbitrary polygons while keeping the regular grid that makes the density filter and skeletonization trivial — the deliberate trade chosen in docs/Rhino Design Philosophy.md (structured quads, not a body-fitted mesh).

Conventions follow the classic top88 topology-optimization code so its element stiffness drops straight into simp:

  • grids are (nely, nelx) with row 0 at the top;

  • nodes are numbered column-major, n = (nely+1)*col + row (node 0 top-left);

  • element (row, col) has linear index el = row + nely*col (Fortran order), so active.ravel(order="F") lines up with edof_matrix() rows.

class civilpy.structural.stm_topology.mesh.GroundMesh(problem, nelx: int = 120)[source]

Bases: object

A structured grid of unit-topology square elements over a region.

edof_matrix() ndarray[source]

(nelx*nely, 8) DOF indices per element, row order el = row + nely*col, DOF order (bottom-left, bottom-right, top-right, top-left) matching the top88 element stiffness.

property n_dofs: int
property n_nodes: int
nearest_node(x: float, y: float) int[source]
node_id(col: int, row: int) int[source]
node_xy(node_id: int) tuple[float, float][source]
nodes_within(x: float, y: float, bearing: float | None) list[int][source]

Grid nodes within bearing/2 of (x, y) — the set a support reaction or applied load is spread over. Falls back to the single nearest node when bearing is unset or smaller than the mesh.

civilpy.structural.stm_topology.pipeline module

End-to-end strut-and-tie solver pipeline: region → mesh → SIMP → truss → solve → design report (Phases 1–5 of docs/StrutAndTieSolver.md).

optimize_to_stm is the one call the notebooks (manual or Rhino) use. It is also reachable as DRegionProblem.solve().

class civilpy.structural.stm_topology.pipeline.STMResult(problem: object, density: DensityResult, model: object, forces: dict, report: DesignReport, merge_used: float, stable: bool)[source]

Bases: object

Everything the pipeline produces, ready for review or write-back.

density: DensityResult
forces: dict
merge_used: float
property mesh
model: object
plot(ax=None)[source]

Density field with the extracted truss overlaid.

problem: object
report: DesignReport
stable: bool
summary() str[source]
civilpy.structural.stm_topology.pipeline.optimize_to_stm(problem, *, nelx: int = 120, threshold: float = 0.3, merge: float | None = None, penal: float = 3.0, rmin: float | None = None, max_iter: int = 120, fully_stressed: bool = True, verbose: bool = False, **price_kwargs) STMResult[source]

Run the full pipeline and return an STMResult.

Extraction skeletonizes the SIMP field for node positions, then solves an LP plastic truss layout optimization for the discrete load path; the resulting model carries its own member forces and reactions (an equilibrium load path, so no separate solve is needed).

civilpy.structural.stm_topology.problem module

The D-region optimization problem — the data contract between the front end (manual Python or a tagged Rhino .3dm) and the topology-optimization pipeline.

The same object drives both authoring workflows described in docs/Rhino Design Philosophy.md: geometry carries what is spatial, tags carry what is scalar. A DRegionProblem is the boundary polygon of the concrete region plus its thickness/material and the supports and loads, and it is consumed by civilpy.structural.stm_topology.simp (the SIMP optimizer) and extract (truss extraction).

Units follow the Rhino contract: lengths (boundary, thickness, bearing) in feet, forces in kips, f_c/E in ksi.

class civilpy.structural.stm_topology.problem.DRegionProblem(boundary: list[tuple[float, float]], thickness: float, material: ~civilpy.structural.stm_topology.problem.Material = <factory>, supports: list[~civilpy.structural.stm_topology.problem.Support] = <factory>, loads: list[~civilpy.structural.stm_topology.problem.Load] = <factory>, voids: list[list[tuple[float, float]]] = <factory>, solids: list[list[tuple[float, float]]] = <factory>, vol_frac: float = 0.3)[source]

Bases: object

A concrete D-region to be filled with an optimized strut-and-tie truss.

Parameters:
  • boundary (list[tuple[float, float]]) – Outer polygon as [(x, y), ...] in feet (closed automatically).

  • thickness (float) – Out-of-plane thickness b in feet (plane-stress depth).

  • material (civilpy.structural.stm_topology.problem.Material) – Material.

  • supports – Boundary conditions, shared with the drawn-truss workflow.

  • loads – Boundary conditions, shared with the drawn-truss workflow.

  • voids – Optional inner polygons: voids are mandatory holes (passive empty), solids are keep-in zones (passive full).

  • solids – Optional inner polygons: voids are mandatory holes (passive empty), solids are keep-in zones (passive full).

  • vol_frac (float) – Target fraction of the region to keep as concrete (SIMP constraint).

add_load(x, y, fx=0.0, fy=0.0, bearing=None)[source]
add_support(x, y, fix_x=True, fix_y=True, bearing=None)[source]
boundary: list[tuple[float, float]]
bounds() tuple[float, float, float, float][source]
classmethod from_3dm(path, **kwargs)[source]

Read a tagged Rhino .3dm authored with the region workflow (a stm.kind=region closed curve plus supports and loads). Thin wrapper over civilpy.structural.rhino_stm.problem_from_3dm() (needs the optional rhino3dm dependency).

loads: list[Load]
material: Material
classmethod rectangle(width, height, thickness, origin=(0.0, 0.0), **kwargs)[source]

A rectangular region with its lower-left corner at origin.

solids: list[list[tuple[float, float]]]
solve(**kwargs)[source]

Run the full pipeline: mesh → SIMP → extract truss → solve → check. Returns a civilpy.structural.stm_topology.pipeline.STMResult.

supports: list[Support]
thickness: float
voids: list[list[tuple[float, float]]]
vol_frac: float = 0.3
class civilpy.structural.stm_topology.problem.Load(x: float, y: float, fx: float = 0.0, fy: float = 0.0, bearing: float | None = None)[source]

Bases: object

A factored point load (kips) applied on the region. Direction is the (fx, fy) vector; bearing is the in-plane bearing width (feet).

bearing: float | None = None
fx: float = 0.0
fy: float = 0.0
property magnitude: float
x: float
y: float
class civilpy.structural.stm_topology.problem.Material(f_c: float = 5.0, E: float | None = None, nu: float = 0.2, unit_weight: float = 0.145)[source]

Bases: object

Concrete material for the optimization and the capacity checks.

E: float | None = None
f_c: float = 5.0
nu: float = 0.2
unit_weight: float = 0.145
class civilpy.structural.stm_topology.problem.Support(x: float, y: float, fix_x: bool = True, fix_y: bool = True, bearing: float | None = None)[source]

Bases: object

A restrained point on the region boundary (becomes an FEA boundary condition and an STM support). bearing is the in-plane loaded width (feet) over which the reaction is spread; None defaults to the mesh size at solve time.

bearing: float | None = None
fix_x: bool = True
fix_y: bool = True
x: float
y: float

civilpy.structural.stm_topology.simp module

SIMP topology optimization for the strut-and-tie solver.

Solid Isotropic Material with Penalization on the structured quad grid: a linear plane-stress FEA (the top88 Q4 element) inside an optimality-criteria loop with a density filter, driven by the region’s supports and loads. The result is a (nely, nelx) density field — the picture of where concrete wants to be — which extract turns into a truss.

Pure NumPy/SciPy; no scikit-fem needed because the structured grid lets us use the closed-form element stiffness directly.

class civilpy.structural.stm_topology.simp.DensityResult(density: ndarray, mesh: GroundMesh, compliance: float, iterations: int, history: list)[source]

Bases: object

Output of the SIMP optimizer.

compliance: float
density: ndarray
history: list
iterations: int
mesh: GroundMesh
civilpy.structural.stm_topology.simp.element_stiffness(nu: float) ndarray[source]

8x8 plane-stress Q4 element stiffness for unit modulus and size (Andreassen et al. 2011, Efficient topology optimization in MATLAB).

civilpy.structural.stm_topology.simp.optimize_density(mesh: GroundMesh, *, vol_frac: float = 0.3, penal: float = 3.0, rmin: float | None = None, max_iter: int = 120, move: float = 0.2, tol: float = 0.01, nu: float = 0.2, verbose: bool = False) DensityResult[source]

Run SIMP on mesh and return the optimized density field.

vol_frac is the target solid fraction of the active (optimizable) region. rmin (filter radius, element units) defaults to ~3% of the width. Supports/loads come from mesh.problem.

Module contents

Topology-optimized strut-and-tie generation.

Turn a concrete D-region (a drawn rectangle or arbitrary polygon, with supports and loads) into an optimized strut-and-tie truss via SIMP topology optimization and skeleton extraction, then size and cost it. The method is documented in docs/StrutAndTieSolver.md.

Typical use:

from civilpy.structural.stm_topology import DRegionProblem

p = DRegionProblem.rectangle(20, 10, thickness=2.0, vol_frac=0.35)
p.add_support(1, 0, bearing=1.0)
p.add_support(19, 0, fix_x=False, bearing=1.0)
p.add_load(10, 10, fy=-600, bearing=1.0)
result = p.solve()
print(result.summary())
result.plot()
class civilpy.structural.stm_topology.DRegionProblem(boundary: list[tuple[float, float]], thickness: float, material: ~civilpy.structural.stm_topology.problem.Material = <factory>, supports: list[~civilpy.structural.stm_topology.problem.Support] = <factory>, loads: list[~civilpy.structural.stm_topology.problem.Load] = <factory>, voids: list[list[tuple[float, float]]] = <factory>, solids: list[list[tuple[float, float]]] = <factory>, vol_frac: float = 0.3)[source]

Bases: object

A concrete D-region to be filled with an optimized strut-and-tie truss.

Parameters:
  • boundary (list[tuple[float, float]]) – Outer polygon as [(x, y), ...] in feet (closed automatically).

  • thickness (float) – Out-of-plane thickness b in feet (plane-stress depth).

  • material (civilpy.structural.stm_topology.problem.Material) – Material.

  • supports – Boundary conditions, shared with the drawn-truss workflow.

  • loads – Boundary conditions, shared with the drawn-truss workflow.

  • voids – Optional inner polygons: voids are mandatory holes (passive empty), solids are keep-in zones (passive full).

  • solids – Optional inner polygons: voids are mandatory holes (passive empty), solids are keep-in zones (passive full).

  • vol_frac (float) – Target fraction of the region to keep as concrete (SIMP constraint).

add_load(x, y, fx=0.0, fy=0.0, bearing=None)[source]
add_support(x, y, fix_x=True, fix_y=True, bearing=None)[source]
boundary: list[tuple[float, float]]
bounds() tuple[float, float, float, float][source]
classmethod from_3dm(path, **kwargs)[source]

Read a tagged Rhino .3dm authored with the region workflow (a stm.kind=region closed curve plus supports and loads). Thin wrapper over civilpy.structural.rhino_stm.problem_from_3dm() (needs the optional rhino3dm dependency).

loads: list[Load]
material: Material
classmethod rectangle(width, height, thickness, origin=(0.0, 0.0), **kwargs)[source]

A rectangular region with its lower-left corner at origin.

solids: list[list[tuple[float, float]]]
solve(**kwargs)[source]

Run the full pipeline: mesh → SIMP → extract truss → solve → check. Returns a civilpy.structural.stm_topology.pipeline.STMResult.

supports: list[Support]
thickness: float
voids: list[list[tuple[float, float]]]
vol_frac: float = 0.3
class civilpy.structural.stm_topology.DensityResult(density: ndarray, mesh: GroundMesh, compliance: float, iterations: int, history: list)[source]

Bases: object

Output of the SIMP optimizer.

compliance: float
density: ndarray
history: list
iterations: int
mesh: GroundMesh
class civilpy.structural.stm_topology.DepthCandidate(depth: float, cost: float, concrete_cost: float, steel_lb: float, strut_angle: float, node_ratio: float, max_tie: float, complete: bool, feasible: bool, result: object)[source]

Bases: object

One depth evaluated in the sweep.

complete: bool
concrete_cost: float
cost: float
depth: float
feasible: bool
max_tie: float
node_ratio: float
result: object
steel_lb: float
strut_angle: float
class civilpy.structural.stm_topology.GroundMesh(problem, nelx: int = 120)[source]

Bases: object

A structured grid of unit-topology square elements over a region.

edof_matrix() ndarray[source]

(nelx*nely, 8) DOF indices per element, row order el = row + nely*col, DOF order (bottom-left, bottom-right, top-right, top-left) matching the top88 element stiffness.

property n_dofs: int
property n_nodes: int
nearest_node(x: float, y: float) int[source]
node_id(col: int, row: int) int[source]
node_xy(node_id: int) tuple[float, float][source]
nodes_within(x: float, y: float, bearing: float | None) list[int][source]

Grid nodes within bearing/2 of (x, y) — the set a support reaction or applied load is spread over. Falls back to the single nearest node when bearing is unset or smaller than the mesh.

class civilpy.structural.stm_topology.Load(x: float, y: float, fx: float = 0.0, fy: float = 0.0, bearing: float | None = None)[source]

Bases: object

A factored point load (kips) applied on the region. Direction is the (fx, fy) vector; bearing is the in-plane bearing width (feet).

bearing: float | None = None
fx: float = 0.0
fy: float = 0.0
property magnitude: float
x: float
y: float
class civilpy.structural.stm_topology.Material(f_c: float = 5.0, E: float | None = None, nu: float = 0.2, unit_weight: float = 0.145)[source]

Bases: object

Concrete material for the optimization and the capacity checks.

E: float | None = None
f_c: float = 5.0
nu: float = 0.2
unit_weight: float = 0.145
class civilpy.structural.stm_topology.PierCapDesign(optimal: DepthCandidate | None, candidates: list[DepthCandidate] = <factory>, span: float = 0.0, thickness: float = 0.0, min_strut_angle: float = 25.0)[source]

Bases: object

Result of optimize_pier_cap().

candidates: list[DepthCandidate]
min_strut_angle: float = 25.0
property model
optimal: DepthCandidate | None
plot(ax=None)[source]

Draw the optimal strut-and-tie model over its density field.

plot_tradeoff(ax=None)[source]

Cost and strut angle vs depth, with the infeasible band shaded and the optimum marked.

property report
span: float = 0.0
summary() str[source]
thickness: float = 0.0
class civilpy.structural.stm_topology.STMResult(problem: object, density: DensityResult, model: object, forces: dict, report: DesignReport, merge_used: float, stable: bool)[source]

Bases: object

Everything the pipeline produces, ready for review or write-back.

density: DensityResult
forces: dict
merge_used: float
property mesh
model: object
plot(ax=None)[source]

Density field with the extracted truss overlaid.

problem: object
report: DesignReport
stable: bool
summary() str[source]
class civilpy.structural.stm_topology.Support(x: float, y: float, fix_x: bool = True, fix_y: bool = True, bearing: float | None = None)[source]

Bases: object

A restrained point on the region boundary (becomes an FEA boundary condition and an STM support). bearing is the in-plane loaded width (feet) over which the reaction is spread; None defaults to the mesh size at solve time.

bearing: float | None = None
fix_x: bool = True
fix_y: bool = True
x: float
y: float
civilpy.structural.stm_topology.estimate_cost(concrete_cf, steel_cf, *, price_concrete_cy: float | None = None, price_steel_lb: float | None = None, prices=None) CostEstimate[source]

ODOT-style cost: concrete by the cubic yard, reinforcing by the pound.

Unit prices come from (highest priority first) the explicit price_* arguments, then the prices mapping, then the registered price provider, then DEFAULT_PRICES (see set_price_provider()).

civilpy.structural.stm_topology.extract_truss(result, *, threshold: float = 0.3, merge: float | None = None) StrutAndTieModel[source]

Extract a StrutAndTieModel from a DensityResult.

Parameters:
  • threshold – Density above which a cell is part of the load path.

  • merge – Node-merge tolerance in pixels (default scales with the mesh). Each traced skeleton path becomes a single straight member between its real graph nodes, so truss joints land only at supports, loads, and genuine junctions.

civilpy.structural.stm_topology.governing_strut_angle(load_xs, column_xs, depth) float[source]

Inclination (deg) of the flattest primary strut: the largest shear span a (girder-to-nearest-column distance) with theta = atan(depth / a).

This geometric a/d measure is robust and monotonic in depth, unlike the angle read off whichever discrete truss the extractor produced at a given depth, so it is what gates the sweep.

civilpy.structural.stm_topology.is_stable(model, tol: float = 1e-08) bool[source]

True when the truss has no mechanism — its free-DOF stiffness matrix (unit member stiffness, supports applied) is non-singular.

This is the honest stability test the pipeline needs: the member/reaction count (StrutAndTieModel.degree_of_indeterminacy()) is necessary but not sufficient, and np.linalg.solve can return garbage for a near- mechanism instead of raising. We compare the smallest singular value of the free-free stiffness block against the largest.

civilpy.structural.stm_topology.layout_optimize_truss(model, density_result, *, keep: float = 0.02, min_density: float = 0.12, search: float | None = None, betweenness: float | None = None, cluster_tol: float | None = None, symmetric: bool = True, boundary_chords: bool = True, boundary_discount: float = 0.3, max_crossing_iter: int = 3)[source]

Extract a strut-and-tie model by LP plastic layout optimization.

Uses model’s joints (cleaned: clustered, and mirrored when the problem is symmetric) as the node set, lays a density-filtered ground structure between them, and solves a single LP for the minimum-volume equilibrium load path. Returns a new StrutAndTieModel with member forces and reactions already populated, or None if the program is infeasible (caller falls back to the FSD refinement).

Two geometric rules give an ACI 318-detailable result:

  • boundary_chords adds a continuous chord along each loaded edge (adjacent supports along the bottom, adjacent loads along the top) and keeps it even when lightly loaded — the conventional flexural tie / top compression chord an engineer expects for detailing and crack control.

  • intersecting members are never left un-noded: after each solve, a joint is inserted at every crossing of two load-carrying members and the layout is re-optimized (up to max_crossing_iter times), so every crossing is a real nodal zone (ACI 318 Ch. 23) rather than two members sharing a point in space.

civilpy.structural.stm_topology.optimize_density(mesh: GroundMesh, *, vol_frac: float = 0.3, penal: float = 3.0, rmin: float | None = None, max_iter: int = 120, move: float = 0.2, tol: float = 0.01, nu: float = 0.2, verbose: bool = False) DensityResult[source]

Run SIMP on mesh and return the optimized density field.

vol_frac is the target solid fraction of the active (optimizable) region. rmin (filter radius, element units) defaults to ~3% of the width. Supports/loads come from mesh.problem.

civilpy.structural.stm_topology.optimize_pier_cap(loads, load_xs, column_xs, *, f_c: float = 5.0, f_y: float = 60.0, thickness: float = 4.0, depth_bounds: tuple[float, float] = (4.0, 12.0), n_depths: int = 9, edge: float = 2.5, vol_frac: float = 0.3, nelx: int = 72, column_bearing: float = 2.0, load_bearing: float = 1.0, min_strut_angle: float = 25.0, price_concrete_cy: float | None = None, price_steel_lb: float | None = None, prices=None, pin_column: int | None = None) PierCapDesign[source]

Find the most economical pier-cap depth for a set of girder reactions.

Parameters:
  • loads – Factored girder reactions (kip, magnitudes) and their bearing locations (ft) along the cap — typically straight from a BrR / DASH run.

  • load_xs – Factored girder reactions (kip, magnitudes) and their bearing locations (ft) along the cap — typically straight from a BrR / DASH run.

  • column_xs – Column centreline locations (ft).

  • thickness – Out-of-plane cap width b (ft).

  • depth_bounds – Range and sampling of cap depths to evaluate.

  • n_depths – Range and sampling of cap depths to evaluate.

  • edge – Cap overhang beyond the outermost girder/column (ft); sets the length.

  • column_bearing – In-plane bearing widths (ft) for the nodal-zone checks.

  • load_bearing – In-plane bearing widths (ft) for the nodal-zone checks.

  • min_strut_angle – AASHTO 5.8.2.5 nodal angle limit (deg); raise it to force a stockier, more clearly deep-beam cap.

  • pin_column – Index of the laterally-restrained (pinned) column; defaults to the one nearest mid-length, the rest acting as vertical rollers.

Return type:

PierCapDesign whose optimal is the shallowest feasible depth.

civilpy.structural.stm_topology.optimize_to_stm(problem, *, nelx: int = 120, threshold: float = 0.3, merge: float | None = None, penal: float = 3.0, rmin: float | None = None, max_iter: int = 120, fully_stressed: bool = True, verbose: bool = False, **price_kwargs) STMResult[source]

Run the full pipeline and return an STMResult.

Extraction skeletonizes the SIMP field for node positions, then solves an LP plastic truss layout optimization for the discrete load path; the resulting model carries its own member forces and reactions (an equilibrium load path, so no separate solve is needed).

civilpy.structural.stm_topology.refine_truss(model, density_result, *, method: str = 'lp', **kwargs)[source]

Turn a raw skeleton truss into a clean, solved strut-and-tie model.

method="lp" (default) runs layout_optimize_truss() — a single global plastic truss layout optimization that is inherently symmetric for a symmetric problem. It returns a new model. method="fsd" (or an lp failure) falls back to the in-place ground-structure fully-stressed-design refinement _refine_fsd().

Returns the model to use (possibly a new object), so callers must use the return value rather than assuming in-place mutation.

civilpy.structural.stm_topology.resolve_prices(prices=None) dict[source]

Resolve unit prices in priority order: explicit prices mapping > the registered provider > DEFAULT_PRICES.

civilpy.structural.stm_topology.set_price_provider(provider)[source]

Register a zero-argument callable that returns a unit-price mapping with keys concrete_cy ($/cy) and steel_lb ($/lb) — and optionally source. It is consulted whenever a take-off is requested without an explicit prices argument. civilpy falls back to DEFAULT_PRICES if the provider is unset, returns nothing, or raises. Returns the previous provider (pass None to clear).