200 lines
6.8 KiB
Python
200 lines
6.8 KiB
Python
|
|
"""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()
|