Source code for civilpy.structural.stm_topology.problem

#  CivilPy
#  Copyright (C) 2019-2026 Dane Parks
#
#  SPDX-License-Identifier: MIT
#  See the LICENSE file in the project root for full license text.

"""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 :class:`DRegionProblem` is the boundary polygon of the
concrete region plus its thickness/material and the supports and loads, and it is
consumed by :mod:`civilpy.structural.stm_topology.simp` (the SIMP optimizer) and
:mod:`~civilpy.structural.stm_topology.extract` (truss extraction).

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

from __future__ import annotations

import math
from dataclasses import dataclass, field


[docs] @dataclass class Material: """Concrete material for the optimization and the capacity checks.""" f_c: float = 5.0 # ksi, compressive strength E: float | None = None # ksi, elastic modulus (defaulted from f_c) nu: float = 0.2 # Poisson's ratio unit_weight: float = 0.145 # kcf, for the AASHTO 5.4.2.4 modulus default def __post_init__(self): if self.E is None: # AASHTO LRFD 5.4.2.4-1: Ec = 120000 K1 wc^2.0 f'c^0.33 (ksi) self.E = 120000.0 * (self.unit_weight ** 2.0) * (self.f_c ** 0.33)
[docs] @dataclass class Support: """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.""" x: float y: float fix_x: bool = True fix_y: bool = True bearing: float | None = None
[docs] @dataclass class Load: """A factored point load (kips) applied on the region. Direction is the ``(fx, fy)`` vector; ``bearing`` is the in-plane bearing width (feet).""" x: float y: float fx: float = 0.0 fy: float = 0.0 bearing: float | None = None @property def magnitude(self) -> float: return math.hypot(self.fx, self.fy)
[docs] @dataclass class DRegionProblem: """A concrete D-region to be filled with an optimized strut-and-tie truss. Parameters ---------- boundary: Outer polygon as ``[(x, y), ...]`` in feet (closed automatically). thickness: Out-of-plane thickness ``b`` in feet (plane-stress depth). material: :class:`Material`. supports, loads: Boundary conditions, shared with the drawn-truss workflow. voids, solids: Optional inner polygons: ``voids`` are mandatory holes (passive empty), ``solids`` are keep-in zones (passive full). vol_frac: Target fraction of the region to keep as concrete (SIMP constraint). """ boundary: list[tuple[float, float]] thickness: float material: Material = field(default_factory=Material) supports: list[Support] = field(default_factory=list) loads: list[Load] = field(default_factory=list) voids: list[list[tuple[float, float]]] = field(default_factory=list) solids: list[list[tuple[float, float]]] = field(default_factory=list) vol_frac: float = 0.3 # ── convenience builders ──────────────────────────────────────────────
[docs] def add_support(self, x, y, fix_x=True, fix_y=True, bearing=None): self.supports.append(Support(x, y, fix_x, fix_y, bearing)) return self
[docs] def add_load(self, x, y, fx=0.0, fy=0.0, bearing=None): self.loads.append(Load(x, y, fx, fy, bearing)) return self
[docs] @classmethod def rectangle(cls, width, height, thickness, origin=(0.0, 0.0), **kwargs): """A rectangular region with its lower-left corner at ``origin``.""" x0, y0 = origin boundary = [(x0, y0), (x0 + width, y0), (x0 + width, y0 + height), (x0, y0 + height)] return cls(boundary=boundary, thickness=thickness, **kwargs)
# ── geometry helpers ──────────────────────────────────────────────────
[docs] def bounds(self) -> tuple[float, float, float, float]: xs = [p[0] for p in self.boundary] ys = [p[1] for p in self.boundary] return min(xs), min(ys), max(xs), max(ys)
# ── Rhino front end ───────────────────────────────────────────────────
[docs] @classmethod def from_3dm(cls, path, **kwargs): """Read a tagged Rhino ``.3dm`` authored with the *region* workflow (a ``stm.kind=region`` closed curve plus supports and loads). Thin wrapper over :func:`civilpy.structural.rhino_stm.problem_from_3dm` (needs the optional ``rhino3dm`` dependency).""" from civilpy.structural.rhino_stm import problem_from_3dm return problem_from_3dm(path, **kwargs)
# ── pipeline entry point ──────────────────────────────────────────────
[docs] def solve(self, **kwargs): """Run the full pipeline: mesh → SIMP → extract truss → solve → check. Returns a :class:`civilpy.structural.stm_topology.pipeline.STMResult`.""" from civilpy.structural.stm_topology.pipeline import optimize_to_stm return optimize_to_stm(self, **kwargs)