otko/examples/basic_truss.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

163 lines
5.4 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.

"""Basic Truss Example — OpenSees Examples Manual, Example 1 (exact geometry).
Three-bar pin-jointed truss in 2D under a nodal load at the crown.
Geometry as in the OpenSees Tcl script (units: kip, in)::
node 1 0.0 0.0 node 4 72.0 96.0
node 2 144.0 0.0 (crown — loaded here)
node 3 168.0 0.0
─── Note the asymmetry:
Base nodes: node 2 at x=144"
(0, 0) ───────── (144, 0) ── (168, 0) node 3 at x=168"
↑ ↑ (only 24" apart!)
This is an asymmetric three-bar truss reaching to a single crown
joint (node 4) at the top. The three bars have different lengths
and two different areas (bar 1 = 10 in², bars 2 and 3 = 5 in²).
Loads (at node 4): Fx = +100 kip, Fy = -50 kip.
Material: Elastic, E = 3000 ksi.
Analytical solution in kip-in units (verified by OpenSees Tcl run):
u_x(node 4) ≈ +0.530 in
u_y(node 4) ≈ -0.178 in
In OTKO we always work in SI internally (m, N), so we
convert inches → metres (×0.0254), kips → newtons (×4448.22) and
ksi → pascals (×6.895e6). The converted model gives the same
dimensionless solution when you multiply back by the inverse
conversion.
Produces ``examples/basic_truss.osmodel``.
"""
from __future__ import annotations
from pathlib import Path
from otko.core import (
CoordinateGridSystem,
ElasticUniaxial,
GridSystem,
LinearTimeSeries,
NodalLoad,
Node,
PlainLoadPattern,
Project,
ProjectMeta,
StaticCase,
TrussElement,
UnitSystem,
make_grid_lines,
)
from otko.services import load_project, save_project
# Unit conversions (imperial → SI).
IN_TO_M = 0.0254
KIP_TO_N = 4448.2216
KSI_TO_PA = 6.895e6
def build_basic_truss() -> Project:
"""Three-bar planar truss — Example 1 of the OpenSees user manual.
Geometry in inches → metres; material E in ksi → Pa; areas in
in² → m²; loads in kips → N. The SI model is a dimensionless
scale of the original kip-in model, so the OpenSees solve gives
the same shape / ratio of displacements.
"""
# Node coordinates in inches (from Tcl) — converted to metres.
x1, y1 = 0.0, 0.0
x2, y2 = 144.0, 0.0
x3, y3 = 168.0, 0.0
x4, y4 = 72.0, 96.0
# Areas in in² — converted to m².
a1 = 10.0 * IN_TO_M ** 2
a2 = 5.0 * IN_TO_M ** 2
a3 = 5.0 * IN_TO_M ** 2
# Loads in kips — converted to N.
fx = 100.0 * KIP_TO_N
fy = -50.0 * KIP_TO_N
# Material E in ksi → Pa.
e = 3000.0 * KSI_TO_PA
# Grid that covers all four nodes (for the canvas overlay).
x_ords = [x1 * IN_TO_M, x4 * IN_TO_M, x2 * IN_TO_M, x3 * IN_TO_M]
y_ords = [y1 * IN_TO_M, y4 * IN_TO_M]
return Project(
meta=ProjectMeta(
name="Basic Truss",
author="OpenSees Examples Manual - Example 1",
description="3-bar asymmetric planar truss, linear static analysis.",
units=UnitSystem.SI_M_N,
),
ndm=2, ndf=2,
coord_systems=[
CoordinateGridSystem(
name="Global",
grid=GridSystem(
x_grid_lines=make_grid_lines("X", x_ords),
y_grid_lines=make_grid_lines("Y", y_ords),
z_grid_lines=make_grid_lines("Z", [0.0]),
),
),
],
nodes=[
Node(id=1, name="Base-L", coords=(x1 * IN_TO_M, y1 * IN_TO_M, 0.0),
restraint=(True, True, False, False, False, False)),
Node(id=2, name="Base-M", coords=(x2 * IN_TO_M, y2 * IN_TO_M, 0.0),
restraint=(True, True, False, False, False, False)),
Node(id=3, name="Base-R", coords=(x3 * IN_TO_M, y3 * IN_TO_M, 0.0),
restraint=(True, True, False, False, False, False)),
Node(id=4, name="Crown", coords=(x4 * IN_TO_M, y4 * IN_TO_M, 0.0)),
],
materials=[
ElasticUniaxial(id=1, name="Steel-3000ksi", E=e),
],
sections=[],
elements=[
TrussElement(id=1, name="Bar-1-4", nodes=(1, 4),
area=a1, material_id=1),
TrussElement(id=2, name="Bar-2-4", nodes=(2, 4),
area=a2, material_id=1),
TrussElement(id=3, name="Bar-3-4", nodes=(3, 4),
area=a3, material_id=1),
],
time_series=[LinearTimeSeries(id=1, name="Ramp")],
load_patterns=[
PlainLoadPattern(
id=1, name="Tip Load",
time_series_id=1,
nodal_loads=[
NodalLoad(node_id=4, forces=(fx, fy, 0, 0, 0, 0)),
],
),
],
analyses=[
StaticCase(id=1, name="Static", pattern_ids=[1], n_steps=1),
],
)
def main() -> None:
project = build_basic_truss()
project.validate_references()
print(f"Built '{project.meta.name}' - {len(project.nodes)} nodes, "
f"{len(project.elements)} truss bars, {len(project.analyses)} case.")
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()