otko/tests/integration/test_runner_imposed_motion.py
smillmorel 61b850bf46
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
style: resolve the remaining ruff findings
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).
2026-09-16 13:20:36 -04:00

138 lines
4.7 KiB
Python

"""ImposedSupportMotion physics: equivalence with a uniform-excitation twin.
A grounded zeroLength isolator (elastic axial spring, ``-doRayleigh``) with a
tip mass, betaKinit Rayleigh damping — the wire-rope benchmark's damping
topology in miniature. The support is driven by a RAMPED sine displacement
d(t) (smooth start: d(0) = d'(0) = 0, so no startup velocity impulse), and the
relative response must match a UniformExcitation run whose accel series is the
ANALYTIC d''(t): OpenSees applies -m*a there, which is exactly the imposed-
motion experiment's relative-coordinate forcing -m*d''. Any residual is
mechanism error — in particular a support velocity that fails to reach the
betaKinit damping coupling of the -doRayleigh element would show up here at
the tens-of-percent level (that failure mode is real: a Plain-pattern ``sp``
under the Transformation handler exhibits it).
"""
from __future__ import annotations
import math
import numpy as np
import pytest
ops = pytest.importorskip("openseespy.opensees")
h5py = pytest.importorskip("h5py")
from otko.core import (
ElasticUniaxial,
ImposedSupportMotionPattern,
Node,
PathTimeSeries,
Project,
TransientCase,
UniformExcitationPattern,
ZeroLengthElement,
)
from otko.services import OpenSeesRunner
pytestmark = pytest.mark.slow
W = 1.57 # drive (rad/s)
DT_SERIES = 0.01
NPTS = 1601 # 16 s
DT = 0.005
N_STEPS = 3200
K = math.pi**2 # with m=1: system omega = pi (T = 2 s)
BETA_K_INIT = 0.05 # exaggerated so a damping-coupling error is loud
T_RAMP = 8.0
def _series() -> tuple[list[float], list[float]]:
t = np.arange(NPTS) * DT_SERIES
ramp = np.where(t < T_RAMP, 0.5 * (1 - np.cos(math.pi * t / T_RAMP)), 1.0)
dramp = np.where(t < T_RAMP, 0.5 * (math.pi / T_RAMP) * np.sin(math.pi * t / T_RAMP), 0.0)
ddramp = np.where(t < T_RAMP, 0.5 * (math.pi / T_RAMP) ** 2 * np.cos(math.pi * t / T_RAMP), 0.0)
s, c = np.sin(W * t), np.cos(W * t)
disp = 0.1 * ramp * s
acc = 0.1 * (ddramp * s + 2 * dramp * W * c - ramp * W * W * s) # analytic d''
return disp.tolist(), acc.tolist()
def _project(pattern, series_values) -> Project: # type: ignore[no-untyped-def]
return Project(
ndm=3,
ndf=6,
nodes=[
Node(id=1, coords=(0, 0, 0), restraint=(True,) * 6),
Node(
id=2,
coords=(0, 0, 0),
restraint=(False, True, True, True, True, True),
mass=(1.0, 0.0, 0.0, 0.0, 0.0, 0.0),
),
],
materials=[ElasticUniaxial(id=1, E=K)],
elements=[
ZeroLengthElement(
id=1,
nodes=(1, 2),
material_ids=(1,),
dofs=(1,),
do_rayleigh=True,
),
],
# use_last: the final analyze step lands a float-accumulation hair past
# the record end — without it the imposed support snaps to 0 there.
time_series=[PathTimeSeries(id=1, dt=DT_SERIES, values=series_values, use_last=True)],
load_patterns=[pattern],
analyses=[
TransientCase(
id=1,
name="drive",
pattern_ids=[9],
dt=DT,
n_steps=N_STEPS,
constraints="Transformation",
algorithm="Newton",
test="NormDispIncr",
tolerance=1e-10,
max_iter=100,
rayleigh_beta_k_init=BETA_K_INIT,
)
],
)
def _run(project: Project, tmp_path) -> tuple[np.ndarray, np.ndarray]: # type: ignore[no-untyped-def]
runner = OpenSeesRunner(project)
results = runner.run(project.analyses[0], tmp_path)
with h5py.File(results.h5_path, "r") as f:
u1 = np.asarray(f["nodes/1/disp"])[:, 0]
u2 = np.asarray(f["nodes/2/disp"])[:, 0]
return u1, u2
def test_imposed_disp_matches_uniform_excitation_twin(tmp_path) -> None: # type: ignore[no-untyped-def]
disp, acc = _series()
imposed = ImposedSupportMotionPattern(
id=9,
direction=1,
disp_series_id=1,
node_ids=[1],
)
ground, absolute = _run(_project(imposed, disp), tmp_path / "imposed")
# The support tracked the record (spot-check the steady peak).
assert np.max(np.abs(ground)) == pytest.approx(0.1, rel=1e-3)
uniform = UniformExcitationPattern(id=9, direction=1, accel_series_id=1)
_, reference = _run(_project(uniform, acc), tmp_path / "uniform")
relative = absolute - ground
peak = np.max(np.abs(reference))
residual = np.max(np.abs(relative - reference))
# Round-3 mechanism experiment: ~0.006% of peak for this mechanism;
# a broken velocity coupling sits at ~30%.
assert residual < 0.001 * peak