feat: named case-result load combinations with full GUI support
Snapshots the current development tree, headlined by proper load combinations (user request): a reusable LoadCombination entity of weighted completed static-case results (e.g. 1.2xDead + 1.6xLive). - core: LoadCombination/LoadCombinationItem entities, Project integration (lookup, unique ids, reference validation) - services: combinations.py (linear superposition + envelope), exported via services __init__ - commands: undoable Add/Delete/Update for combinations - GUI: Load Combinations manager dialog, Run-dialog evaluation, envelope display in Results panel, Combinations tab in Table dock - tests: unit coverage (validation, math, error paths) + integration superposition check vs a single factored run
This commit is contained in:
commit
f361fee969
560 changed files with 178701 additions and 0 deletions
161
examples/rc_frame_earthquake.py
Normal file
161
examples/rc_frame_earthquake.py
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
"""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()
|
||||
Loading…
Reference in a new issue