otko/examples/beam_quad_2d.py
smillmorel 612936a00b
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: initial otko import
2026-09-08 02:12:15 -04:00

200 lines
6.8 KiB
Python
Raw Permalink 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.

"""Simply Supported Beam with 2D Solid (Quad) Elements — OpenSees Ex 6.4.
A 40" × 10" simply supported deep beam meshed with 16×4 plane-stress
quadrilateral elements under two centre-span vertical loads (one at the
top midspan, one at the bottom midspan, each 1 kip downward). The
load is ramped up over 10 LoadControl steps, giving a final midspan
deflection of ~0.394 in. A second case then *removes* the preload and
lets the beam free-vibrate with 2% stiffness-proportional damping for
1500 Newmark steps at dt = 0.5 s — reproducing the full Tcl flow.
Matches the Tcl walkthrough at:
https://opensees.berkeley.edu/wiki/index.php?title=Simply_supported_beam_modeled_with_two_dimensional_solid_elements
Model (kip, in, sec):
- Geometry: L = 40 in, H = 10 in, t = 1 in (plane stress)
- Material: ElasticIsotropic — E = 1000 ksi, ν = 0.25, ρ = 3.0
- Elements: 16 × 4 = 64 four-node quads (``quad`` variant,
``PlaneStress2D`` formulation)
- Supports: pin at node 1 (bottom-left), roller at node 17
(bottom-right, Uy restrained only)
- Loads: 2 × 1 kip ↓ at (20, 0) and (20, 10), ramped via
LoadControl from 0 to 10 over 10 steps
Analysis cases:
1. ``Static`` — 10-step LoadControl, midspan Uy ≈ -0.394 in
2. ``FreeVibration`` — Transient with preload = case 1, remove
pattern 1, Newmark (γ=0.5, β=0.25), dt = 0.5 s, 1500 steps,
βK auto-computed from 2% damping at the 1st mode.
"""
from __future__ import annotations
from pathlib import Path
from otko.core import (
ElasticIsotropic,
LinearTimeSeries,
NodalLoad,
Node,
PlainLoadPattern,
Project,
ProjectMeta,
QuadElement,
StaticCase,
TransientCase,
UnitSystem,
)
from otko.services import load_project, save_project
# Geometry (inches).
L = 40.0
H = 10.0
THICKNESS = 1.0
# Mesh — nx must be even so a node lands at the midspan.
NX = 16
NY = 4
# Material (kip-in units).
E = 1000.0
NU = 0.25
RHO = 3.0
def _node_id(i: int, j: int) -> int:
"""Row-major node id (matching the Tcl ``block2D`` numbering).
i = 0..NX column index (along length), j = 0..NY row index (through depth).
"""
return j * (NX + 1) + i + 1
def _mid_bottom_node_id() -> int:
"""Node at (L/2, 0) — bottom flange midspan where one load attaches."""
return _node_id(NX // 2, 0)
def _mid_top_node_id() -> int:
"""Node at (L/2, H) — top flange midspan where the other load attaches."""
return _node_id(NX // 2, NY)
def _right_bottom_node_id() -> int:
"""Bottom-right corner — the roller support."""
return _node_id(NX, 0)
def build_beam_quad_2d() -> Project:
dx = L / NX
dy = H / NY
nodes: list[Node] = []
for j in range(NY + 1):
for i in range(NX + 1):
nid = _node_id(i, j)
if nid == 1:
restraint = (True, True, False, False, False, False) # pin
elif nid == _right_bottom_node_id():
restraint = (False, True, False, False, False, False) # roller (Uy only)
else:
restraint = (False,) * 6
nodes.append(Node(
id=nid, name=f"N{nid}",
coords=(i * dx, j * dy, 0.0),
restraint=restraint,
))
elements: list[QuadElement] = []
eid = 1
for j in range(NY):
for i in range(NX):
n1 = _node_id(i, j) # bottom-left
n2 = _node_id(i + 1, j) # bottom-right
n3 = _node_id(i + 1, j + 1) # top-right
n4 = _node_id(i, j + 1) # top-left (counter-clockwise)
elements.append(QuadElement(
id=eid, name=f"Q{eid}",
nodes=(n1, n2, n3, n4),
thickness=THICKNESS,
material_id=1,
variant="quad",
behaviour="PlaneStress2D",
))
eid += 1
return Project(
meta=ProjectMeta(
name="Simply Supported Beam — Quad Elements (OpenSees Ex 6.4)",
author="OpenSees Examples Manual",
description=(
f"{NX}×{NY} plane-stress quad mesh of a {L:.0f}×{H:.0f} deep "
"beam with two midspan point loads, LoadControl 0→10 in 10 steps."
),
units=UnitSystem.US_IN_KIP,
),
ndm=2, ndf=2,
nodes=nodes,
materials=[ElasticIsotropic(id=1, name="Elastic", E=E, nu=NU, rho=RHO)],
elements=elements,
time_series=[LinearTimeSeries(id=1, name="Ramp")],
load_patterns=[PlainLoadPattern(
id=1, name="MidspanLoad",
time_series_id=1,
nodal_loads=[
NodalLoad(node_id=_mid_bottom_node_id(),
forces=(0.0, -1.0, 0.0, 0.0, 0.0, 0.0)),
NodalLoad(node_id=_mid_top_node_id(),
forces=(0.0, -1.0, 0.0, 0.0, 0.0, 0.0)),
],
)],
analyses=[
StaticCase(
id=1, name="Static",
pattern_ids=[1],
n_steps=10, load_factor_increment=1.0,
system="ProfileSPD", constraints="Plain",
integrator="LoadControl", algorithm="Newton",
test="EnergyIncr", tolerance=1e-12, max_iter=10,
),
# Free-vibration continuation: runs case 1 to completion,
# drops the midspan load pattern, sets up 2% βK Rayleigh
# damping from the 1st mode, then integrates 1500 steps of
# Newmark (γ=0.5, β=0.25) at dt = 0.5 s.
TransientCase(
id=2, name="FreeVibration",
preload_case_ids=[1],
remove_patterns=[1],
pattern_ids=[],
dt=0.5, n_steps=1500,
system="BandGeneral", constraints="Plain",
integrator="Newmark", integrator_params=(0.5, 0.25),
algorithm="Newton",
test="EnergyIncr", tolerance=1e-12, max_iter=10,
rayleigh_mode1_damping=0.02,
),
],
)
def main() -> None:
project = build_beam_quad_2d()
project.validate_references()
print(f"Built '{project.meta.name}'")
print(f" ndm={project.ndm}, ndf={project.ndf}, "
f"units={project.meta.units.value}")
print(f" {len(project.nodes)} nodes, {len(project.elements)} quads")
print(f" Midspan loaded nodes: bottom={_mid_bottom_node_id()}, "
f"top={_mid_top_node_id()}")
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()