Skip to content

Instantly share code, notes, and snippets.

@likecodingloveproblems
Created November 16, 2024 08:41
Show Gist options
  • Save likecodingloveproblems/97b437cf0d776f690daf6684402b983e to your computer and use it in GitHub Desktop.
Save likecodingloveproblems/97b437cf0d776f690daf6684402b983e to your computer and use it in GitHub Desktop.
Sample Opensees API wrapper
import enum
from typing import Any, Literal, Optional
from openseespy import opensees as ops
from pydantic import BaseModel, Field
def tag_generator():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
class TaggedObject(BaseModel):
tag: int = Field(default_factory=tag_generator())
class Wipe(BaseModel):
def model_post_init(self, __context: Any) -> None:
ops.wipe()
class Model(BaseModel):
type_: str = "basic"
ndm: Literal[1, 2, 3]
ndf: Optional[int] = Field(None, gt=0, lt=7, init=False)
def model_post_init(self, __context: Any) -> None:
if self.ndf is None:
self.ndf = int(self.ndm * (self.ndm + 1) / 2)
ops.model(self.type_, "-ndm", self.ndm, "-ndf", self.ndf)
class Node(TaggedObject):
crds: list[float]
def model_post_init(self, __context: Any) -> None:
ops.node(self.tag, *self.crds)
class FixEnum(enum.Enum):
FREE = 0
FIX = 1
class Fix(BaseModel):
node: Node
values: list[FixEnum]
def model_post_init(self, __context: Any) -> None:
fix_values = [constr.value for constr in self.values]
ops.fix(self.node.tag, *fix_values)
class Material(TaggedObject):
pass
class Elastic(Material):
E: float
eta: float = 0.0
Eneg: Optional[float] = None
def model_post_init(self, __context: Any) -> None:
if self.Eneg is None:
self.Eneg = self.E
ops.uniaxialMaterial(
self.__class__.__name__,
self.tag,
self.E,
self.eta,
self.Eneg,
)
class MassMatrixEnum(enum.Enum):
LUMPED = 0
CONSISTENT = 1
class RayleighDampingEnum(enum.Enum):
OFF = 0
ON = 1
class Truss(TaggedObject):
nodes: list[Node]
A: float
material: Material
rho: float = 0.0
cFlag: MassMatrixEnum = MassMatrixEnum.LUMPED
rFlag: RayleighDampingEnum = RayleighDampingEnum.OFF
def model_post_init(self, __context: Any) -> None:
ops.element(
self.__class__.__name__,
self.tag,
*(node.tag for node in self.nodes),
self.A,
self.material.tag,
"-rho",
self.rho,
"-cMass",
self.cFlag.value,
"-doRayleigh",
self.rFlag.value,
)
class TimeSeries(TaggedObject):
pass
class TimeSeriesLinear(TimeSeries):
type_: str = "Linear"
factor: Optional[float] = None
tStart: Optional[float ]=None
def model_post_init(self, __context: Any) -> None:
command = [self.type_, self.tag]
if self.factor is not None:
command.extend(['-factor', self.factor])
if self.tStart is not None:
command.extend(['-tStart', self.tStart])
ops.timeSeries(
*command
)
class PatternPlain(TaggedObject):
type_: str = "Plain"
time_series: TimeSeries
fact: Optional[float] = None
def model_post_init(self, __context: Any) -> None:
command = [
self.type_,
self.tag,
self.time_series.tag,
]
if self.fact is not None:
command.extend(["-fact", self.fact])
ops.pattern(*command)
class Load(BaseModel):
node: Node
values: list[float]
def model_post_init(self, __context: Any) -> None:
ops.load(self.node.tag, *self.values)
class System(BaseModel):
type_: Literal[
"BandGen",
"BandSPD",
"ProfileSPD",
"SuperLU",
"UmfPack",
"FullGeneral",
"SparseSYM",
"Mumps",
]
def model_post_init(self, __context: Any) -> None:
ops.system(self.type_)
class Numberer(BaseModel):
type_: Literal["Plain", "RCM", "AMD", "ParallelPlain", "ParallelRCM"]
def model_post_init(self, __context: Any) -> None:
ops.numberer(self.type_)
class Constraints(BaseModel):
type_: Literal["Plain", "Transformation"]
alphaS: float = 1.0
alphaM: float = 1.0
@property
def __accept_alpha_params(self):
return self.type_ in {"Lagrange", "Penalty"}
def model_post_init(self, __context: Any) -> None:
if self.__accept_alpha_params:
ops.constraints(self.type_, self.alphaS, self.alphaM)
else:
ops.constraints(self.type_)
class IntegratorLoadControl(BaseModel):
type_: str = "LoadControl"
incr: float
num_iter: int = 1
min_incr: Optional[float] = None
max_incr: Optional[float] = None
def model_post_init(self, __context: Any) -> None:
if self.min_incr is None:
self.min_incr = self.incr
if self.max_incr is None:
self.max_incr = self.incr
ops.integrator(
self.type_, self.incr, self.num_iter, self.min_incr, self.max_incr
)
class AlgorithmLinear(BaseModel):
type_: str = "Linear"
secant: bool = False
initial: bool = False
factor_once: bool = False
def model_post_init(self, __context: Any) -> None:
ops.algorithm(self.type_, self.secant, self.initial, self.factor_once)
class Analysis(BaseModel):
type_: Literal["Static", "Transient", "VariableTransient", "PFEM"]
def model_post_init(self, __context: Any) -> None:
ops.analysis(self.type_)
def analyze(
num_incr: int = 1,
dt: float = 0.0,
dt_min: float = 0.0,
dt_max: float = 0.0,
jd: float = 0.0,
) -> bool:
return ops.analyze(num_incr, dt, dt_min, dt_max, jd)
class Analyze(BaseModel):
num_incr: int = 1
dt: float = 0.0
dt_min: float = 0.0
dt_max: float = 0.0
Jd: float = 0.0
failed: Optional[bool] = None
def model_post_init(self, __context: Any) -> None:
self.failed = ops.analyze(self.num_incr, self.dt, self.dt_min, self.dt_max, self.Jd) < 0
def node_disp(node: Node, dof: int = -1) -> float:
return ops.nodeDisp(node.tag, dof)
########################################################################
####### the application part #######
########################################################################
from ops import (
Wipe,
Model,
Node,
Fix,
FixEnum,
Elastic,
Truss,
TimeSeriesLinear,
Load,
PatternPlain,
System,
Numberer,
Constraints,
IntegratorLoadControl,
AlgorithmLinear,
Analysis,
Analyze,
node_disp,
)
# ------------------------------
# Start of model generation
# -----------------------------
# remove existing model
Wipe()
# set modelbuilder
Model(ndm=2, ndf=2)
# create nodes
fixed_nodes = [
Node(crds=[0.0, 0.0]),
Node(crds=[144.0, 0.0]),
Node(crds=[168.0, 0.0]),
]
top_node = Node(crds=[72.0, 96.0])
# set boundary condition
for node in fixed_nodes:
Fix(node=node, values=[FixEnum.FIX, FixEnum.FIX])
# define materials
material = Elastic(E=3000.0)
# define elements
areas = [10.0, 5.0, 5.0]
for node, area in zip(fixed_nodes, areas):
Truss(nodes=[node, top_node], A=area, material=material)
# create TimeSeries
time_series = TimeSeriesLinear()
# create a plain load pattern
PatternPlain(time_series=time_series)
# Create the nodal load - command: load nodeID xForce yForce
Load(node=top_node, values=[100.0, -50.0])
# ------------------------------
# Start of analysis generation
# ------------------------------
# create SOE
System(type_="BandSPD")
# create DOF number
Numberer(type_="RCM")
# create constraint handler
Constraints(type_="Plain")
# create integrator
IntegratorLoadControl(incr=1.0)
# create algorithm
AlgorithmLinear()
# create analysis object
Analysis(type_="Static")
# perform the analysis
analyze_ = Analyze()
if analyze_.failed:
print("analyze failed!")
ux = node_disp(node=top_node, dof=1)
uy = node_disp(node=top_node, dof=2)
if (
abs(ux - 0.53009277713228375450) < 1e-12
and abs(uy + 0.17789363846931768864) < 1e-12
):
print("Passed!")
else:
print("Failed!")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment