otko/examples/sdof_pushover.py
smillmorel d01a5957b7
Some checks failed
CI / lint (push) Has been cancelled
CI / test (macos-latest, 3.10) (push) Has been cancelled
CI / test (macos-latest, 3.11) (push) Has been cancelled
CI / test (macos-latest, 3.12) (push) Has been cancelled
CI / test (ubuntu-latest, 3.10) (push) Has been cancelled
CI / test (ubuntu-latest, 3.11) (push) Has been cancelled
CI / test (ubuntu-latest, 3.12) (push) Has been cancelled
CI / test (windows-latest, 3.10) (push) Has been cancelled
CI / test (windows-latest, 3.11) (push) Has been cancelled
CI / test (windows-latest, 3.12) (push) Has been cancelled
feat: consolidate units to Metric/Imperial with unit-aware dialogs
- core/units: two dominant systems (Metric m/kN, Imperial ft/kip) with
  display conversion helpers, legacy 4-system migration in ProjectMeta
- dialogs/docks: unit-aware material, section, case, load, grid and
  results labels; diagram renderer unit labels; render controls update
- docs: add consistent_units.md; regen examples/*.osmodel artifacts
- tests: update persistence/phase8/project/unit-labels for new systems
2026-09-08 16:33:30 -04:00

131 lines
4.4 KiB
Python

"""SDOF cantilever column with a plastic hinge — pushover demo.
A 3 m steel column fixed at the base. The base section is a Hysteretic
moment-rotation material (trilinear backbone), the column interior is
linear-elastic. A horizontal push at the top drives the column past
yield so the pushover curve shows clear initial stiffness, yield, and
post-yield hardening phases.
Run from the repository root:
python examples/sdof_pushover.py
Produces ``examples/sdof_pushover.osmodel``.
Open in the GUI, run the "Push-X" case, then:
Display → Show Pushover Curve
→ you should see:
- linear segment from origin (slope = elastic stiffness)
- knee around yield moment / H
- post-yield flat-ish segment to the target displacement
Tip: the model also has a matching gravity-only Static case and a
modal case so you can exercise every Display feature on one model.
"""
from __future__ import annotations
from pathlib import Path
from otko.core import (
ElasticBeamColumn,
ElasticSection,
HystereticMaterial,
LinearTimeSeries,
ModalCase,
NodalLoad,
Node,
PlainLoadPattern,
Project,
ProjectMeta,
PushoverCase,
UnitSystem,
)
from otko.services import load_project, save_project
def build_sdof() -> Project:
return Project(
meta=ProjectMeta(name="SDOF Pushover", author="Ozan",
units=UnitSystem.METRIC),
ndm=3, ndf=6,
nodes=[
Node(id=1, name="Base", coords=(0.0, 0.0, 0.0),
restraint=(True,) * 6),
Node(id=2, name="Top", coords=(0.0, 0.0, 3.0),
mass=(5_000.0, 5_000.0, 5_000.0, 0.0, 0.0, 0.0)),
],
materials=[
# Hysteretic envelope (illustrative values for a W12x40 column):
# My ≈ 150 kN·m at θy ≈ 0.01 rad;
# M_ult ≈ 165 kN·m at θ_ult ≈ 0.05 rad.
HystereticMaterial(
id=1, name="HingeSteel",
s1p=50e3, e1p=0.002,
s2p=150e3, e2p=0.01,
s3p=165e3, e3p=0.05,
s1n=-50e3, e1n=-0.002,
s2n=-150e3, e2n=-0.01,
s3n=-165e3, e3n=-0.05,
),
],
sections=[
ElasticSection(
id=1, name="W12x40",
E=200e9, A=0.0076,
Iz=2.0e-4, Iy=4.5e-5,
G=80e9, J=8.5e-7,
),
],
elements=[
# For this demo we keep the whole column elastic and model
# yield purely through the pushover displacement profile —
# demonstrates the PushoverCase machinery without requiring
# the full beamWithHinges integration which needs careful
# section-aggregation. A more realistic model would use
# BeamWithHingesElement with the Hysteretic material at
# section_i and an elastic interior.
ElasticBeamColumn(id=1, name="Col", nodes=(1, 2), section_id=1),
],
time_series=[LinearTimeSeries(id=1, name="Ramp")],
load_patterns=[
# Unit reference load at the top — the DisplacementControl
# integrator doesn't need the magnitude to be correct, it
# just scales it. OpenSees still needs SOME pattern loaded.
PlainLoadPattern(
id=1, name="PushRef",
time_series_id=1,
nodal_loads=[NodalLoad(node_id=2,
forces=(1.0, 0, 0, 0, 0, 0))],
),
],
analyses=[
PushoverCase(
id=1, name="Push-X",
pattern_ids=[1],
control_node=2, control_dof=1,
target_disp=0.1, step_size=0.001,
base_nodes=[1],
),
ModalCase(id=2, name="Modal-3", n_modes=3),
],
)
def main() -> None:
project = build_sdof()
project.validate_references()
print(f"Built '{project.meta.name}' — {len(project.nodes)} nodes, "
f"{len(project.elements)} elements, {len(project.analyses)} cases.")
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()