chore: adopt remaining local development state
Catch-all for the intermixed residue of the unpushed otko-development work ported into this tree: combinations/console-dock/quick-guide wiring across commands, core, services, views and tests; repo-wide ruff-format normalization; README/CONTRIBUTING updates; and the toolbar default (both toolbars now open in the top area, quick guide text updated). Splitting this further would require hunk-level surgery with low confidence; the preceding commits in this branch isolate the self-contained features.
This commit is contained in:
parent
cb4bf8fccd
commit
ba783718d4
152 changed files with 3394 additions and 1651 deletions
|
|
@ -13,6 +13,8 @@ pytest.importorskip("openseespy")
|
|||
|
||||
from otko.services.opensees_runner import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
def test_basic_truss_matches_opensees_tcl_reference() -> None:
|
||||
"""Crown displacement must match the OpenSees Tcl Example-1 reference.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ pytest.importorskip("openseespy")
|
|||
from otko.services import load_project, save_project # noqa: E402
|
||||
from otko.services.opensees_runner import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
def test_beam_quad_2d_midspan_deflection(tmp_path) -> None: # type: ignore[no-untyped-def]
|
||||
"""10-step LoadControl on a 16×4 plane-stress quad mesh — reference
|
||||
|
|
@ -23,6 +25,7 @@ def test_beam_quad_2d_midspan_deflection(tmp_path) -> None: # type: ignore[no-u
|
|||
_mid_top_node_id,
|
||||
build_beam_quad_2d,
|
||||
)
|
||||
|
||||
proj = build_beam_quad_2d()
|
||||
proj.validate_references()
|
||||
|
||||
|
|
@ -49,10 +52,10 @@ def test_beam_quad_2d_midspan_deflection(tmp_path) -> None: # type: ignore[no-u
|
|||
|
||||
# By symmetry, left and right support reactions must balance the two
|
||||
# 10-kip midspan loads (total -20 kip vertical).
|
||||
assert reloaded.nodes[0].id == 1 # left pin
|
||||
assert reloaded.nodes[0].id == 1 # left pin
|
||||
reaction_sum_fy = sum(
|
||||
result.node_reaction[nid][-1, 1]
|
||||
for nid in (1, 17) # node 17 = (L, 0) = roller
|
||||
for nid in (1, 17) # node 17 = (L, 0) = roller
|
||||
)
|
||||
assert reaction_sum_fy == pytest.approx(20.0, abs=1e-6)
|
||||
|
||||
|
|
@ -69,6 +72,7 @@ def test_beam_quad_2d_free_vibration_chain(tmp_path) -> None: # type: ignore[no
|
|||
_mid_bottom_node_id,
|
||||
build_beam_quad_2d,
|
||||
)
|
||||
|
||||
proj = build_beam_quad_2d()
|
||||
proj.validate_references()
|
||||
|
||||
|
|
@ -81,6 +85,7 @@ def test_beam_quad_2d_free_vibration_chain(tmp_path) -> None: # type: ignore[no
|
|||
r = OpenSeesRunner(reloaded).run(reloaded.analyses[1], results_dir=results_dir)
|
||||
|
||||
import h5py
|
||||
|
||||
with h5py.File(r.h5_path) as f:
|
||||
t = f["time"][:]
|
||||
u = f[f"nodes/{_mid_bottom_node_id()}/disp"][:, 1]
|
||||
|
|
|
|||
|
|
@ -34,22 +34,25 @@ from otko.core import ( # noqa: E402
|
|||
)
|
||||
from otko.services.opensees_runner import OpenSeesRunner # noqa: E402
|
||||
|
||||
# ── Model constants ────────────────────────────────────────────────────────────
|
||||
L = 1.0 # column height [m]
|
||||
B = H = 0.3 # cross-section dimensions [m]
|
||||
A = B * H # section area [m²]
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
FC = -30e6 # peak compressive strength [Pa] (negative)
|
||||
EPSC0 = -0.002 # strain at peak strength (negative)
|
||||
EPSCU = -0.005 # ultimate compressive strain (negative)
|
||||
EC = 30e9 # initial tangent modulus [Pa]
|
||||
|
||||
# ── Model constants ────────────────────────────────────────────────────────────
|
||||
L = 1.0 # column height [m]
|
||||
B = H = 0.3 # cross-section dimensions [m]
|
||||
A = B * H # section area [m²]
|
||||
|
||||
FC = -30e6 # peak compressive strength [Pa] (negative)
|
||||
EPSC0 = -0.002 # strain at peak strength (negative)
|
||||
EPSCU = -0.005 # ultimate compressive strain (negative)
|
||||
EC = 30e9 # initial tangent modulus [Pa]
|
||||
|
||||
# Applied axial load: small enough (< 1 % of capacity) that the Popovics
|
||||
# curve is indistinguishable from its linear tangent at origin.
|
||||
P_AXIAL = -1200.0 # N (downward → compressive)
|
||||
P_AXIAL = -1200.0 # N (downward → compressive)
|
||||
|
||||
# Analytical axial shortening: P * L / (Ec * A)
|
||||
EXPECTED_UY = P_AXIAL * L / (EC * A) # ≈ -1.333e-7 m
|
||||
EXPECTED_UY = P_AXIAL * L / (EC * A) # ≈ -1.333e-7 m
|
||||
|
||||
|
||||
def _build_project() -> Project:
|
||||
|
|
@ -61,32 +64,45 @@ def _build_project() -> Project:
|
|||
ndm=2,
|
||||
ndf=3,
|
||||
nodes=[
|
||||
Node(id=1, name="Base", coords=(0.0, 0.0, 0.0),
|
||||
restraint=(True, True, True, False, False, False)),
|
||||
Node(id=2, name="Top", coords=(0.0, L, 0.0)),
|
||||
Node(
|
||||
id=1,
|
||||
name="Base",
|
||||
coords=(0.0, 0.0, 0.0),
|
||||
restraint=(True, True, True, False, False, False),
|
||||
),
|
||||
Node(id=2, name="Top", coords=(0.0, L, 0.0)),
|
||||
],
|
||||
materials=[
|
||||
Concrete04(
|
||||
id=1, name="C30-Popovics",
|
||||
fpc=FC, epsc0=EPSC0, epscu=EPSCU, Ec=EC,
|
||||
id=1,
|
||||
name="C30-Popovics",
|
||||
fpc=FC,
|
||||
epsc0=EPSC0,
|
||||
epscu=EPSCU,
|
||||
Ec=EC,
|
||||
),
|
||||
],
|
||||
sections=[
|
||||
FiberSection(
|
||||
id=1, name="RC-Fiber",
|
||||
id=1,
|
||||
name="RC-Fiber",
|
||||
patches=[
|
||||
RectangularPatch(
|
||||
material_id=1,
|
||||
n_fib_y=4, n_fib_z=4,
|
||||
y_i=-H / 2, z_i=-B / 2,
|
||||
y_j= H / 2, z_j= B / 2,
|
||||
n_fib_y=4,
|
||||
n_fib_z=4,
|
||||
y_i=-H / 2,
|
||||
z_i=-B / 2,
|
||||
y_j=H / 2,
|
||||
z_j=B / 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
elements=[
|
||||
ForceBeamColumn(
|
||||
id=1, name="Column",
|
||||
id=1,
|
||||
name="Column",
|
||||
nodes=(1, 2),
|
||||
section_id=1,
|
||||
integration_points=3,
|
||||
|
|
@ -96,7 +112,9 @@ def _build_project() -> Project:
|
|||
time_series=[LinearTimeSeries(id=1, name="Ramp")],
|
||||
load_patterns=[
|
||||
PlainLoadPattern(
|
||||
id=1, name="Gravity", time_series_id=1,
|
||||
id=1,
|
||||
name="Gravity",
|
||||
time_series_id=1,
|
||||
nodal_loads=[
|
||||
NodalLoad(node_id=2, forces=(0.0, P_AXIAL, 0.0, 0.0, 0.0, 0.0)),
|
||||
],
|
||||
|
|
@ -104,7 +122,8 @@ def _build_project() -> Project:
|
|||
],
|
||||
analyses=[
|
||||
StaticCase(
|
||||
id=1, name="Gravity",
|
||||
id=1,
|
||||
name="Gravity",
|
||||
pattern_ids=[1],
|
||||
n_steps=1,
|
||||
load_factor_increment=1.0,
|
||||
|
|
@ -139,9 +158,14 @@ def test_concrete04_with_tension_does_not_raise() -> None:
|
|||
proj = _build_project()
|
||||
# Replace material with tensile-branch variant.
|
||||
proj.materials[0] = Concrete04(
|
||||
id=1, name="C30-WithTension",
|
||||
fpc=FC, epsc0=EPSC0, epscu=EPSCU, Ec=EC,
|
||||
fct=3.0e6, et=1e-4,
|
||||
id=1,
|
||||
name="C30-WithTension",
|
||||
fpc=FC,
|
||||
epsc0=EPSC0,
|
||||
epscu=EPSCU,
|
||||
Ec=EC,
|
||||
fct=3.0e6,
|
||||
et=1e-4,
|
||||
)
|
||||
case = proj.analyses[0]
|
||||
result = OpenSeesRunner(proj).run(case)
|
||||
|
|
|
|||
|
|
@ -23,15 +23,16 @@ from otko.core import ( # noqa: E402
|
|||
)
|
||||
from otko.services.opensees_runner import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
def _make_truss_project(ndf: int) -> Project:
|
||||
return Project(
|
||||
ndm=2, ndf=ndf,
|
||||
ndm=2,
|
||||
ndf=ndf,
|
||||
nodes=[
|
||||
Node(id=1, coords=(0, 0, 0),
|
||||
restraint=(True, True, False, False, False, False)),
|
||||
Node(id=2, coords=(3, 0, 0),
|
||||
restraint=(True, True, False, False, False, False)),
|
||||
Node(id=1, coords=(0, 0, 0), restraint=(True, True, False, False, False, False)),
|
||||
Node(id=2, coords=(3, 0, 0), restraint=(True, True, False, False, False, False)),
|
||||
Node(id=3, coords=(1.5, 2, 0)),
|
||||
],
|
||||
materials=[ElasticUniaxial(id=1, E=200e9)],
|
||||
|
|
@ -40,10 +41,13 @@ def _make_truss_project(ndf: int) -> Project:
|
|||
TrussElement(id=2, nodes=(2, 3), area=1e-3, material_id=1),
|
||||
],
|
||||
time_series=[LinearTimeSeries(id=1, name="R")],
|
||||
load_patterns=[PlainLoadPattern(
|
||||
id=1, time_series_id=1,
|
||||
nodal_loads=[NodalLoad(node_id=3, forces=(1e3, -5e3, 0, 0, 0, 0))],
|
||||
)],
|
||||
load_patterns=[
|
||||
PlainLoadPattern(
|
||||
id=1,
|
||||
time_series_id=1,
|
||||
nodal_loads=[NodalLoad(node_id=3, forces=(1e3, -5e3, 0, 0, 0, 0))],
|
||||
)
|
||||
],
|
||||
analyses=[StaticCase(id=1, name="Static", pattern_ids=[1], n_steps=1)],
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ from otko.core import ModalCase # noqa: E402
|
|||
from otko.services import load_project, save_project # noqa: E402
|
||||
from otko.services.opensees_runner import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
def _reload(proj, tmp_path): # type: ignore[no-untyped-def]
|
||||
path = tmp_path / "two_storey_one_bay.osmodel"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ from otko.core import ModalCase # noqa: E402
|
|||
from otko.services import load_project, save_project # noqa: E402
|
||||
from otko.services.opensees_runner import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
def _reload(proj, tmp_path): # type: ignore[no-untyped-def]
|
||||
path = tmp_path / "two_storey_shear.osmodel"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ from otko.core import ModalCase, StaticCase # noqa: E402
|
|||
from otko.services import load_project, save_project # noqa: E402
|
||||
from otko.services.opensees_runner import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
BASE_NODES = (1, 2, 3, 4)
|
||||
|
||||
|
|
@ -27,13 +29,18 @@ def _reload(proj, tmp_path): # type: ignore[no-untyped-def]
|
|||
def test_elastic_frame_gravity_reactions(tmp_path) -> None: # type: ignore[no-untyped-def]
|
||||
"""ΣFy at base = total applied gravity (distributed w × beam × floors)."""
|
||||
from examples.elastic_frame import (
|
||||
BAY, LOAD_F1, LOAD_F2, LOAD_F3, N_BAYS, build_elastic_frame,
|
||||
BAY,
|
||||
LOAD_F1,
|
||||
LOAD_F2,
|
||||
LOAD_F3,
|
||||
N_BAYS,
|
||||
build_elastic_frame,
|
||||
)
|
||||
|
||||
proj = _reload(build_elastic_frame(), tmp_path)
|
||||
|
||||
gravity_case = next(
|
||||
c for c in proj.analyses
|
||||
if isinstance(c, StaticCase) and c.name == "Gravity"
|
||||
c for c in proj.analyses if isinstance(c, StaticCase) and c.name == "Gravity"
|
||||
)
|
||||
r = OpenSeesRunner(proj).run(gravity_case)
|
||||
|
||||
|
|
@ -51,24 +58,33 @@ def test_elastic_frame_gravity_reactions(tmp_path) -> None: # type: ignore[no-u
|
|||
|
||||
# By symmetry exterior-column reactions pair up, as do interior.
|
||||
assert r.node_reaction[1][-1, 1] == pytest.approx(
|
||||
r.node_reaction[4][-1, 1], abs=1e-6,
|
||||
r.node_reaction[4][-1, 1],
|
||||
abs=1e-6,
|
||||
)
|
||||
assert r.node_reaction[2][-1, 1] == pytest.approx(
|
||||
r.node_reaction[3][-1, 1], abs=1e-6,
|
||||
r.node_reaction[3][-1, 1],
|
||||
abs=1e-6,
|
||||
)
|
||||
|
||||
|
||||
def test_elastic_frame_gravity_plus_lateral_reactions(tmp_path) -> None: # type: ignore[no-untyped-def]
|
||||
"""ΣFx at base must equal -(lateral applied) within PDelta tolerance."""
|
||||
from examples.elastic_frame import (
|
||||
BAY, LOAD_F1, LOAD_F2, LOAD_F3,
|
||||
N_BAYS, P_F1, P_F2, P_F3, build_elastic_frame,
|
||||
BAY,
|
||||
LOAD_F1,
|
||||
LOAD_F2,
|
||||
LOAD_F3,
|
||||
N_BAYS,
|
||||
P_F1,
|
||||
P_F2,
|
||||
P_F3,
|
||||
build_elastic_frame,
|
||||
)
|
||||
|
||||
proj = _reload(build_elastic_frame(), tmp_path)
|
||||
|
||||
combined = next(
|
||||
c for c in proj.analyses
|
||||
if isinstance(c, StaticCase) and c.name == "Gravity+Lateral"
|
||||
c for c in proj.analyses if isinstance(c, StaticCase) and c.name == "Gravity+Lateral"
|
||||
)
|
||||
r = OpenSeesRunner(proj).run(combined)
|
||||
|
||||
|
|
@ -87,6 +103,7 @@ def test_elastic_frame_gravity_plus_lateral_reactions(tmp_path) -> None: # type
|
|||
def test_elastic_frame_modal_periods(tmp_path) -> None: # type: ignore[no-untyped-def]
|
||||
"""5-mode eigen analysis: first periods match the Tcl reference values."""
|
||||
from examples.elastic_frame import build_elastic_frame
|
||||
|
||||
proj = _reload(build_elastic_frame(), tmp_path)
|
||||
|
||||
modal = next(c for c in proj.analyses if isinstance(c, ModalCase))
|
||||
|
|
@ -104,6 +121,4 @@ def test_elastic_frame_modal_periods(tmp_path) -> None: # type: ignore[no-untyp
|
|||
expected = [1.040, 0.3526, 0.1930, 0.1562, 0.130]
|
||||
periods = [2.0 * math.pi / math.sqrt(v) for v in r.eigenvalues]
|
||||
for i, (T, T_ref) in enumerate(zip(periods, expected), start=1):
|
||||
assert T == pytest.approx(T_ref, rel=0.02), (
|
||||
f"T{i} = {T:.4f} s, reference {T_ref:.4f} s"
|
||||
)
|
||||
assert T == pytest.approx(T_ref, rel=0.02), f"T{i} = {T:.4f} s, reference {T_ref:.4f} s"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ from otko.core import PushoverCase, TransientCase # noqa: E402
|
|||
from otko.services import load_project, save_project # noqa: E402
|
||||
from otko.services.opensees_runner import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
def _reload(proj, tmp_path): # type: ignore[no-untyped-def]
|
||||
path = tmp_path / "ex1a_canti2d.osmodel"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ pytest.importorskip("openseespy")
|
|||
from otko.services import load_project, save_project # noqa: E402
|
||||
from otko.services.opensees_runner import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
def test_ex1a_canti2d_eq_runs_and_oscillates(tmp_path) -> None: # type: ignore[no-untyped-def]
|
||||
from examples.ex1a_canti2d_eq import (
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ from otko.core import PushoverCase, TransientCase # noqa: E402
|
|||
from otko.services import load_project, save_project # noqa: E402
|
||||
from otko.services.opensees_runner import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
def _reload(proj, tmp_path): # type: ignore[no-untyped-def]
|
||||
path = tmp_path / "ex1b_portal2d.osmodel"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ from otko.core import PushoverCase, TransientCase # noqa: E402
|
|||
from otko.services import load_project, save_project # noqa: E402
|
||||
from otko.services.opensees_runner import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
def _reload(proj, tmp_path): # type: ignore[no-untyped-def]
|
||||
path = tmp_path / "ex2a_canti2d_elastic_element.osmodel"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ from otko.core import PushoverCase, TransientCase # noqa: E402
|
|||
from otko.services import load_project, save_project # noqa: E402
|
||||
from otko.services.opensees_runner import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
def _reload(proj, tmp_path): # type: ignore[no-untyped-def]
|
||||
path = tmp_path / "ex2b_canti2d_inelastic_section.osmodel"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ from otko.core import PushoverCase, TransientCase # noqa: E402
|
|||
from otko.services import load_project, save_project # noqa: E402
|
||||
from otko.services.opensees_runner import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
def _reload(proj, tmp_path): # type: ignore[no-untyped-def]
|
||||
path = tmp_path / "ex2c_canti2d_inelastic_fiber_section.osmodel"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ from otko.core import PushoverCase, TransientCase # noqa: E402
|
|||
from otko.services import load_project, save_project # noqa: E402
|
||||
from otko.services.opensees_runner import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
def _reload(proj, tmp_path, stem: str): # type: ignore[no-untyped-def]
|
||||
path = tmp_path / f"{stem}.osmodel"
|
||||
|
|
@ -27,10 +29,16 @@ def _reload(proj, tmp_path, stem: str): # type: ignore[no-untyped-def]
|
|||
[
|
||||
("build_ex3_canti2d_elastic_element", "examples.ex3_canti2d_elastic_element", False),
|
||||
("build_ex3_canti2d_inelastic_section", "examples.ex3_canti2d_inelastic_section", True),
|
||||
("build_ex3_canti2d_inelastic_fiber_section", "examples.ex3_canti2d_inelastic_fiber_section", True),
|
||||
(
|
||||
"build_ex3_canti2d_inelastic_fiber_section",
|
||||
"examples.ex3_canti2d_inelastic_fiber_section",
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_ex3_variant_pushover_runs(tmp_path, builder_name: str, module_name: str, nonlinear: bool) -> None: # type: ignore[no-untyped-def]
|
||||
def test_ex3_variant_pushover_runs(
|
||||
tmp_path, builder_name: str, module_name: str, nonlinear: bool
|
||||
) -> None: # type: ignore[no-untyped-def]
|
||||
mod = __import__(module_name, fromlist=[builder_name, "PUSH_STEP", "PUSH_TARGET"])
|
||||
proj = _reload(getattr(mod, builder_name)(), tmp_path, builder_name)
|
||||
push_case = next(c for c in proj.analyses if isinstance(c, PushoverCase))
|
||||
|
|
@ -41,8 +49,12 @@ def test_ex3_variant_pushover_runs(tmp_path, builder_name: str, module_name: str
|
|||
assert result.control_disp[-1] == pytest.approx(mod.PUSH_TARGET, rel=1e-6)
|
||||
assert max(result.base_shear) > 0.0
|
||||
|
||||
early = (result.base_shear[5] - result.base_shear[0]) / (result.control_disp[5] - result.control_disp[0])
|
||||
late = (result.base_shear[-1] - result.base_shear[-6]) / (result.control_disp[-1] - result.control_disp[-6])
|
||||
early = (result.base_shear[5] - result.base_shear[0]) / (
|
||||
result.control_disp[5] - result.control_disp[0]
|
||||
)
|
||||
late = (result.base_shear[-1] - result.base_shear[-6]) / (
|
||||
result.control_disp[-1] - result.control_disp[-6]
|
||||
)
|
||||
if nonlinear:
|
||||
assert early > 2.0 * late
|
||||
else:
|
||||
|
|
@ -54,7 +66,10 @@ def test_ex3_variant_pushover_runs(tmp_path, builder_name: str, module_name: str
|
|||
[
|
||||
("build_ex3_canti2d_elastic_element", "examples.ex3_canti2d_elastic_element"),
|
||||
("build_ex3_canti2d_inelastic_section", "examples.ex3_canti2d_inelastic_section"),
|
||||
("build_ex3_canti2d_inelastic_fiber_section", "examples.ex3_canti2d_inelastic_fiber_section"),
|
||||
(
|
||||
"build_ex3_canti2d_inelastic_fiber_section",
|
||||
"examples.ex3_canti2d_inelastic_fiber_section",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_ex3_variant_earthquake_runs(tmp_path, builder_name: str, module_name: str) -> None: # type: ignore[no-untyped-def]
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ from otko.core import PushoverCase, TransientCase # noqa: E402
|
|||
from otko.services import load_project, save_project # noqa: E402
|
||||
from otko.services.opensees_runner import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
def _reload(proj, tmp_path, stem: str): # type: ignore[no-untyped-def]
|
||||
path = tmp_path / f"{stem}.osmodel"
|
||||
|
|
@ -27,10 +29,16 @@ def _reload(proj, tmp_path, stem: str): # type: ignore[no-untyped-def]
|
|||
[
|
||||
("build_ex4_portal2d_elastic_element", "examples.ex4_portal2d_elastic_element", False),
|
||||
("build_ex4_portal2d_inelastic_section", "examples.ex4_portal2d_inelastic_section", True),
|
||||
("build_ex4_portal2d_inelastic_fiber_section", "examples.ex4_portal2d_inelastic_fiber_section", True),
|
||||
(
|
||||
"build_ex4_portal2d_inelastic_fiber_section",
|
||||
"examples.ex4_portal2d_inelastic_fiber_section",
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_ex4_variant_pushover_runs(tmp_path, builder_name: str, module_name: str, nonlinear: bool) -> None: # type: ignore[no-untyped-def]
|
||||
def test_ex4_variant_pushover_runs(
|
||||
tmp_path, builder_name: str, module_name: str, nonlinear: bool
|
||||
) -> None: # type: ignore[no-untyped-def]
|
||||
mod = __import__(module_name, fromlist=[builder_name, "PUSH_STEP", "PUSH_TARGET"])
|
||||
proj = _reload(getattr(mod, builder_name)(), tmp_path, builder_name)
|
||||
push_case = next(c for c in proj.analyses if isinstance(c, PushoverCase))
|
||||
|
|
@ -45,8 +53,12 @@ def test_ex4_variant_pushover_runs(tmp_path, builder_name: str, module_name: str
|
|||
assert result.control_disp[-1] == pytest.approx(mod.PUSH_TARGET, abs=5e-3)
|
||||
assert max(result.base_shear) > 0.0
|
||||
|
||||
early = (result.base_shear[5] - result.base_shear[0]) / (result.control_disp[5] - result.control_disp[0])
|
||||
late = (result.base_shear[-1] - result.base_shear[-6]) / (result.control_disp[-1] - result.control_disp[-6])
|
||||
early = (result.base_shear[5] - result.base_shear[0]) / (
|
||||
result.control_disp[5] - result.control_disp[0]
|
||||
)
|
||||
late = (result.base_shear[-1] - result.base_shear[-6]) / (
|
||||
result.control_disp[-1] - result.control_disp[-6]
|
||||
)
|
||||
if nonlinear:
|
||||
assert early > 1.25 * late
|
||||
else:
|
||||
|
|
@ -58,7 +70,10 @@ def test_ex4_variant_pushover_runs(tmp_path, builder_name: str, module_name: str
|
|||
[
|
||||
("build_ex4_portal2d_elastic_element", "examples.ex4_portal2d_elastic_element"),
|
||||
("build_ex4_portal2d_inelastic_section", "examples.ex4_portal2d_inelastic_section"),
|
||||
("build_ex4_portal2d_inelastic_fiber_section", "examples.ex4_portal2d_inelastic_fiber_section"),
|
||||
(
|
||||
"build_ex4_portal2d_inelastic_fiber_section",
|
||||
"examples.ex4_portal2d_inelastic_fiber_section",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_ex4_variant_sine_runs(tmp_path, builder_name: str, module_name: str) -> None: # type: ignore[no-untyped-def]
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ from otko.services.material_tester import (
|
|||
test_uniaxial_material,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
# ---- helpers ---------------------------------------------------------------
|
||||
|
||||
|
||||
|
|
@ -45,7 +48,8 @@ def _simple_cantilever() -> Project:
|
|||
ndf=3,
|
||||
nodes=[
|
||||
Node(
|
||||
id=1, name="Base",
|
||||
id=1,
|
||||
name="Base",
|
||||
coords=(0.0, 0.0, 0.0),
|
||||
# 2D-frame DOF mapping: (Ux, Uy, Uz, Rx, Ry, Rz) -> runner uses (0,1,5).
|
||||
# Fixed base: Ux=True, Uy=True, Rz=True (index 5).
|
||||
|
|
@ -61,17 +65,25 @@ def _simple_cantilever() -> Project:
|
|||
time_series=[LinearTimeSeries(id=1)],
|
||||
load_patterns=[
|
||||
PlainLoadPattern(
|
||||
id=1, time_series_id=1,
|
||||
id=1,
|
||||
time_series_id=1,
|
||||
# Downward tip load (Uy direction).
|
||||
nodal_loads=[NodalLoad(node_id=2, forces=(0.0, -1.0e4, 0.0, 0.0, 0.0, 0.0))],
|
||||
),
|
||||
],
|
||||
analyses=[
|
||||
StaticCase(
|
||||
id=1, pattern_ids=[1], n_steps=1, load_factor_increment=1.0,
|
||||
system="BandGeneral", constraints="Plain",
|
||||
integrator="LoadControl", algorithm="Newton",
|
||||
test="NormDispIncr", tolerance=1e-8, max_iter=10,
|
||||
id=1,
|
||||
pattern_ids=[1],
|
||||
n_steps=1,
|
||||
load_factor_increment=1.0,
|
||||
system="BandGeneral",
|
||||
constraints="Plain",
|
||||
integrator="LoadControl",
|
||||
algorithm="Newton",
|
||||
test="NormDispIncr",
|
||||
tolerance=1e-8,
|
||||
max_iter=10,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
|
@ -110,8 +122,8 @@ def test_elastic_uniaxial_monotonic_stress_strain() -> None:
|
|||
def test_elastic_pp_compressive_plateau() -> None:
|
||||
"""ElasticPP: stress is exactly -Fy for all strains past compressive yield."""
|
||||
e_mod = 200e9
|
||||
epsy = 1.25e-3 # yield strain in tension
|
||||
fy = e_mod * epsy # implied yield stress = 250 MPa
|
||||
epsy = 1.25e-3 # yield strain in tension
|
||||
fy = e_mod * epsy # implied yield stress = 250 MPa
|
||||
|
||||
mat = ElasticPP(id=1, E=e_mod, epsy_pos=epsy)
|
||||
protocol = LoadProtocol(
|
||||
|
|
@ -129,9 +141,9 @@ def test_elastic_pp_compressive_plateau() -> None:
|
|||
assert len(past_yield) > 0, "no post-yield data points found"
|
||||
|
||||
for s, sig in past_yield:
|
||||
assert sig == pytest.approx(-fy, rel=1e-6), (
|
||||
f"plateau broken at strain={s:.4g}: got {sig:.4g}, expected {-fy:.4g}"
|
||||
)
|
||||
assert sig == pytest.approx(
|
||||
-fy, rel=1e-6
|
||||
), f"plateau broken at strain={s:.4g}: got {sig:.4g}, expected {-fy:.4g}"
|
||||
|
||||
|
||||
# ---- Steel01 cyclic energy -------------------------------------------------
|
||||
|
|
@ -147,9 +159,9 @@ def test_steel01_cyclic_hysteresis_energy() -> None:
|
|||
fy = 250e6
|
||||
e0 = 200e9
|
||||
b = 0.0
|
||||
ey = fy / e0 # = 1.25e-3
|
||||
ea = 5.0 * ey # = 6.25e-3
|
||||
n = 100 # steps per branch
|
||||
ey = fy / e0 # = 1.25e-3
|
||||
ea = 5.0 * ey # = 6.25e-3
|
||||
n = 100 # steps per branch
|
||||
|
||||
mat = Steel01(id=1, Fy=fy, E0=e0, b=b)
|
||||
protocol = LoadProtocol(
|
||||
|
|
@ -162,30 +174,28 @@ def test_steel01_cyclic_hysteresis_energy() -> None:
|
|||
result = test_uniaxial_material(mat, protocol)
|
||||
|
||||
# Theoretical energy per stable cycle (EPP closed-form)
|
||||
e_ref = 4.0 * fy * (ea - ey) # = 5 000 000 J/m^3
|
||||
e_ref = 4.0 * fy * (ea - ey) # = 5 000 000 J/m^3
|
||||
|
||||
pts_per_cycle = 3 * n # = 300 (three branches per cycle)
|
||||
pts_per_cycle = 3 * n # = 300 (three branches per cycle)
|
||||
total_pts = len(result.strain)
|
||||
assert total_pts == 3 * pts_per_cycle, f"expected 900 points, got {total_pts}"
|
||||
|
||||
for i_cycle in [1, 2]: # stable cycles 1 and 2 (0-indexed); closed loops
|
||||
for i_cycle in [1, 2]: # stable cycles 1 and 2 (0-indexed); closed loops
|
||||
# Include the last point of the preceding cycle as the opening vertex
|
||||
# so the integration path is a closed loop.
|
||||
lo = i_cycle * pts_per_cycle - 1
|
||||
hi = (i_cycle + 1) * pts_per_cycle # Python slice: exclusive upper bound
|
||||
hi = (i_cycle + 1) * pts_per_cycle # Python slice: exclusive upper bound
|
||||
strain_loop = result.strain[lo:hi]
|
||||
stress_loop = result.stress[lo:hi]
|
||||
assert len(strain_loop) == pts_per_cycle + 1 # 301 points
|
||||
|
||||
# Trapezoidal area of closed stress-strain loop = dissipated energy.
|
||||
e_num = sum(
|
||||
0.5 * (stress_loop[j] + stress_loop[j + 1])
|
||||
* (strain_loop[j + 1] - strain_loop[j])
|
||||
0.5 * (stress_loop[j] + stress_loop[j + 1]) * (strain_loop[j + 1] - strain_loop[j])
|
||||
for j in range(len(strain_loop) - 1)
|
||||
)
|
||||
assert abs(e_num) == pytest.approx(e_ref, rel=0.01), (
|
||||
f"cycle {i_cycle + 1}: numerical energy {abs(e_num):.4g} "
|
||||
f"vs reference {e_ref:.4g}"
|
||||
f"cycle {i_cycle + 1}: numerical energy {abs(e_num):.4g} " f"vs reference {e_ref:.4g}"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -207,7 +217,7 @@ def test_concrete04_monotonic_popovics_envelope() -> None:
|
|||
epsc0 = -0.002
|
||||
epscu = -0.005
|
||||
ec = 30e9
|
||||
n_steps = 200 # enough resolution to detect a kink clearly
|
||||
n_steps = 200 # enough resolution to detect a kink clearly
|
||||
|
||||
mat = Concrete04(id=1, fpc=fpc, epsc0=epsc0, epscu=epscu, Ec=ec)
|
||||
protocol = LoadProtocol(
|
||||
|
|
@ -243,23 +253,21 @@ def test_concrete04_monotonic_popovics_envelope() -> None:
|
|||
)
|
||||
|
||||
# C1 continuity at peak: tangent slope ~ 0 from both sides.
|
||||
d_eps = strain[peak_idx] - strain[peak_idx - 1] # negative step size
|
||||
d_eps = strain[peak_idx] - strain[peak_idx - 1] # negative step size
|
||||
slope_before = (stress[peak_idx] - stress[peak_idx - 1]) / d_eps
|
||||
slope_after = (stress[peak_idx + 1] - stress[peak_idx]) / (
|
||||
strain[peak_idx + 1] - strain[peak_idx]
|
||||
)
|
||||
|
||||
# Both slopes must be near zero (Popovics curve is C1 at the peak).
|
||||
assert abs(slope_before) / ec < 0.05, (
|
||||
f"slope before peak too large: {slope_before / ec:.4f} x Ec"
|
||||
)
|
||||
assert abs(slope_after) / ec < 0.05, (
|
||||
f"slope after peak too large: {slope_after / ec:.4f} x Ec"
|
||||
)
|
||||
assert (
|
||||
abs(slope_before) / ec < 0.05
|
||||
), f"slope before peak too large: {slope_before / ec:.4f} x Ec"
|
||||
assert abs(slope_after) / ec < 0.05, f"slope after peak too large: {slope_after / ec:.4f} x Ec"
|
||||
# No kink: slope change at the peak must be smooth (< 5% of Ec).
|
||||
assert abs(slope_before - slope_after) / ec < 0.05, (
|
||||
f"kink detected at peak: delta_slope = {abs(slope_before - slope_after) / ec:.4f} x Ec"
|
||||
)
|
||||
assert (
|
||||
abs(slope_before - slope_after) / ec < 0.05
|
||||
), f"kink detected at peak: delta_slope = {abs(slope_before - slope_after) / ec:.4f} x Ec"
|
||||
|
||||
|
||||
# ---- state-cleanup proof ---------------------------------------------------
|
||||
|
|
@ -281,12 +289,8 @@ def test_state_cleanup_ten_consecutive_calls() -> None:
|
|||
ref_strain = results[0].strain
|
||||
ref_stress = results[0].stress
|
||||
for i, r in enumerate(results[1:], start=1):
|
||||
assert r.strain == pytest.approx(ref_strain, rel=1e-9), (
|
||||
f"strain diverged on call {i + 1}"
|
||||
)
|
||||
assert r.stress == pytest.approx(ref_stress, rel=1e-9), (
|
||||
f"stress diverged on call {i + 1}"
|
||||
)
|
||||
assert r.strain == pytest.approx(ref_strain, rel=1e-9), f"strain diverged on call {i + 1}"
|
||||
assert r.stress == pytest.approx(ref_stress, rel=1e-9), f"stress diverged on call {i + 1}"
|
||||
|
||||
|
||||
# ---- interleave test -------------------------------------------------------
|
||||
|
|
@ -323,6 +327,6 @@ def test_interleave_with_runner_analysis() -> None:
|
|||
# Uy at node 2 (DOF 1 in 0-indexed = DOF 2 in 1-indexed) must be identical.
|
||||
uy1 = float(result1.node_disp[2][0, 1])
|
||||
uy2 = float(result2.node_disp[2][0, 1])
|
||||
assert uy1 == pytest.approx(uy2, rel=1e-9), (
|
||||
f"runner Uy changed after interleaved material test: {uy1} vs {uy2}"
|
||||
)
|
||||
assert uy1 == pytest.approx(
|
||||
uy2, rel=1e-9
|
||||
), f"runner Uy changed after interleaved material test: {uy1} vs {uy2}"
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ from otko.core import ( # noqa: E402
|
|||
)
|
||||
from otko.services.opensees_runner import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
def _moment_curvature_project(moment: float) -> Project:
|
||||
"""Two coincident nodes + a rectangular fibre section + one moment step.
|
||||
|
|
@ -36,32 +38,44 @@ def _moment_curvature_project(moment: float) -> Project:
|
|||
at node 2's DOF 3 (Rz). With a linear-elastic fibre material the
|
||||
curvature should be ``moment / (E·I)``.
|
||||
"""
|
||||
E = 30000.0 # Elastic modulus
|
||||
b, h = 10.0, 20.0 # width × depth (in)
|
||||
E = 30000.0 # Elastic modulus
|
||||
b, h = 10.0, 20.0 # width × depth (in)
|
||||
return Project(
|
||||
ndm=2, ndf=3,
|
||||
ndm=2,
|
||||
ndf=3,
|
||||
nodes=[
|
||||
Node(id=1, coords=(0, 0, 0),
|
||||
restraint=(True, True, False, False, False, True)),
|
||||
Node(id=2, coords=(0, 0, 0),
|
||||
restraint=(False, True, False, False, False, False)),
|
||||
Node(id=1, coords=(0, 0, 0), restraint=(True, True, False, False, False, True)),
|
||||
Node(id=2, coords=(0, 0, 0), restraint=(False, True, False, False, False, False)),
|
||||
],
|
||||
materials=[ElasticUniaxial(id=1, name="Elastic", E=E)],
|
||||
sections=[FiberSection(
|
||||
id=1, name="Rect", patches=[RectangularPatch(
|
||||
material_id=1, n_fib_y=20, n_fib_z=1,
|
||||
y_i=-h / 2, z_i=-b / 2, y_j=h / 2, z_j=b / 2,
|
||||
)],
|
||||
)],
|
||||
sections=[
|
||||
FiberSection(
|
||||
id=1,
|
||||
name="Rect",
|
||||
patches=[
|
||||
RectangularPatch(
|
||||
material_id=1,
|
||||
n_fib_y=20,
|
||||
n_fib_z=1,
|
||||
y_i=-h / 2,
|
||||
z_i=-b / 2,
|
||||
y_j=h / 2,
|
||||
z_j=b / 2,
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
elements=[ZeroLengthSectionElement(id=1, nodes=(1, 2), section_id=1)],
|
||||
time_series=[LinearTimeSeries(id=1, name="R")],
|
||||
load_patterns=[PlainLoadPattern(
|
||||
id=1, time_series_id=1,
|
||||
# NodalLoad.forces = (Fx, Fy, Fz, Mx, My, Mz). Moment around
|
||||
# z (= curvature driver in 2D) goes into index 5, not 2.
|
||||
nodal_loads=[NodalLoad(node_id=2,
|
||||
forces=(0, 0, 0, 0, 0, moment))],
|
||||
)],
|
||||
load_patterns=[
|
||||
PlainLoadPattern(
|
||||
id=1,
|
||||
time_series_id=1,
|
||||
# NodalLoad.forces = (Fx, Fy, Fz, Mx, My, Mz). Moment around
|
||||
# z (= curvature driver in 2D) goes into index 5, not 2.
|
||||
nodal_loads=[NodalLoad(node_id=2, forces=(0, 0, 0, 0, 0, moment))],
|
||||
)
|
||||
],
|
||||
analyses=[StaticCase(id=1, name="MK", pattern_ids=[1], n_steps=1)],
|
||||
)
|
||||
|
||||
|
|
@ -72,16 +86,16 @@ def test_zero_length_section_elastic_curvature_matches_closed_form() -> None:
|
|||
M = 500.0
|
||||
E = 30000.0
|
||||
b, h = 10.0, 20.0
|
||||
I = b * h ** 3 / 12.0
|
||||
I = b * h**3 / 12.0
|
||||
expected_kappa = M / (E * I)
|
||||
|
||||
proj = _moment_curvature_project(moment=M)
|
||||
result = OpenSeesRunner(proj).run(proj.analyses[0])
|
||||
# Rz at node 2 IS the curvature for a zero-length section.
|
||||
ux, uy, rz = result.node_disp[2][-1]
|
||||
assert rz == pytest.approx(expected_kappa, rel=5e-3), (
|
||||
f"κ = {rz:.6e}, expected {expected_kappa:.6e}"
|
||||
)
|
||||
assert rz == pytest.approx(
|
||||
expected_kappa, rel=5e-3
|
||||
), f"κ = {rz:.6e}, expected {expected_kappa:.6e}"
|
||||
|
||||
|
||||
def test_pushover_drives_rotation_for_moment_curvature() -> None:
|
||||
|
|
@ -94,23 +108,28 @@ def test_pushover_drives_rotation_for_moment_curvature() -> None:
|
|||
straight line through the origin with slope E·I.
|
||||
"""
|
||||
from otko.core import PushoverCase
|
||||
|
||||
E = 30000.0
|
||||
b, h = 10.0, 20.0
|
||||
I = b * h ** 3 / 12.0
|
||||
I = b * h**3 / 12.0
|
||||
target_kappa = 1e-5
|
||||
steps = 20
|
||||
|
||||
# PushoverCase with DisplacementControl scales the load pattern —
|
||||
# needs a *non-zero* reference moment at the control DOF.
|
||||
proj = _moment_curvature_project(moment=1.0)
|
||||
proj.analyses = [PushoverCase(
|
||||
id=1, name="MK-push",
|
||||
pattern_ids=[1],
|
||||
control_node=2, control_dof=3, # DOF 3 = Rz
|
||||
target_disp=target_kappa, # "displacement" == curvature here
|
||||
step_size=target_kappa / steps,
|
||||
base_nodes=[1],
|
||||
)]
|
||||
proj.analyses = [
|
||||
PushoverCase(
|
||||
id=1,
|
||||
name="MK-push",
|
||||
pattern_ids=[1],
|
||||
control_node=2,
|
||||
control_dof=3, # DOF 3 = Rz
|
||||
target_disp=target_kappa, # "displacement" == curvature here
|
||||
step_size=target_kappa / steps,
|
||||
base_nodes=[1],
|
||||
)
|
||||
]
|
||||
result = OpenSeesRunner(proj).run(proj.analyses[0])
|
||||
|
||||
# Every (κ, M) point must satisfy M = E·I·κ (1 % tolerance allows
|
||||
|
|
@ -119,9 +138,9 @@ def test_pushover_drives_rotation_for_moment_curvature() -> None:
|
|||
if abs(kappa) < 1e-12:
|
||||
continue
|
||||
expected_M = E * I * kappa
|
||||
assert moment == pytest.approx(expected_M, rel=1e-2), (
|
||||
f"at κ={kappa:.3e}: M={moment:.3e}, expected {expected_M:.3e}"
|
||||
)
|
||||
assert moment == pytest.approx(
|
||||
expected_M, rel=1e-2
|
||||
), f"at κ={kappa:.3e}: M={moment:.3e}, expected {expected_M:.3e}"
|
||||
# Terminal curvature must reach the target.
|
||||
assert result.control_disp[-1] == pytest.approx(target_kappa, rel=1e-3)
|
||||
|
||||
|
|
@ -145,6 +164,7 @@ def test_moment_curvature_with_constant_axial_preload() -> None:
|
|||
Steel01,
|
||||
StraightLayer,
|
||||
)
|
||||
|
||||
colWidth = 15.0
|
||||
colDepth = 24.0
|
||||
cover = 1.5
|
||||
|
|
@ -153,58 +173,104 @@ def test_moment_curvature_with_constant_axial_preload() -> None:
|
|||
z1 = colWidth / 2
|
||||
|
||||
proj = Project(
|
||||
ndm=2, ndf=3,
|
||||
ndm=2,
|
||||
ndf=3,
|
||||
nodes=[
|
||||
Node(id=1, coords=(0, 0, 0),
|
||||
restraint=(True, True, False, False, False, True)),
|
||||
Node(id=2, coords=(0, 0, 0),
|
||||
restraint=(False, True, False, False, False, False)),
|
||||
Node(id=1, coords=(0, 0, 0), restraint=(True, True, False, False, False, True)),
|
||||
Node(id=2, coords=(0, 0, 0), restraint=(False, True, False, False, False, False)),
|
||||
],
|
||||
materials=[
|
||||
Concrete01(id=1, name="Core",
|
||||
fpc=-6.0, epsc0=-0.004,
|
||||
fpcu=-5.0, epsU=-0.014),
|
||||
Concrete01(id=2, name="Cover",
|
||||
fpc=-5.0, epsc0=-0.002,
|
||||
fpcu=0.0, epsU=-0.006),
|
||||
Concrete01(id=1, name="Core", fpc=-6.0, epsc0=-0.004, fpcu=-5.0, epsU=-0.014),
|
||||
Concrete01(id=2, name="Cover", fpc=-5.0, epsc0=-0.002, fpcu=0.0, epsU=-0.006),
|
||||
Steel01(id=3, name="Steel", Fy=60.0, E0=30000.0, b=0.01),
|
||||
],
|
||||
sections=[FiberSection(
|
||||
id=1, name="RC",
|
||||
patches=[
|
||||
# Core (confined)
|
||||
RectangularPatch(material_id=1, n_fib_y=10, n_fib_z=1,
|
||||
y_i=cover - y1, z_i=cover - z1,
|
||||
y_j=y1 - cover, z_j=z1 - cover),
|
||||
# Top cover
|
||||
RectangularPatch(material_id=2, n_fib_y=10, n_fib_z=1,
|
||||
y_i=-y1, z_i=z1 - cover,
|
||||
y_j=y1, z_j=z1),
|
||||
# Bottom cover
|
||||
RectangularPatch(material_id=2, n_fib_y=10, n_fib_z=1,
|
||||
y_i=-y1, z_i=-z1,
|
||||
y_j=y1, z_j=cover - z1),
|
||||
# Left cover
|
||||
RectangularPatch(material_id=2, n_fib_y=2, n_fib_z=1,
|
||||
y_i=-y1, z_i=cover - z1,
|
||||
y_j=cover - y1, z_j=z1 - cover),
|
||||
# Right cover
|
||||
RectangularPatch(material_id=2, n_fib_y=2, n_fib_z=1,
|
||||
y_i=y1 - cover, z_i=cover - z1,
|
||||
y_j=y1, z_j=z1 - cover),
|
||||
],
|
||||
layers=[
|
||||
StraightLayer(material_id=3, n_bars=3, bar_area=As,
|
||||
y_start=y1 - cover, z_start=z1 - cover,
|
||||
y_end=y1 - cover, z_end=cover - z1),
|
||||
StraightLayer(material_id=3, n_bars=2, bar_area=As,
|
||||
y_start=0.0, z_start=z1 - cover,
|
||||
y_end=0.0, z_end=cover - z1),
|
||||
StraightLayer(material_id=3, n_bars=3, bar_area=As,
|
||||
y_start=cover - y1, z_start=z1 - cover,
|
||||
y_end=cover - y1, z_end=cover - z1),
|
||||
],
|
||||
)],
|
||||
sections=[
|
||||
FiberSection(
|
||||
id=1,
|
||||
name="RC",
|
||||
patches=[
|
||||
# Core (confined)
|
||||
RectangularPatch(
|
||||
material_id=1,
|
||||
n_fib_y=10,
|
||||
n_fib_z=1,
|
||||
y_i=cover - y1,
|
||||
z_i=cover - z1,
|
||||
y_j=y1 - cover,
|
||||
z_j=z1 - cover,
|
||||
),
|
||||
# Top cover
|
||||
RectangularPatch(
|
||||
material_id=2,
|
||||
n_fib_y=10,
|
||||
n_fib_z=1,
|
||||
y_i=-y1,
|
||||
z_i=z1 - cover,
|
||||
y_j=y1,
|
||||
z_j=z1,
|
||||
),
|
||||
# Bottom cover
|
||||
RectangularPatch(
|
||||
material_id=2,
|
||||
n_fib_y=10,
|
||||
n_fib_z=1,
|
||||
y_i=-y1,
|
||||
z_i=-z1,
|
||||
y_j=y1,
|
||||
z_j=cover - z1,
|
||||
),
|
||||
# Left cover
|
||||
RectangularPatch(
|
||||
material_id=2,
|
||||
n_fib_y=2,
|
||||
n_fib_z=1,
|
||||
y_i=-y1,
|
||||
z_i=cover - z1,
|
||||
y_j=cover - y1,
|
||||
z_j=z1 - cover,
|
||||
),
|
||||
# Right cover
|
||||
RectangularPatch(
|
||||
material_id=2,
|
||||
n_fib_y=2,
|
||||
n_fib_z=1,
|
||||
y_i=y1 - cover,
|
||||
z_i=cover - z1,
|
||||
y_j=y1,
|
||||
z_j=z1 - cover,
|
||||
),
|
||||
],
|
||||
layers=[
|
||||
StraightLayer(
|
||||
material_id=3,
|
||||
n_bars=3,
|
||||
bar_area=As,
|
||||
y_start=y1 - cover,
|
||||
z_start=z1 - cover,
|
||||
y_end=y1 - cover,
|
||||
z_end=cover - z1,
|
||||
),
|
||||
StraightLayer(
|
||||
material_id=3,
|
||||
n_bars=2,
|
||||
bar_area=As,
|
||||
y_start=0.0,
|
||||
z_start=z1 - cover,
|
||||
y_end=0.0,
|
||||
z_end=cover - z1,
|
||||
),
|
||||
StraightLayer(
|
||||
material_id=3,
|
||||
n_bars=3,
|
||||
bar_area=As,
|
||||
y_start=cover - y1,
|
||||
z_start=z1 - cover,
|
||||
y_end=cover - y1,
|
||||
z_end=cover - z1,
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
elements=[ZeroLengthSectionElement(id=1, nodes=(1, 2), section_id=1)],
|
||||
time_series=[
|
||||
ConstantTimeSeries(id=1, name="AxialP"),
|
||||
|
|
@ -212,14 +278,16 @@ def test_moment_curvature_with_constant_axial_preload() -> None:
|
|||
],
|
||||
load_patterns=[
|
||||
PlainLoadPattern(
|
||||
id=1, name="AxialP", time_series_id=1,
|
||||
nodal_loads=[NodalLoad(node_id=2,
|
||||
forces=(-180.0, 0, 0, 0, 0, 0))],
|
||||
id=1,
|
||||
name="AxialP",
|
||||
time_series_id=1,
|
||||
nodal_loads=[NodalLoad(node_id=2, forces=(-180.0, 0, 0, 0, 0, 0))],
|
||||
),
|
||||
PlainLoadPattern(
|
||||
id=2, name="RefMoment", time_series_id=2,
|
||||
nodal_loads=[NodalLoad(node_id=2,
|
||||
forces=(0, 0, 0, 0, 0, 1.0))],
|
||||
id=2,
|
||||
name="RefMoment",
|
||||
time_series_id=2,
|
||||
nodal_loads=[NodalLoad(node_id=2, forces=(0, 0, 0, 0, 0, 1.0))],
|
||||
),
|
||||
],
|
||||
analyses=[],
|
||||
|
|
@ -227,25 +295,29 @@ def test_moment_curvature_with_constant_axial_preload() -> None:
|
|||
# Yield curvature estimate from the Tcl example.
|
||||
d = colDepth - cover
|
||||
Ky = 60.0 / 30000.0 / (0.7 * d)
|
||||
target = Ky * 15 # μ = 15
|
||||
proj.analyses = [PushoverCase(
|
||||
id=1, name="MK",
|
||||
pattern_ids=[1, 2],
|
||||
control_node=2, control_dof=3,
|
||||
target_disp=target,
|
||||
step_size=target / 100,
|
||||
base_nodes=[1],
|
||||
test="NormUnbalance",
|
||||
tolerance=1e-9, max_iter=25,
|
||||
)]
|
||||
target = Ky * 15 # μ = 15
|
||||
proj.analyses = [
|
||||
PushoverCase(
|
||||
id=1,
|
||||
name="MK",
|
||||
pattern_ids=[1, 2],
|
||||
control_node=2,
|
||||
control_dof=3,
|
||||
target_disp=target,
|
||||
step_size=target / 100,
|
||||
base_nodes=[1],
|
||||
test="NormUnbalance",
|
||||
tolerance=1e-9,
|
||||
max_iter=25,
|
||||
)
|
||||
]
|
||||
|
||||
result = OpenSeesRunner(proj).run(proj.analyses[0])
|
||||
|
||||
# Analysis must actually converge past yield (not collapse at
|
||||
# step 1 like it did before the two-stage preload fix).
|
||||
assert len(result.control_disp) > 50, (
|
||||
f"Converged for only {len(result.control_disp)} of 100 steps — "
|
||||
"preload stage broken?"
|
||||
f"Converged for only {len(result.control_disp)} of 100 steps — " "preload stage broken?"
|
||||
)
|
||||
# Curvature reached or passed yield.
|
||||
kappa_max = float(max(abs(k) for k in result.control_disp))
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ pytest.importorskip("openseespy")
|
|||
from otko.services import load_project # noqa: E402
|
||||
from otko.services.opensees_runner import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
def test_moment_curvature_example_round_trips_and_converges(tmp_path) -> None: # type: ignore[no-untyped-def]
|
||||
"""build_moment_curvature() → save → load → run → expected shape."""
|
||||
|
|
@ -21,11 +23,13 @@ def test_moment_curvature_example_round_trips_and_converges(tmp_path) -> None:
|
|||
MU,
|
||||
NUM_INCR,
|
||||
)
|
||||
|
||||
proj = build_moment_curvature()
|
||||
proj.validate_references()
|
||||
|
||||
# Save + reload — catches schema drift.
|
||||
from otko.services import save_project
|
||||
|
||||
path = tmp_path / "mk.osmodel"
|
||||
save_project(proj, path)
|
||||
reloaded = load_project(path)
|
||||
|
|
@ -39,15 +43,14 @@ def test_moment_curvature_example_round_trips_and_converges(tmp_path) -> None:
|
|||
ky = (FY / E_STEEL) / (0.7 * d)
|
||||
|
||||
# At least half of the NUM_INCR pushover steps converged.
|
||||
assert len(result.control_disp) > NUM_INCR * 0.5, (
|
||||
"Pushover bailed out prematurely — check Concrete01 softening"
|
||||
)
|
||||
assert (
|
||||
len(result.control_disp) > NUM_INCR * 0.5
|
||||
), "Pushover bailed out prematurely — check Concrete01 softening"
|
||||
# Reached the mu * Ky target.
|
||||
assert result.control_disp[-1] == pytest.approx(MU * ky, rel=1e-2)
|
||||
# Moment at yield curvature is plausible: > 3 kip·in and < 10 kip·in per rebar
|
||||
# → for 8 bars total, the section moment capacity is roughly O(3000-6000) kip·in.
|
||||
peak_moment = max(abs(m) for m in result.base_shear)
|
||||
assert 2000 < peak_moment < 10000, (
|
||||
f"Peak moment {peak_moment:.1f} kip·in is outside the "
|
||||
"expected RC section range"
|
||||
f"Peak moment {peak_moment:.1f} kip·in is outside the " "expected RC section range"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ from otko.core import ( # noqa: E402
|
|||
)
|
||||
from otko.services import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
def _cantilever() -> Project:
|
||||
length = 5.0
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ pytest.importorskip("openseespy")
|
|||
from otko.services import load_project, save_project # noqa: E402
|
||||
from otko.services.opensees_runner import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
def test_rc_frame_earthquake_runs_and_has_oscillatory_response(tmp_path) -> None: # type: ignore[no-untyped-def]
|
||||
"""Synthetic ground motion produces bounded, oscillatory response."""
|
||||
|
|
@ -20,6 +22,7 @@ def test_rc_frame_earthquake_runs_and_has_oscillatory_response(tmp_path) -> None
|
|||
DT,
|
||||
N_PTS,
|
||||
)
|
||||
|
||||
proj = build_rc_frame_earthquake()
|
||||
proj.validate_references()
|
||||
|
||||
|
|
@ -33,9 +36,9 @@ def test_rc_frame_earthquake_runs_and_has_oscillatory_response(tmp_path) -> None
|
|||
|
||||
# Simulation covers most of the 4-second record (ModifiedNewton
|
||||
# fallback may trim a few steps at stiffness jumps; we allow that).
|
||||
assert result.n_steps >= int(0.9 * N_PTS), (
|
||||
f"Only {result.n_steps}/{N_PTS} steps — fallback didn't recover"
|
||||
)
|
||||
assert result.n_steps >= int(
|
||||
0.9 * N_PTS
|
||||
), f"Only {result.n_steps}/{N_PTS} steps — fallback didn't recover"
|
||||
|
||||
# Node 3 Ux history: bounded, non-trivial, some positive AND some
|
||||
# negative (oscillation confirms the base excitation actually
|
||||
|
|
@ -50,4 +53,4 @@ def test_rc_frame_earthquake_runs_and_has_oscillatory_response(tmp_path) -> None
|
|||
# Uy on the top nodes — small compared to Ux (gravity holds, base
|
||||
# excitation is horizontal).
|
||||
uy = h3[:, 1]
|
||||
assert max(abs(uy)) < 1.0, f"max |Uy| = {max(abs(uy)):.4f} in too large"
|
||||
assert max(abs(uy)) < 1.0, f"max |Uy| = {max(abs(uy)):.4f} in too large"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ pytest.importorskip("openseespy")
|
|||
from otko.services import load_project, save_project # noqa: E402
|
||||
from otko.services.opensees_runner import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
def test_rc_frame_gravity_matches_opensees_reference(tmp_path) -> None: # type: ignore[no-untyped-def]
|
||||
"""Build → save → load → run → compare to OpenSees Tcl output.
|
||||
|
|
@ -21,6 +23,7 @@ def test_rc_frame_gravity_matches_opensees_reference(tmp_path) -> None: # type:
|
|||
load stepped onto each top node).
|
||||
"""
|
||||
from examples.rc_frame_gravity import build_rc_frame_gravity, P_LOAD
|
||||
|
||||
proj = build_rc_frame_gravity()
|
||||
proj.validate_references()
|
||||
|
||||
|
|
@ -54,7 +57,7 @@ def test_rc_frame_gravity_matches_opensees_reference(tmp_path) -> None: # type:
|
|||
# OpenSees equilibrium sign, i.e. the first component equals the
|
||||
# applied vertical load on end i.
|
||||
col1 = result.element_forces[1][-1]
|
||||
assert abs(col1[0]) == pytest.approx(P_LOAD, abs=1.0), (
|
||||
f"Column 1 axial {col1[0]:.2f} ≠ ±{P_LOAD} kip"
|
||||
)
|
||||
assert abs(col1[0]) == pytest.approx(
|
||||
P_LOAD, abs=1.0
|
||||
), f"Column 1 axial {col1[0]:.2f} ≠ ±{P_LOAD} kip"
|
||||
assert abs(col1[3]) == pytest.approx(P_LOAD, abs=1.0)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ pytest.importorskip("openseespy")
|
|||
from otko.services import load_project, save_project # noqa: E402
|
||||
from otko.services.opensees_runner import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
def test_rc_frame_pushover_reaches_target_with_fallback(tmp_path) -> None: # type: ignore[no-untyped-def]
|
||||
"""The 15-in pushover requires the ModifiedNewton convergence
|
||||
|
|
@ -22,6 +24,7 @@ def test_rc_frame_pushover_reaches_target_with_fallback(tmp_path) -> None: # ty
|
|||
D_TARGET,
|
||||
)
|
||||
from otko.core import PushoverCase
|
||||
|
||||
proj = build_rc_frame_pushover()
|
||||
proj.validate_references()
|
||||
|
||||
|
|
@ -35,7 +38,7 @@ def test_rc_frame_pushover_reaches_target_with_fallback(tmp_path) -> None: # ty
|
|||
push_case = next(c for c in reloaded.analyses if isinstance(c, PushoverCase))
|
||||
result = OpenSeesRunner(reloaded).run(push_case)
|
||||
|
||||
expected_pts = int(D_TARGET / D_STEP) + 1 # 151 including step 0
|
||||
expected_pts = int(D_TARGET / D_STEP) + 1 # 151 including step 0
|
||||
assert len(result.control_disp) == expected_pts, (
|
||||
f"Got {len(result.control_disp)} points, expected {expected_pts} — "
|
||||
"ModifiedNewton fallback probably didn't kick in."
|
||||
|
|
@ -64,6 +67,6 @@ def test_rc_frame_pushover_reaches_target_with_fallback(tmp_path) -> None: # ty
|
|||
# of the pushover (step 1) should be close to P = 180 kip.
|
||||
step1_col1 = result.element_forces[1][1]
|
||||
axial_step1 = abs(step1_col1[0])
|
||||
assert 100.0 < axial_step1 < 260.0, (
|
||||
f"Col 1 axial at step 1 = {axial_step1:.1f} kip — gravity preload lost?"
|
||||
)
|
||||
assert (
|
||||
100.0 < axial_step1 < 260.0
|
||||
), f"Col 1 axial at step 1 = {axial_step1:.1f} kip — gravity preload lost?"
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ from otko.core import ( # noqa: E402
|
|||
)
|
||||
from otko.services import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
W = 1.57 # drive (rad/s)
|
||||
DT_SERIES = 0.01
|
||||
NPTS = 1601 # 16 s
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ from otko.core import ( # noqa: E402
|
|||
)
|
||||
from otko.services import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
def test_sdof_pole_first_frequency_matches_kspring_over_m() -> None:
|
||||
"""Vertical pole, mass at top, fixed base. ω₁ = √(3EI/(mL³))."""
|
||||
|
|
@ -36,11 +38,11 @@ def test_sdof_pole_first_frequency_matches_kspring_over_m() -> None:
|
|||
m_tip = 1000.0
|
||||
|
||||
project = Project(
|
||||
ndm=2, ndf=3,
|
||||
ndm=2,
|
||||
ndf=3,
|
||||
nodes=[
|
||||
Node(id=1, coords=(0.0, 0.0, 0.0), restraint=(True, True, False, False, False, True)),
|
||||
Node(id=2, coords=(0.0, L, 0.0),
|
||||
mass=(m_tip, m_tip, 0.0, 0.0, 0.0, 0.0)),
|
||||
Node(id=2, coords=(0.0, L, 0.0), mass=(m_tip, m_tip, 0.0, 0.0, 0.0, 0.0)),
|
||||
],
|
||||
sections=[ElasticSection(id=1, E=E, A=A, Iz=I)],
|
||||
elements=[ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1)],
|
||||
|
|
@ -54,9 +56,9 @@ def test_sdof_pole_first_frequency_matches_kspring_over_m() -> None:
|
|||
omega_expected = math.sqrt(k / m_tip)
|
||||
|
||||
omega_actual = float(results.angular_frequencies[0])
|
||||
assert math.isclose(omega_actual, omega_expected, rel_tol=5e-3), (
|
||||
f"ω₁ mismatch: expected {omega_expected:.4f} rad/s, got {omega_actual:.4f} rad/s"
|
||||
)
|
||||
assert math.isclose(
|
||||
omega_actual, omega_expected, rel_tol=5e-3
|
||||
), f"ω₁ mismatch: expected {omega_expected:.4f} rad/s, got {omega_actual:.4f} rad/s"
|
||||
|
||||
|
||||
def test_runner_falls_back_to_lapack_for_small_models() -> None:
|
||||
|
|
@ -64,11 +66,11 @@ def test_runner_falls_back_to_lapack_for_small_models() -> None:
|
|||
from unittest.mock import MagicMock
|
||||
|
||||
project = Project(
|
||||
ndm=2, ndf=3,
|
||||
ndm=2,
|
||||
ndf=3,
|
||||
nodes=[
|
||||
Node(id=1, coords=(0.0, 0.0, 0.0), restraint=(True, True, False, False, False, True)),
|
||||
Node(id=2, coords=(0.0, 3.0, 0.0),
|
||||
mass=(1000.0, 1000.0, 0.0, 0.0, 0.0, 0.0)),
|
||||
Node(id=2, coords=(0.0, 3.0, 0.0), mass=(1000.0, 1000.0, 0.0, 0.0, 0.0, 0.0)),
|
||||
],
|
||||
sections=[ElasticSection(id=1, E=200e9, A=0.01, Iz=8.333e-6)],
|
||||
elements=[ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1)],
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ from otko.core import ( # noqa: E402
|
|||
)
|
||||
from otko.services import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
def test_cantilever_tip_deflection_matches_closed_form() -> None:
|
||||
"""Horizontal cantilever in 2D: P at tip, expect δ = P L³ / (3 E I)."""
|
||||
|
|
@ -34,7 +36,8 @@ def test_cantilever_tip_deflection_matches_closed_form() -> None:
|
|||
I = 8.333e-6
|
||||
|
||||
project = Project(
|
||||
ndm=2, ndf=3,
|
||||
ndm=2,
|
||||
ndf=3,
|
||||
nodes=[
|
||||
Node(id=1, coords=(0.0, 0.0, 0.0), restraint=(True, True, False, False, False, True)),
|
||||
Node(id=2, coords=(L, 0.0, 0.0)),
|
||||
|
|
@ -44,7 +47,8 @@ def test_cantilever_tip_deflection_matches_closed_form() -> None:
|
|||
time_series=[LinearTimeSeries(id=1)],
|
||||
load_patterns=[
|
||||
PlainLoadPattern(
|
||||
id=1, time_series_id=1,
|
||||
id=1,
|
||||
time_series_id=1,
|
||||
# Fy at the tip (downward); Rz of node 1 is restrained, others free.
|
||||
nodal_loads=[NodalLoad(node_id=2, forces=(0.0, -P, 0.0, 0.0, 0.0, 0.0))],
|
||||
)
|
||||
|
|
@ -54,12 +58,12 @@ def test_cantilever_tip_deflection_matches_closed_form() -> None:
|
|||
case = StaticCase(id=1, name="Cantilever", pattern_ids=[1])
|
||||
results = OpenSeesRunner(project).run(case)
|
||||
|
||||
delta_expected = -P * L**3 / (3.0 * E * I) # negative (downward)
|
||||
delta_actual = results.disp(node_id=2, dof=2) # Uy at node 2
|
||||
delta_expected = -P * L**3 / (3.0 * E * I) # negative (downward)
|
||||
delta_actual = results.disp(node_id=2, dof=2) # Uy at node 2
|
||||
|
||||
assert math.isclose(delta_actual, delta_expected, rel_tol=1e-3), (
|
||||
f"Tip deflection mismatch: expected {delta_expected:.6e}, got {delta_actual:.6e}"
|
||||
)
|
||||
assert math.isclose(
|
||||
delta_actual, delta_expected, rel_tol=1e-3
|
||||
), f"Tip deflection mismatch: expected {delta_expected:.6e}, got {delta_actual:.6e}"
|
||||
|
||||
# Reaction at the support equals the applied load.
|
||||
Fy_reaction = results.node_reaction[1][0, 1]
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ from otko.core import ( # noqa: E402
|
|||
)
|
||||
from otko.services import OpenSeesRunner # noqa: E402
|
||||
|
||||
pytestmark = pytest.mark.slow
|
||||
|
||||
|
||||
def test_sdof_free_vibration_matches_cosine(tmp_path) -> None: # type: ignore[no-untyped-def]
|
||||
"""Initial displacement, no external load, no damping → u(t) = u₀ cos(ωt)."""
|
||||
|
|
@ -52,25 +54,27 @@ def test_sdof_free_vibration_matches_cosine(tmp_path) -> None: # type: ignore[n
|
|||
u0 = F0 / k
|
||||
|
||||
project = Project(
|
||||
ndm=2, ndf=3,
|
||||
ndm=2,
|
||||
ndf=3,
|
||||
nodes=[
|
||||
Node(id=1, coords=(0.0, 0.0, 0.0), restraint=(True, True, False, False, False, True)),
|
||||
Node(id=2, coords=(0.0, L, 0.0),
|
||||
mass=(m_tip, m_tip, 0.0, 0.0, 0.0, 0.0)),
|
||||
Node(id=2, coords=(0.0, L, 0.0), mass=(m_tip, m_tip, 0.0, 0.0, 0.0, 0.0)),
|
||||
],
|
||||
sections=[ElasticSection(id=1, E=E, A=A, Iz=I)],
|
||||
elements=[ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1)],
|
||||
time_series=[
|
||||
ConstantTimeSeries(id=1, factor=1.0), # static initial
|
||||
LinearTimeSeries(id=2), # transient (zero load)
|
||||
LinearTimeSeries(id=2), # transient (zero load)
|
||||
],
|
||||
load_patterns=[
|
||||
PlainLoadPattern(
|
||||
id=1, time_series_id=1,
|
||||
id=1,
|
||||
time_series_id=1,
|
||||
nodal_loads=[NodalLoad(node_id=2, forces=(F0, 0.0, 0.0, 0.0, 0.0, 0.0))],
|
||||
),
|
||||
PlainLoadPattern(
|
||||
id=2, time_series_id=2,
|
||||
id=2,
|
||||
time_series_id=2,
|
||||
nodal_loads=[NodalLoad(node_id=2, forces=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0))],
|
||||
),
|
||||
],
|
||||
|
|
@ -85,8 +89,11 @@ def test_sdof_free_vibration_matches_cosine(tmp_path) -> None: # type: ignore[n
|
|||
n_steps = 200
|
||||
dt = T / 50.0
|
||||
case = TransientCase(
|
||||
id=2, name="FreeVib", pattern_ids=[2],
|
||||
dt=dt, n_steps=n_steps,
|
||||
id=2,
|
||||
name="FreeVib",
|
||||
pattern_ids=[2],
|
||||
dt=dt,
|
||||
n_steps=n_steps,
|
||||
# Average-acceleration Newmark is unconditionally stable.
|
||||
integrator_params=(0.5, 0.25),
|
||||
)
|
||||
|
|
@ -111,7 +118,8 @@ def test_sdof_free_vibration_matches_cosine(tmp_path) -> None: # type: ignore[n
|
|||
def test_transient_writes_hdf5_with_time_dataset(tmp_path) -> None: # type: ignore[no-untyped-def]
|
||||
"""Transient run must produce an HDF5 with a /time dataset of length n_steps."""
|
||||
project = Project(
|
||||
ndm=2, ndf=3,
|
||||
ndm=2,
|
||||
ndf=3,
|
||||
nodes=[
|
||||
Node(id=1, coords=(0.0, 0.0, 0.0), restraint=(True, True, False, False, False, True)),
|
||||
Node(id=2, coords=(0.0, 3.0, 0.0), mass=(1000.0,) * 3 + (0.0,) * 3),
|
||||
|
|
@ -121,7 +129,8 @@ def test_transient_writes_hdf5_with_time_dataset(tmp_path) -> None: # type: ign
|
|||
time_series=[LinearTimeSeries(id=1)],
|
||||
load_patterns=[
|
||||
PlainLoadPattern(
|
||||
id=1, time_series_id=1,
|
||||
id=1,
|
||||
time_series_id=1,
|
||||
nodal_loads=[NodalLoad(node_id=2, forces=(10.0, 0, 0, 0, 0, 0))],
|
||||
)
|
||||
],
|
||||
|
|
|
|||
Loading…
Reference in a new issue