"""
Cross-Section Geometry
======================
:author: Caleb Buahin
:copyright: Copyright (c) 2026 Caleb Buahin
:license: MIT
Provides the :class:`CrossSection` dataclass, which wraps the raw
``(shape, geom1, geom2, geom3, geom4)`` tuple a link reports with
human-readable field labels and a ``shape_name`` derived from the
:class:`XSectShape` enum.
Obtain one from :meth:`Links.get_xsect_info` or ``link.xsect.info()``. For the
hydraulic geometry of a section — area, top width, hydraulic radius, critical
depth — see :class:`~openswmm.engine.XSectionGeometry` instead.
Example::
from openswmm.engine import Solver, Links, CrossSection
with Solver("model.inp", "model.rpt", "model.out") as s:
links = Links(s)
xs = links.get_xsect_info(0)
print(xs.shape_name) # "CIRCULAR"
print(xs.geom_labels) # {"diameter": 1.2}
"""
from __future__ import annotations
from dataclasses import dataclass
from ._enums import XSectShape
# ---------------------------------------------------------------------------
# Shape-name lookup
# ---------------------------------------------------------------------------
_XSECT_SHAPE_NAMES: dict[int, str] = {
int(s): s.name for s in XSectShape
}
# ---------------------------------------------------------------------------
# Geometry parameter labels per shape
# ---------------------------------------------------------------------------
# Each tuple entry corresponds to geom1, geom2, geom3, geom4 in order.
# Entries shorter than 4 mean the trailing geoms are unused / zero.
_GEOM_LABELS: dict[int, tuple[str, ...]] = {
XSectShape.CIRCULAR: ("diameter",),
XSectShape.FILLED_CIRCULAR: ("diameter", "filled_depth"),
XSectShape.RECT_CLOSED: ("height", "width"),
XSectShape.RECT_OPEN: ("height", "width", "sides_removed"),
XSectShape.TRAPEZOIDAL: ("height", "bottom_width", "left_slope", "right_slope"),
XSectShape.TRIANGULAR: ("height", "top_width"),
XSectShape.PARABOLIC: ("height", "top_width"),
XSectShape.POWER: ("height", "top_width", "exponent"),
XSectShape.MODBASKETHANDLE: ("height", "bottom_width", "top_radius"),
XSectShape.EGGSHAPED: ("height",),
XSectShape.HORSESHOE: ("height",),
XSectShape.GOTHIC: ("height",),
XSectShape.CATENARY: ("height",),
XSectShape.SEMIELLIPTICAL: ("height",),
XSectShape.BASKETHANDLE: ("height",),
XSectShape.SEMICIRCULAR: ("height",),
XSectShape.RECT_TRIANG: ("height", "top_width", "triangle_height"),
XSectShape.RECT_ROUND: ("height", "top_width", "bottom_radius"),
XSectShape.HORIZ_ELLIPSE: ("height", "width"),
XSectShape.VERT_ELLIPSE: ("height", "width"),
XSectShape.ARCH: ("height", "width"),
XSectShape.IRREGULAR: ("transect_index",),
XSectShape.CUSTOM: ("height", "shape_curve_index"),
XSectShape.FORCE_MAIN: ("diameter", "roughness"),
XSectShape.STREET_XSECT: ("street_index",),
XSectShape.DUMMY: (),
}
def _resolve_geom_labels(shape: int) -> tuple[str, ...]:
"""Return the ordered label tuple for *shape*, falling back to generic names."""
labels = _GEOM_LABELS.get(shape)
if labels is not None:
return labels
return ("geom1", "geom2", "geom3", "geom4")
# ---------------------------------------------------------------------------
# CrossSection dataclass
# ---------------------------------------------------------------------------
[docs]
@dataclass(frozen=True)
class CrossSection:
"""Structured cross-section geometry returned by :meth:`Links.get_xsect_info`.
All four ``geom`` values are always present; unused parameters are ``0.0``.
Use :attr:`geom_labels` to get a ``{label: value}`` dict that filters out
unused (zero) parameters and names each one meaningfully.
@ivar shape: Integer cross-section shape code (see :class:`XSectShape`).
@ivar shape_name: Human-readable name, e.g. ``"CIRCULAR"``.
@ivar geom1: First geometry parameter (meaning depends on shape).
@ivar geom2: Second geometry parameter (or 0.0 if unused).
@ivar geom3: Third geometry parameter (or 0.0 if unused).
@ivar geom4: Fourth geometry parameter (or 0.0 if unused).
"""
shape: int
shape_name: str
geom1: float
geom2: float
geom3: float
geom4: float
[docs]
@classmethod
def from_raw(cls, shape: int, geom1: float, geom2: float, geom3: float,
geom4: float) -> "CrossSection":
"""Build from the raw ``(shape, geom1..geom4)`` the engine reports.
@param shape: Integer shape code (see :class:`XSectShape`).
@param geom1: First geometry parameter.
@param geom2: Second geometry parameter.
@param geom3: Third geometry parameter.
@param geom4: Fourth geometry parameter.
@rtype: CrossSection
"""
code = int(shape)
return cls(
shape=code,
shape_name=_XSECT_SHAPE_NAMES.get(code, f"UNKNOWN({code})"),
geom1=float(geom1),
geom2=float(geom2),
geom3=float(geom3),
geom4=float(geom4),
)
@property
def geom_labels(self) -> dict[str, float]:
"""Return ``{label: value}`` for the geometry parameters of this shape.
Only parameters meaningful for this shape are included (trailing
unused zeros are omitted). Example for a 1.2 m diameter circular pipe::
{"diameter": 1.2}
@rtype: dict[str, float]
"""
labels = _resolve_geom_labels(self.shape)
values = (self.geom1, self.geom2, self.geom3, self.geom4)
return {label: values[i] for i, label in enumerate(labels)}
def __repr__(self) -> str:
labels = self.geom_labels
params = ", ".join(f"{k}={v}" for k, v in labels.items())
return f"CrossSection({self.shape_name}, {params})"