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
138
examples/portal_frame.py
Normal file
138
examples/portal_frame.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
"""Build a 3D portal frame with Static + Modal + Transient cases.
|
||||
|
||||
Run from the repository root:
|
||||
|
||||
python examples/portal_frame.py
|
||||
|
||||
Produces ``examples/portal_frame.osmodel`` — open it from the GUI's
|
||||
File → Open menu, then exercise:
|
||||
|
||||
- **Static** case → Display → Show Deformed Shape, Show Force Diagram
|
||||
- **Modal** case → Display → Animate Mode Shape
|
||||
- **Transient** case → Display → Time-History Plot, Hysteresis Plot
|
||||
|
||||
The model is a single-bay portal frame loaded laterally:
|
||||
|
||||
Top-L ──── Beam ──── Top-R z
|
||||
│ │ │
|
||||
│ │ │
|
||||
Col-L Col-R └── x
|
||||
│ │
|
||||
Base-L Base-R (y = 0; planar in x-z)
|
||||
|
||||
Mass is lumped at the top nodes so modal/transient solvers have
|
||||
non-singular mass matrices.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
from otko.core import (
|
||||
ElasticBeamColumn,
|
||||
ElasticSection,
|
||||
LinearTimeSeries,
|
||||
ModalCase,
|
||||
NodalLoad,
|
||||
Node,
|
||||
PathTimeSeries,
|
||||
PlainLoadPattern,
|
||||
Project,
|
||||
ProjectMeta,
|
||||
StaticCase,
|
||||
Steel01,
|
||||
TransientCase,
|
||||
UnitSystem,
|
||||
)
|
||||
from otko.services import load_project, save_project
|
||||
|
||||
|
||||
def _sine_pulse_factors() -> list[float]:
|
||||
"""One half-cycle sine over the first 0.5s, then zero for the rest of 2s."""
|
||||
n_pulse = 50 # 0.5 s @ 100 Hz
|
||||
n_total = 200
|
||||
return [math.sin(math.pi * i / n_pulse) if i < n_pulse else 0.0
|
||||
for i in range(n_total)]
|
||||
|
||||
|
||||
def build_portal_frame() -> Project:
|
||||
"""A two-column, one-beam steel portal frame with static + dynamic cases."""
|
||||
return Project(
|
||||
meta=ProjectMeta(name="Portal Frame", author="Ozan", units=UnitSystem.SI_M_N),
|
||||
ndm=3,
|
||||
ndf=6,
|
||||
nodes=[
|
||||
Node(id=1, name="Base-L", coords=(0.0, 0.0, 0.0), restraint=(True,) * 6),
|
||||
Node(id=2, name="Base-R", coords=(6.0, 0.0, 0.0), restraint=(True,) * 6),
|
||||
Node(id=3, name="Top-L", coords=(0.0, 0.0, 3.0),
|
||||
mass=(5000.0, 5000.0, 5000.0, 0.0, 0.0, 0.0)),
|
||||
Node(id=4, name="Top-R", coords=(6.0, 0.0, 3.0),
|
||||
mass=(5000.0, 5000.0, 5000.0, 0.0, 0.0, 0.0)),
|
||||
],
|
||||
materials=[
|
||||
Steel01(id=1, name="S420", Fy=420e6, E0=200e9, b=0.01),
|
||||
],
|
||||
sections=[
|
||||
ElasticSection(
|
||||
id=1, name="W14x90",
|
||||
E=200e9, A=0.017,
|
||||
Iz=4.16e-4, Iy=1.29e-4,
|
||||
G=80e9, J=2.04e-6,
|
||||
),
|
||||
],
|
||||
elements=[
|
||||
ElasticBeamColumn(id=1, name="Col-L", nodes=(1, 3), section_id=1),
|
||||
ElasticBeamColumn(id=2, name="Col-R", nodes=(2, 4), section_id=1),
|
||||
ElasticBeamColumn(id=3, name="Beam", nodes=(3, 4), section_id=1),
|
||||
],
|
||||
time_series=[
|
||||
LinearTimeSeries(id=1, name="Ramp"),
|
||||
PathTimeSeries(
|
||||
id=2, name="SineGust",
|
||||
values=_sine_pulse_factors(),
|
||||
dt=0.01,
|
||||
),
|
||||
],
|
||||
load_patterns=[
|
||||
# Pattern 1: 50 kN lateral push at top-left for Static.
|
||||
PlainLoadPattern(
|
||||
id=1, name="Lateral",
|
||||
time_series_id=1,
|
||||
nodal_loads=[NodalLoad(node_id=3, forces=(50_000.0, 0, 0, 0, 0, 0))],
|
||||
),
|
||||
# Pattern 2: 100 kN sine pulse at top-left for Transient.
|
||||
PlainLoadPattern(
|
||||
id=2, name="SineGustPattern",
|
||||
time_series_id=2,
|
||||
nodal_loads=[NodalLoad(node_id=3, forces=(100_000.0, 0, 0, 0, 0, 0))],
|
||||
),
|
||||
],
|
||||
analyses=[
|
||||
StaticCase(id=1, name="Linear-Static", pattern_ids=[1]),
|
||||
ModalCase(id=2, name="Modal-3", n_modes=3),
|
||||
TransientCase(
|
||||
id=3, name="Sine-Gust-2s",
|
||||
pattern_ids=[2],
|
||||
dt=0.01, n_steps=200, # 2 seconds @ 100 Hz
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
project = build_portal_frame()
|
||||
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()
|
||||
Loading…
Reference in a new issue