otko/examples/portal_pushover.py
smill f361fee969 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
2026-09-11 13:19:59 -04:00

189 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Portal frame with fiber-section plastic hinges and pushover analysis.
A single-bay portal frame where the column bases use FiberSections
(concrete core + rebar layers) wrapped in a SectionAggregator (for
torsion) and assigned to BeamWithHingesElements. The beam and column
tops remain elastic.
Demonstrates the full Phase 9 pipeline:
1. Define concrete + steel uniaxialMaterials
2. Build a FiberSection with a rectangular concrete patch + rebar layers
3. Wrap in a SectionAggregator that adds elastic torsion
4. Assign BeamWithHingesElements to the columns (hinges at base only)
5. Run a monotonic pushover to 0.15 m lateral displacement
6. View the pushover curve and force diagrams
Run:
python examples/portal_pushover.py
Produces ``examples/portal_pushover.osmodel``.
GUI walkthrough:
File → Open → portal_pushover.osmodel
Analyze → Cases → Run "Push-X"
Display → Show Pushover Curve → see yielding + hardening
Display → Show Force Diagram → M3 at the last pushover step
"""
from __future__ import annotations
from pathlib import Path
from otko.core import (
AggregatorDOF,
BeamWithHingesElement,
Concrete01,
ElasticBeamColumn,
ElasticSection,
ElasticUniaxial,
FiberSection,
LinearTimeSeries,
ModalCase,
NodalLoad,
Node,
PlainLoadPattern,
Project,
ProjectMeta,
PushoverCase,
RectangularPatch,
SectionAggregator,
Steel01,
StraightLayer,
UnitSystem,
)
from otko.services import load_project, save_project
def build_portal_pushover() -> Project:
"""Portal frame: 2 columns (hinge at base) + 1 beam (elastic)."""
H = 3.0 # story height
L = 6.0 # bay width
b = 0.30 # column width (y)
h = 0.40 # column depth (z)
cover = 0.04
return Project(
meta=ProjectMeta(name="Portal Pushover", 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=(L, 0.0, 0.0),
restraint=(True,) * 6),
Node(id=3, name="Top-L", coords=(0.0, 0.0, H),
mass=(10_000.0, 10_000.0, 10_000.0, 0.0, 0.0, 0.0)),
Node(id=4, name="Top-R", coords=(L, 0.0, H),
mass=(10_000.0, 10_000.0, 10_000.0, 0.0, 0.0, 0.0)),
],
materials=[
# Concrete: unconfined C30 (fpc negative by convention)
Concrete01(id=1, name="C30",
fpc=-30e6, epsc0=-0.002, fpcu=-0.0, epsU=-0.005),
# Steel rebar: S420 bilinear
Steel01(id=2, name="S420", Fy=420e6, E0=200e9, b=0.01),
# Elastic torsion spring
ElasticUniaxial(id=3, name="GJ-spring", E=80e9 * 3.6e-4),
],
sections=[
# FiberSection for the column hinge region:
# - Rectangular concrete patch (core, excluding cover for simplicity)
# - Bottom rebar layer (3 × Ø20 → A=3×314e-6=942e-6 m²)
# - Top rebar layer
FiberSection(
id=1, name="RC-Column",
patches=[RectangularPatch(
material_id=1,
n_fib_y=8, n_fib_z=10,
y_i=-b / 2 + cover, z_i=-h / 2 + cover,
y_j=b / 2 - cover, z_j=h / 2 - cover,
)],
layers=[
# Bottom rebar (z = -h/2 + cover)
StraightLayer(
material_id=2, n_bars=3, bar_area=314e-6,
y_start=-b / 2 + cover, z_start=-h / 2 + cover,
y_end=b / 2 - cover, z_end=-h / 2 + cover,
),
# Top rebar (z = +h/2 - cover)
StraightLayer(
material_id=2, n_bars=3, bar_area=314e-6,
y_start=-b / 2 + cover, z_start=h / 2 - cover,
y_end=b / 2 - cover, z_end=h / 2 - cover,
),
],
),
# Aggregator: FiberSection + elastic torsion
SectionAggregator(
id=2, name="Col-Agg",
section_id=1,
pairings=[AggregatorDOF(material_id=3, dof="T")],
),
# Elastic beam section (for the beam and column elastic interior)
ElasticSection(
id=3, name="W14x90",
E=200e9, A=0.017,
Iz=4.16e-4, Iy=1.29e-4,
G=80e9, J=2.04e-6,
),
],
elements=[
# Columns: BeamWithHinges at the base (hinge at end-i only;
# end-j uses the same section but with a tiny Lp so it stays
# essentially elastic there).
BeamWithHingesElement(
id=1, name="Col-L", nodes=(1, 3),
section_i_id=2, section_j_id=2,
lp_i=0.40, lp_j=0.01, # plastic hinge at base only
E=200e9, A=0.017,
Iz=4.16e-4, Iy=1.29e-4,
G=80e9, J=2.04e-6,
),
BeamWithHingesElement(
id=2, name="Col-R", nodes=(2, 4),
section_i_id=2, section_j_id=2,
lp_i=0.40, lp_j=0.01,
E=200e9, A=0.017,
Iz=4.16e-4, Iy=1.29e-4,
G=80e9, J=2.04e-6,
),
# Beam: elastic
ElasticBeamColumn(id=3, name="Beam", nodes=(3, 4), section_id=3),
],
time_series=[LinearTimeSeries(id=1, name="Ramp")],
load_patterns=[
PlainLoadPattern(
id=1, name="PushRef",
time_series_id=1,
nodal_loads=[NodalLoad(node_id=3, forces=(1.0, 0, 0, 0, 0, 0))],
),
],
analyses=[
PushoverCase(
id=1, name="Push-X",
pattern_ids=[1],
control_node=3, control_dof=1,
target_disp=0.15, step_size=0.0005,
base_nodes=[1, 2],
),
ModalCase(id=2, name="Modal-3", n_modes=3),
],
)
def main() -> None:
project = build_portal_pushover()
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()