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
236 lines
8.4 KiB
Python
236 lines
8.4 KiB
Python
"""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
|