feat: initial otko import
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
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
This commit is contained in:
commit
612936a00b
540 changed files with 174136 additions and 0 deletions
236
tests/unit/test_pattern_factors.py
Normal file
236
tests/unit/test_pattern_factors.py
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
"""Case-level pattern factors (Static/Transient/Pushover).
|
||||
|
||||
Unit + translation + export coverage. No Qt, no solver.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from otko.core import (
|
||||
ConstantTimeSeries,
|
||||
LinearTimeSeries,
|
||||
NodalLoad,
|
||||
Node,
|
||||
PathTimeSeries,
|
||||
PlainLoadPattern,
|
||||
Project,
|
||||
PushoverCase,
|
||||
StaticCase,
|
||||
Steel01,
|
||||
TransientCase,
|
||||
TrussElement,
|
||||
UniformExcitationPattern,
|
||||
)
|
||||
from otko.services.export import export_opspy, export_tcl
|
||||
from otko.services.opensees_runner import OpenSeesRunner
|
||||
|
||||
|
||||
def _truss_2d() -> Project:
|
||||
return Project(
|
||||
ndm=2,
|
||||
ndf=2,
|
||||
nodes=[
|
||||
Node(id=1, coords=(0, 0, 0), restraint=(True, True, False, False, False, False)),
|
||||
Node(id=2, coords=(4, 0, 0), restraint=(False, True, False, False, False, False)),
|
||||
Node(id=3, coords=(2, 3, 0)),
|
||||
],
|
||||
materials=[Steel01(id=1, Fy=420e6, E0=200e9, b=0.01)],
|
||||
elements=[
|
||||
TrussElement(id=1, nodes=(1, 3), area=1e-3, material_id=1),
|
||||
TrussElement(id=2, nodes=(2, 3), area=1e-3, material_id=1),
|
||||
],
|
||||
time_series=[LinearTimeSeries(id=1)],
|
||||
load_patterns=[
|
||||
PlainLoadPattern(
|
||||
id=1,
|
||||
time_series_id=1,
|
||||
nodal_loads=[NodalLoad(node_id=3, forces=(0, -1000, 0, 0, 0, 0))],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class TestValidation:
|
||||
@pytest.mark.parametrize(
|
||||
"make",
|
||||
[
|
||||
lambda **kw: StaticCase(id=1, pattern_ids=[1], **kw),
|
||||
lambda **kw: TransientCase(id=1, pattern_ids=[1], dt=0.01, n_steps=5, **kw),
|
||||
lambda **kw: PushoverCase(
|
||||
id=1,
|
||||
pattern_ids=[1],
|
||||
control_node=1,
|
||||
control_dof=1,
|
||||
target_disp=0.1,
|
||||
**kw,
|
||||
),
|
||||
],
|
||||
ids=["static", "transient", "pushover"],
|
||||
)
|
||||
def test_stray_key_rejected_naming_id(self, make) -> None: # type: ignore[no-untyped-def]
|
||||
with pytest.raises(ValueError, match="99"):
|
||||
make(pattern_factors={99: 2.0})
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"make",
|
||||
[
|
||||
lambda **kw: StaticCase(id=1, pattern_ids=[1], **kw),
|
||||
lambda **kw: TransientCase(id=1, pattern_ids=[1], dt=0.01, n_steps=5, **kw),
|
||||
lambda **kw: PushoverCase(
|
||||
id=1,
|
||||
pattern_ids=[1],
|
||||
control_node=1,
|
||||
control_dof=1,
|
||||
target_disp=0.1,
|
||||
**kw,
|
||||
),
|
||||
],
|
||||
ids=["static", "transient", "pushover"],
|
||||
)
|
||||
def test_empty_map_round_trip(self, make) -> None: # type: ignore[no-untyped-def]
|
||||
case = make()
|
||||
assert case.pattern_factors == {}
|
||||
dump = case.model_dump()
|
||||
assert dump["pattern_factors"] == {}
|
||||
assert type(case).model_validate(dump).pattern_factors == {}
|
||||
# JSON keys round-trip back to int keys.
|
||||
loaded = type(case).model_validate_json(case.model_dump_json())
|
||||
assert loaded.pattern_factors == {}
|
||||
assert all(isinstance(k, int) for k in loaded.pattern_factors)
|
||||
|
||||
def test_any_float_allowed(self) -> None:
|
||||
case = StaticCase(id=1, pattern_ids=[1], pattern_factors={1: -2.5})
|
||||
assert case.pattern_factors[1] == -2.5
|
||||
zero = StaticCase(id=2, pattern_ids=[1], pattern_factors={1: 0.0})
|
||||
assert zero.pattern_factors[1] == 0.0
|
||||
|
||||
|
||||
class TestTranslation:
|
||||
def test_factor_doubled(self) -> None:
|
||||
ops = MagicMock()
|
||||
runner = OpenSeesRunner(_truss_2d(), ops_module=ops)
|
||||
runner.build()
|
||||
runner._emit_patterns_for_case([1], {1: 2.0})
|
||||
ops.timeSeries.assert_called_with("Linear", 1, "-factor", 2.0)
|
||||
|
||||
def test_legacy_empty_map(self) -> None:
|
||||
ops = MagicMock()
|
||||
runner = OpenSeesRunner(_truss_2d(), ops_module=ops)
|
||||
runner.build()
|
||||
runner._emit_patterns_for_case([1], {})
|
||||
ops.timeSeries.assert_called_with("Linear", 1, "-factor", 1.0)
|
||||
|
||||
def test_legacy_none_map(self) -> None:
|
||||
ops = MagicMock()
|
||||
runner = OpenSeesRunner(_truss_2d(), ops_module=ops)
|
||||
runner.build()
|
||||
runner._emit_patterns_for_case([1])
|
||||
ops.timeSeries.assert_called_with("Linear", 1, "-factor", 1.0)
|
||||
ops.pattern.assert_called_with("Plain", 1, 1)
|
||||
|
||||
def test_constant_and_path_scaled(self) -> None:
|
||||
proj = Project(
|
||||
ndm=2,
|
||||
ndf=2,
|
||||
nodes=[
|
||||
Node(
|
||||
id=1,
|
||||
coords=(0, 0, 0),
|
||||
restraint=(True, True, False, False, False, False),
|
||||
),
|
||||
Node(id=2, coords=(1, 0, 0)),
|
||||
],
|
||||
materials=[Steel01(id=1, Fy=250e6, E0=200e9, b=0.01)],
|
||||
elements=[TrussElement(id=1, nodes=(1, 2), area=1e-3, material_id=1)],
|
||||
time_series=[
|
||||
ConstantTimeSeries(id=1, factor=3.0),
|
||||
PathTimeSeries(id=2, dt=0.1, values=[0.0, 1.0], factor=4.0),
|
||||
],
|
||||
load_patterns=[
|
||||
PlainLoadPattern(
|
||||
id=1,
|
||||
time_series_id=1,
|
||||
nodal_loads=[NodalLoad(node_id=2, forces=(1, 0, 0, 0, 0, 0))],
|
||||
),
|
||||
PlainLoadPattern(
|
||||
id=2,
|
||||
time_series_id=2,
|
||||
nodal_loads=[NodalLoad(node_id=2, forces=(1, 0, 0, 0, 0, 0))],
|
||||
),
|
||||
],
|
||||
)
|
||||
ops = MagicMock()
|
||||
runner = OpenSeesRunner(proj, ops_module=ops)
|
||||
runner.build()
|
||||
runner._emit_patterns_for_case([1, 2], {1: 2.0, 2: 0.5})
|
||||
ops.timeSeries.assert_any_call("Constant", 1, "-factor", 6.0)
|
||||
ops.timeSeries.assert_any_call("Path", 2, "-dt", 0.1, "-values", 0.0, 1.0, "-factor", 2.0)
|
||||
|
||||
def test_uniform_excitation_fact_scaled(self) -> None:
|
||||
proj = Project(
|
||||
ndm=2,
|
||||
ndf=2,
|
||||
nodes=[
|
||||
Node(
|
||||
id=1,
|
||||
coords=(0, 0, 0),
|
||||
restraint=(True, True, False, False, False, False),
|
||||
),
|
||||
Node(id=2, coords=(1, 0, 0)),
|
||||
],
|
||||
materials=[Steel01(id=1, Fy=250e6, E0=200e9, b=0.01)],
|
||||
elements=[TrussElement(id=1, nodes=(1, 2), area=1e-3, material_id=1)],
|
||||
time_series=[LinearTimeSeries(id=5, factor=1.5)],
|
||||
load_patterns=[
|
||||
UniformExcitationPattern(id=1, direction=1, accel_series_id=5, factor=2.0)
|
||||
],
|
||||
)
|
||||
ops = MagicMock()
|
||||
runner = OpenSeesRunner(proj, ops_module=ops)
|
||||
runner.build()
|
||||
runner._emit_patterns_for_case([1], {1: 3.0})
|
||||
# Pattern -fact scales: 2.0 * 3.0 = 6.0; series stays unscaled.
|
||||
ops.timeSeries.assert_called_with("Linear", 5, "-factor", 1.5)
|
||||
ops.pattern.assert_called_with("UniformExcitation", 1, 1, "-accel", 5, "-fact", 6.0)
|
||||
|
||||
def test_uniform_excitation_legacy_omits_fact(self) -> None:
|
||||
proj = Project(
|
||||
ndm=2,
|
||||
ndf=2,
|
||||
nodes=[
|
||||
Node(
|
||||
id=1,
|
||||
coords=(0, 0, 0),
|
||||
restraint=(True, True, False, False, False, False),
|
||||
),
|
||||
Node(id=2, coords=(1, 0, 0)),
|
||||
],
|
||||
materials=[Steel01(id=1, Fy=250e6, E0=200e9, b=0.01)],
|
||||
elements=[TrussElement(id=1, nodes=(1, 2), area=1e-3, material_id=1)],
|
||||
time_series=[LinearTimeSeries(id=5)],
|
||||
load_patterns=[UniformExcitationPattern(id=1, direction=1, accel_series_id=5)],
|
||||
)
|
||||
ops = MagicMock()
|
||||
runner = OpenSeesRunner(proj, ops_module=ops)
|
||||
runner.build()
|
||||
runner._emit_patterns_for_case([1], {})
|
||||
ops.pattern.assert_called_with("UniformExcitation", 1, 1, "-accel", 5)
|
||||
|
||||
|
||||
class TestExport:
|
||||
def test_export_contains_scaled_factor(self) -> None:
|
||||
proj = _truss_2d()
|
||||
proj.analyses.append(StaticCase(id=1, name="s", pattern_ids=[1], pattern_factors={1: 2.0}))
|
||||
py = export_opspy(proj, case_id=1)
|
||||
assert 'ops.timeSeries("Linear", 1, "-factor", 2.0)' in py
|
||||
tcl = export_tcl(proj, case_id=1)
|
||||
assert "timeSeries Linear 1 -factor 2.0" in tcl
|
||||
|
||||
def test_export_legacy_factor(self) -> None:
|
||||
proj = _truss_2d()
|
||||
proj.analyses.append(StaticCase(id=1, name="s", pattern_ids=[1]))
|
||||
py = export_opspy(proj, case_id=1)
|
||||
assert 'ops.timeSeries("Linear", 1, "-factor", 1.0)' in py
|
||||
Loading…
Reference in a new issue