Some checks failed
CI / lint (pull_request) Has been cancelled
CI / type (pull_request) Has been cancelled
CI / test-headless (macos-latest, 3.10) (pull_request) Has been cancelled
CI / test-headless (macos-latest, 3.11) (pull_request) Has been cancelled
CI / test-headless (macos-latest, 3.12) (pull_request) Has been cancelled
CI / test-headless (ubuntu-latest, 3.10) (pull_request) Has been cancelled
CI / test-headless (ubuntu-latest, 3.11) (pull_request) Has been cancelled
CI / test-headless (ubuntu-latest, 3.12) (pull_request) Has been cancelled
CI / test-headless (windows-latest, 3.10) (pull_request) Has been cancelled
CI / test-headless (windows-latest, 3.11) (pull_request) Has been cancelled
CI / test-headless (windows-latest, 3.12) (pull_request) Has been cancelled
CI / test-gui (pull_request) Has been cancelled
CI / test-integration (macos-latest) (pull_request) Has been cancelled
CI / test-integration (ubuntu-latest) (pull_request) Has been cancelled
CI / test-integration (windows-latest) (pull_request) Has been cancelled
Auto-fixes (UP037, UP038, F401, I001, SIM105, RUF100) plus hand-fixes: drop the now-unused `Union` imports, annotate mutable class attributes with ClassVar, raise ValidationError instead of blind Exception in the zero-length test, delete two dead locals, and bind the per-iteration grid-builder closures to their loop variables (B023).
113 lines
3.9 KiB
Python
113 lines
3.9 KiB
Python
"""Integration test for the Simply Supported Beam (Quad) example (Ex 6.4)."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import tempfile
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
pytest.importorskip("openseespy")
|
||
|
||
from otko.services import load_project, save_project
|
||
from otko.services.opensees_runner import OpenSeesRunner
|
||
|
||
pytestmark = pytest.mark.slow
|
||
|
||
|
||
def test_beam_quad_2d_midspan_deflection(tmp_path) -> None: # type: ignore[no-untyped-def]
|
||
"""10-step LoadControl on a 16×4 plane-stress quad mesh — reference
|
||
midspan deflection ≈ 0.394 in (matches the OpenSees Wiki Ex 6.4 result).
|
||
Validates the new QuadElement emit path for ndm=2, ndf=2 models.
|
||
"""
|
||
from examples.beam_quad_2d import (
|
||
_mid_bottom_node_id,
|
||
_mid_top_node_id,
|
||
build_beam_quad_2d,
|
||
)
|
||
|
||
proj = build_beam_quad_2d()
|
||
proj.validate_references()
|
||
|
||
path = tmp_path / "beam_quad.osmodel"
|
||
save_project(proj, path)
|
||
reloaded = load_project(path)
|
||
reloaded.validate_references()
|
||
|
||
result = OpenSeesRunner(reloaded).run(reloaded.analyses[0])
|
||
|
||
l1 = _mid_bottom_node_id()
|
||
l2 = _mid_top_node_id()
|
||
uy_bottom = result.node_disp[l1][-1, 1]
|
||
uy_top = result.node_disp[l2][-1, 1]
|
||
|
||
# Reference (pure openseespy with the same mesh): Uy ≈ -0.3943 in at
|
||
# both midspan nodes after 10 steps of LoadControl (load factor = 10).
|
||
assert uy_bottom == pytest.approx(-0.3943, abs=1e-3)
|
||
assert uy_top == pytest.approx(-0.3943, abs=1e-3)
|
||
|
||
# Top and bottom midspan deflections differ only by Poisson-contraction
|
||
# through the beam depth — a few microinches.
|
||
assert abs(uy_top - uy_bottom) < 1e-4
|
||
|
||
# By symmetry, left and right support reactions must balance the two
|
||
# 10-kip midspan loads (total -20 kip vertical).
|
||
assert reloaded.nodes[0].id == 1 # left pin
|
||
reaction_sum_fy = sum(
|
||
result.node_reaction[nid][-1, 1]
|
||
for nid in (1, 17) # node 17 = (L, 0) = roller
|
||
)
|
||
assert reaction_sum_fy == pytest.approx(20.0, abs=1e-6)
|
||
|
||
|
||
def test_beam_quad_2d_free_vibration_chain(tmp_path) -> None: # type: ignore[no-untyped-def]
|
||
"""TransientCase with preload + remove_patterns + mode-1 Rayleigh.
|
||
|
||
Confirms the chained-analysis support: after the Static preload,
|
||
dropping pattern 1 leaves the beam oscillating freely about Uy=0
|
||
with 2% stiffness-proportional damping, and the amplitude decays
|
||
monotonically in the envelope sense.
|
||
"""
|
||
from examples.beam_quad_2d import (
|
||
_mid_bottom_node_id,
|
||
build_beam_quad_2d,
|
||
)
|
||
|
||
proj = build_beam_quad_2d()
|
||
proj.validate_references()
|
||
|
||
path = tmp_path / "beam_quad.osmodel"
|
||
save_project(proj, path)
|
||
reloaded = load_project(path)
|
||
reloaded.validate_references()
|
||
|
||
results_dir = Path(tempfile.mkdtemp(prefix="beamvib_"))
|
||
r = OpenSeesRunner(reloaded).run(reloaded.analyses[1], results_dir=results_dir)
|
||
|
||
import h5py
|
||
|
||
with h5py.File(r.h5_path) as f:
|
||
t = f["time"][:]
|
||
u = f[f"nodes/{_mid_bottom_node_id()}/disp"][:, 1]
|
||
|
||
# Transient starts at t=0 (loadConst reset) and runs 1500 × 0.5 = 750 s.
|
||
assert t[0] == pytest.approx(0.5, abs=0.01)
|
||
assert t[-1] == pytest.approx(750.0, abs=0.5)
|
||
|
||
# Initial displacement inherits the ~0.394-in static sag. The first
|
||
# sample is one transient step in (dt = 0.5 s), so we're a hair away
|
||
# from the peak but still well inside the first half-cycle.
|
||
assert u[0] == pytest.approx(-0.394, abs=0.02)
|
||
|
||
# Oscillatory motion: amplitude swings to both signs.
|
||
assert u.min() < -0.1
|
||
assert u.max() > +0.05
|
||
|
||
# 2 % stiffness-proportional damping over 750 s drives the amplitude
|
||
# to well below the initial sag — envelope (peak-to-peak across the
|
||
# final 100 samples) should be a small fraction of the initial |u|.
|
||
final_envelope = abs(u[-100:]).max()
|
||
assert final_envelope < 0.05, (
|
||
f"Final-envelope amplitude {final_envelope:.4f} in — "
|
||
"damping did not attenuate the free-vibration response."
|
||
)
|