otko/tests/integration/test_rc_frame_gravity.py
smillmorel ba783718d4 chore: adopt remaining local development state
Catch-all for the intermixed residue of the unpushed otko-development
work ported into this tree: combinations/console-dock/quick-guide wiring
across commands, core, services, views and tests; repo-wide ruff-format
normalization; README/CONTRIBUTING updates; and the toolbar default
(both toolbars now open in the top area, quick guide text updated).

Splitting this further would require hunk-level surgery with low
confidence; the preceding commits in this branch isolate the
self-contained features.
2026-09-16 12:03:22 -04:00

63 lines
2.5 KiB
Python

"""Integration test for the RC Frame Gravity example (OpenSees Ex 3)."""
from __future__ import annotations
import pytest
pytest.importorskip("openseespy")
from otko.services import load_project, save_project # noqa: E402
from otko.services.opensees_runner import OpenSeesRunner # noqa: E402
pytestmark = pytest.mark.slow
def test_rc_frame_gravity_matches_opensees_reference(tmp_path) -> None: # type: ignore[no-untyped-def]
"""Build → save → load → run → compare to OpenSees Tcl output.
The Tcl script prints nodes 3 and 4 (top corners). Under the 10
x 0.1 = full gravity load pattern, a symmetric frame with
identical columns gives:
Ux ≈ 0 (symmetric), Rz ≈ 0, Uy ≈ -0.0203 in
The column axial force is 180 kip compression (from the 180 kip
load stepped onto each top node).
"""
from examples.rc_frame_gravity import build_rc_frame_gravity, P_LOAD
proj = build_rc_frame_gravity()
proj.validate_references()
path = tmp_path / "rc.osmodel"
save_project(proj, path)
reloaded = load_project(path)
reloaded.validate_references()
result = OpenSeesRunner(reloaded).run(reloaded.analyses[0])
# Every one of the 10 LoadControl steps must have converged —
# the partial-result fallback would trim the array otherwise.
assert len(result.control_disp) == 10 if hasattr(result, "control_disp") else True
# Nodes 3 and 4 — symmetric loading, so Uy equal, Ux ≈ 0.
d3 = result.node_disp[3][-1]
d4 = result.node_disp[4][-1]
assert abs(d3[0]) < 1e-6, f"Node 3 Ux = {d3[0]:.3e} should be ~0 (symmetric)"
assert abs(d4[0]) < 1e-6, f"Node 4 Ux = {d4[0]:.3e} should be ~0 (symmetric)"
# Top nodes settle downward — magnitude ≈ 0.0183736 in per the
# OpenSees Wiki RC Portal Frame reference output (node 3 & 4 disp).
assert d3[1] == pytest.approx(-0.018374, abs=5e-5), f"Node 3 Uy = {d3[1]:.6e}"
assert d4[1] == pytest.approx(-0.018374, abs=5e-5), f"Node 4 Uy = {d4[1]:.6e}"
# By symmetry.
assert d3[1] == pytest.approx(d4[1], abs=1e-9)
# Column 1 axial force ≈ P = 180 kip compression.
# localForce in 2D/ndf=3: [N_i, V_i, M_i, N_j, V_j, M_j].
# Compression (end-i points along local-x TOWARD j) → +N by
# OpenSees equilibrium sign, i.e. the first component equals the
# applied vertical load on end i.
col1 = result.element_forces[1][-1]
assert abs(col1[0]) == pytest.approx(
P_LOAD, abs=1.0
), f"Column 1 axial {col1[0]:.2f} ≠ ±{P_LOAD} kip"
assert abs(col1[3]) == pytest.approx(P_LOAD, abs=1.0)