Cross-Section Geometry#

Note

Engine: OpenSWMM 6 — refactored. This page documents openswmm.engine.XSectionGeometry, which exposes the engine’s cross-section geometry kernels directly. Reference: openswmm_xsect.h.

Every cross-section question the routing solvers ask — what is the flow area at this depth? the top width? the hydraulic radius? the critical depth for this flow? — is answered by a small set of geometry kernels. XSectionGeometry exposes those same kernels, so you can use them as a reference implementation without running a simulation, and get answers that agree with the engine exactly.

from openswmm.engine import XSectionGeometry, XSectShape

pipe = XSectionGeometry(XSectShape.CIRCULAR, 1.0, units="SI")
pipe.area(0.5)          # 0.3927 m² — a half-full 1 m pipe
pipe.full_area          # 0.7854 m²
pipe.is_open            # False

Units are explicit#

units is keyword-only and required. There is no default, because a cross-section carries no record of the units its numbers were written in — a silently assumed unit system is a silently wrong answer.

units

Lengths / areas

critical_depth() flow

"US"

ft, ft²

CFS

"SI"

m, m²

CMS

Section factors (A·R^(2/3)) come back in length8/3. A section taken from a link inherits its model’s units, including its actual flow units, which may be any of CFS/GPM/MGD/CMS/LPS/MLD:

xs = s.links["C1"].xsect.geometry()
xs.units          # 'US'
xs.flow_units     # 'GPM', if that is what the model uses

Scalars or arrays#

Every query takes a float or a NumPy array. Array input runs as a single batched C call and returns an ndarray of the same shape, which is how you build a rating curve without a Python loop:

import numpy as np

pipe = XSectionGeometry(XSectShape.CIRCULAR, 2.0, units="US")
depths = np.linspace(0.0, 2.0, 200)

area = pipe.area(depths)
hrad = pipe.hyd_radius(depths)

# Manning's equation over the whole curve at once.
n, slope = 0.013, 0.005
q = (1.49 / n) * area * hrad ** (2 / 3) * slope ** 0.5

import matplotlib.pyplot as plt
plt.plot(q, depths)
plt.xlabel("Discharge (cfs)")
plt.ylabel("Depth (ft)")

Queries#

Method

Returns

area(depth)

Flow area

width(depth)

Top width of the water surface

hyd_radius(depth)

Hydraulic radius (area / wetted perimeter)

depth_from_area(area)

Depth — the inverse of area()

hyd_radius_from_area(area)

Hydraulic radius from area

section_factor(area)

Section factor, A·R^(2/3)

area_from_section_factor(sf)

Area — used to solve for normal depth

dsda(area)

dS/dA

critical_depth(flow)

Critical depth

And the full-flow properties, as attributes: full_depth, full_area, full_hyd_radius, max_width, full_section_factor, max_area, is_open.

Depths above the full depth of a closed shape are clamped, not rejected — the routing solvers behave the same way, so a surcharged conduit does not raise. Negative or non-finite inputs do raise ValueError.


Building a section#

Self-contained shapes#

Pass the shape and its [XSECTIONS] Geom1–Geom4 values:

XSectionGeometry(XSectShape.CIRCULAR, 1.0, units="SI")
XSectionGeometry(XSectShape.RECT_CLOSED, 3.0, 5.0, units="US")
XSectionGeometry(XSectShape.TRAPEZOIDAL, 4.0, 2.0, 1.0, 3.0, units="US")
#                                        depth bot  m1   m2

Two shapes are worth calling out:

  • FORCE_MAIN is geometrically a circular pipe — it differs only in its friction law, so its geometric queries match CIRCULAR of the same diameter.

  • DUMMY has no geometry: it constructs, and every query returns 0.

Irregular (natural) channels#

Mirrors a [TRANSECTS] entry. The Manning’s n values are not decoration — the hydraulic-radius table is conveyance-weighted across the left overbank, channel and right overbank, so they change what hyd_radius() reports:

creek = XSectionGeometry.from_transect(
    stations=[0.0, 4.0, 6.0, 10.0],
    elevations=[4.0, 0.0, 0.0, 4.0],
    left_bank=4.0, right_bank=6.0,
    n_channel=0.03, n_left=0.06, n_right=0.06,
    units="US",
)
creek.full_depth    # 4.0
creek.max_width     # 10.0
creek.is_open       # True

Custom shape curves and streets#

# A SHAPE-type [CURVES] entry, scaled to a 5 ft full depth.
XSectionGeometry.from_curve(5.0, [0.0, 0.5, 1.0], [0.2, 1.0, 0.6],
                            units="US")

# A [STREETS] entry. Slopes are percentages, as in the input file.
XSectionGeometry.from_street(width=20.0, curb_height=0.5, slope=2.0,
                             roughness=0.016, sides=2, units="US")

A street’s full depth is whichever of the curb or the crown stands higher above the gutter — with a 2% slope over 20 ft the crown rises 0.4 ft, so the 0.5 ft curb governs.



Labelled parameters#

Links.get_xsect_info() names the geometry parameters for you, rather than leaving you to remember what Geom3 means for a given shape:

xs = s.links.get_xsect_info("C1")
xs.shape_name     # 'CIRCULAR'
xs.geom_labels    # {'diameter': 1.2}

s.links.get_xsect_info("CH1").geom_labels
# {'height': 4.0, 'bottom_width': 2.0, 'left_slope': 1.0, 'right_slope': 3.0}

Warning

Shape codes were renumbered in 6.0. XSectShape.IRREGULAR, CUSTOM and FORCE_MAIN previously carried the values 16/17/18, which the engine read as RECT_TRIANG/RECT_ROUND/HORIZ_ELLIPSE — assigning them silently produced the wrong cross-section. Seven shapes that the enum had been missing (RECT_TRIANG, RECT_ROUND, HORIZ_ELLIPSE, VERT_ELLIPSE, ARCH, STREET_XSECT, DUMMY) were added at the same time, so XSectShape now covers all 26.

Always pass the enum member rather than a bare integer and the correction is automatic. shape_name() resolves a code at runtime if you need to check stored data.

See also

Links for the rest of the link surface, and XSectShape for the per-shape meaning of Geom1–Geom4.