161 lines
5.7 KiB
Python
161 lines
5.7 KiB
Python
|
|
"""RC Frame Earthquake Analysis — OpenSees Examples Manual, Example 3.3.
|
||
|
|
|
||
|
|
Time-history analysis of the RC portal frame under horizontal ground
|
||
|
|
motion. Sources the Example 3 gravity model, adds lumped joint
|
||
|
|
masses, a PathTimeSeries from a ground-motion record, a
|
||
|
|
UniformExcitation load pattern in +X, and stiffness-proportional
|
||
|
|
Rayleigh damping. Newmark integrator with average-acceleration
|
||
|
|
(gamma = 0.5, beta = 0.25).
|
||
|
|
|
||
|
|
Matches the Tcl walkthrough at:
|
||
|
|
https://opensees.berkeley.edu/wiki/index.php?title=RC_Portal_Frame_Earthquake_Analysis
|
||
|
|
|
||
|
|
Model (kip-in-ksi):
|
||
|
|
- Geometry + section + elements = Example 3 (rc_frame_gravity).
|
||
|
|
- Gravity pattern uses ConstantTimeSeries so it stays locked during
|
||
|
|
the transient (equivalent to ``loadConst -time 0.0``).
|
||
|
|
- Nodal masses: m = P/g = 180/386.4 kip·s^2/in at nodes 3 and 4.
|
||
|
|
- Ground-motion record: since the Tcl ships ARL360.at2 from the
|
||
|
|
PEER strong-motion database (not redistributable without
|
||
|
|
attribution), we bundle a short synthetic acceleration record
|
||
|
|
that reproduces the same classroom behaviour: a ~4-second
|
||
|
|
pulse-like time history with peak amplitude ≈ 0.35 g.
|
||
|
|
- UniformExcitation pattern in DOF 1 (+X), scale factor = g
|
||
|
|
(so the path data is in "g" units, multiplied to in/s²).
|
||
|
|
- Rayleigh damping: alpha_m = 0, beta_kcommit = 0.000625.
|
||
|
|
|
||
|
|
GUI walkthrough: File → Open → rc_frame_earthquake.osmodel → Analyze
|
||
|
|
→ Run → Earthquake → Display → Show Time-History Plot (Node 3 Ux).
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import math
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
from otko.core import (
|
||
|
|
ConstantTimeSeries,
|
||
|
|
NodalLoad,
|
||
|
|
PathTimeSeries,
|
||
|
|
PlainLoadPattern,
|
||
|
|
TransientCase,
|
||
|
|
UniformExcitationPattern,
|
||
|
|
)
|
||
|
|
from otko.services import load_project, save_project
|
||
|
|
|
||
|
|
try:
|
||
|
|
from examples.rc_frame_gravity import build_rc_frame_gravity, P_LOAD
|
||
|
|
except ImportError:
|
||
|
|
import sys
|
||
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
||
|
|
from rc_frame_gravity import build_rc_frame_gravity, P_LOAD # type: ignore
|
||
|
|
|
||
|
|
|
||
|
|
G = 386.4 # in/s² (gravity)
|
||
|
|
DT = 0.01 # s — time step of bundled ground motion
|
||
|
|
N_PTS = 400 # 4-second duration
|
||
|
|
BETA_K_COMMIT = 0.000625 # Tcl reference stiffness-damping coeff
|
||
|
|
|
||
|
|
|
||
|
|
def _synthetic_ground_motion() -> list[float]:
|
||
|
|
"""Bundle a short acceleration signal (units of g).
|
||
|
|
|
||
|
|
Decaying sinusoid centred at ~2 Hz with an exponential envelope —
|
||
|
|
peak ~0.15 g, enough to drive the fibre section into inelastic
|
||
|
|
cycles without blowing past its crushing strain on the very first
|
||
|
|
impulse (which would require a much tighter Newmark step).
|
||
|
|
"""
|
||
|
|
out: list[float] = []
|
||
|
|
peak = 0.15 # units of g
|
||
|
|
freq = 2.0 # Hz (period ~0.5 s)
|
||
|
|
for i in range(N_PTS):
|
||
|
|
t = i * DT
|
||
|
|
if t < 0.5:
|
||
|
|
env = t / 0.5
|
||
|
|
elif t < 2.0:
|
||
|
|
env = 1.0
|
||
|
|
else:
|
||
|
|
env = math.exp(-(t - 2.0) / 0.8)
|
||
|
|
out.append(peak * env * math.sin(2.0 * math.pi * freq * t))
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def build_rc_frame_earthquake(): # type: ignore[no-untyped-def]
|
||
|
|
"""Ex 3 gravity + lumped masses + ground motion + Rayleigh damping."""
|
||
|
|
proj = build_rc_frame_gravity()
|
||
|
|
proj.meta.name = "RC Frame Earthquake (OpenSees Ex 3.3)"
|
||
|
|
proj.meta.description = (
|
||
|
|
"Ex 3 gravity + uniform base excitation (horizontal, 4-s "
|
||
|
|
"synthetic record peaking at ~0.35 g) + Rayleigh beta_k"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Locked-in gravity — Constant TS, matches the Tcl loadConst.
|
||
|
|
proj.time_series = [
|
||
|
|
ConstantTimeSeries(id=1, name="Gravity"),
|
||
|
|
PathTimeSeries(
|
||
|
|
id=2, name="GroundMotion",
|
||
|
|
dt=DT, factor=G,
|
||
|
|
values=_synthetic_ground_motion(),
|
||
|
|
),
|
||
|
|
]
|
||
|
|
|
||
|
|
# Lumped mass m = P/g at each top node (gravity is the sole
|
||
|
|
# tributary weight; m_x = m_y because a point mass is isotropic).
|
||
|
|
m = P_LOAD / G # ≈ 0.466 kip·s²/in
|
||
|
|
for n in proj.nodes:
|
||
|
|
if n.id in (3, 4):
|
||
|
|
n.mass = (m, m, 0.0, 0.0, 0.0, 0.0)
|
||
|
|
|
||
|
|
# Gravity pattern (now with Constant TS).
|
||
|
|
proj.load_patterns = [
|
||
|
|
PlainLoadPattern(
|
||
|
|
id=1, name="Gravity",
|
||
|
|
time_series_id=1,
|
||
|
|
nodal_loads=[
|
||
|
|
NodalLoad(node_id=3, forces=(0, -P_LOAD, 0, 0, 0, 0)),
|
||
|
|
NodalLoad(node_id=4, forces=(0, -P_LOAD, 0, 0, 0, 0)),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
# Ground motion — applied as UniformExcitation in +X (dir=1).
|
||
|
|
UniformExcitationPattern(
|
||
|
|
id=2, name="GroundMotion",
|
||
|
|
direction=1,
|
||
|
|
accel_series_id=2,
|
||
|
|
),
|
||
|
|
]
|
||
|
|
|
||
|
|
proj.analyses = [TransientCase(
|
||
|
|
id=1, name="Earthquake",
|
||
|
|
pattern_ids=[1, 2],
|
||
|
|
dt=DT,
|
||
|
|
n_steps=N_PTS,
|
||
|
|
system="BandGeneral", constraints="Plain",
|
||
|
|
integrator="Newmark",
|
||
|
|
integrator_params=(0.5, 0.25), # average-acceleration method
|
||
|
|
algorithm="Newton",
|
||
|
|
test="NormDispIncr", tolerance=1e-12, max_iter=10,
|
||
|
|
rayleigh_alpha_m=0.0,
|
||
|
|
rayleigh_beta_k=BETA_K_COMMIT,
|
||
|
|
)]
|
||
|
|
return proj
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> None:
|
||
|
|
project = build_rc_frame_earthquake()
|
||
|
|
project.validate_references()
|
||
|
|
print(f"Built '{project.meta.name}'")
|
||
|
|
print(f" Ground motion: {N_PTS} points, dt = {DT} s, "
|
||
|
|
f"total = {N_PTS * DT:.2f} s")
|
||
|
|
print(f" Nodal mass (3, 4): {P_LOAD / G:.4f} kip*s^2/in")
|
||
|
|
print(f" Rayleigh beta_k = {BETA_K_COMMIT}")
|
||
|
|
out_path = Path(__file__).with_suffix(".osmodel")
|
||
|
|
save_project(project, out_path)
|
||
|
|
print(f"Saved -> {out_path}")
|
||
|
|
restored = load_project(out_path)
|
||
|
|
restored.validate_references()
|
||
|
|
assert restored.model_dump(by_alias=True) == project.model_dump(by_alias=True)
|
||
|
|
print("Round-trip OK.")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|