otko/tests/gui/test_analysis_commands.py
smill f361fee969 feat: named case-result load combinations with full GUI support
Snapshots the current development tree, headlined by proper load
combinations (user request): a reusable LoadCombination entity of
weighted completed static-case results (e.g. 1.2xDead + 1.6xLive).

- core: LoadCombination/LoadCombinationItem entities, Project
  integration (lookup, unique ids, reference validation)
- services: combinations.py (linear superposition + envelope),
  exported via services __init__
- commands: undoable Add/Delete/Update for combinations
- GUI: Load Combinations manager dialog, Run-dialog evaluation,
  envelope display in Results panel, Combinations tab in Table dock
- tests: unit coverage (validation, math, error paths) + integration
  superposition check vs a single factored run
2026-09-11 13:19:59 -04:00

151 lines
5.1 KiB
Python
Raw 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.

"""Unit tests for analysis case commands."""
from __future__ import annotations
import pytest
pytest.importorskip("PySide6")
from otko.commands import ( # noqa: E402
AddAnalysisCasesCommand,
DeleteAnalysisCasesCommand,
UpdateAnalysisCaseCommand,
)
from otko.core import ( # noqa: E402
LinearTimeSeries,
ModalCase,
NodalLoad,
Node,
PlainLoadPattern,
StaticCase,
TransientCase,
)
from otko.viewmodels import ProjectViewModel # noqa: E402
def _vm_with_pattern() -> ProjectViewModel:
vm = ProjectViewModel()
vm.new_project()
vm.project.nodes.append(Node(id=1, coords=(0, 0, 0)))
vm.project.time_series.append(LinearTimeSeries(id=1))
vm.project.load_patterns.append(
PlainLoadPattern(
id=1, time_series_id=1, nodal_loads=[NodalLoad(node_id=1, forces=(100, 0, 0, 0, 0, 0))]
)
)
return vm
# ──────────────────────── Add ────────────────────────
@pytest.mark.gui
def test_add_static_case(qtbot) -> None: # type: ignore[no-untyped-def]
vm = _vm_with_pattern()
case = StaticCase(id=1, name="LS", pattern_ids=[1])
vm.apply_command(AddAnalysisCasesCommand(vm, [case]))
assert len(vm.project.analyses) == 1
assert isinstance(vm.project.analyses[0], StaticCase)
@pytest.mark.gui
def test_add_modal_case_no_pattern_needed(qtbot) -> None: # type: ignore[no-untyped-def]
vm = ProjectViewModel()
vm.new_project()
vm.apply_command(AddAnalysisCasesCommand(vm, [ModalCase(id=1, n_modes=3)]))
assert isinstance(vm.project.analyses[0], ModalCase)
@pytest.mark.gui
def test_add_emits_modelMutated(qtbot) -> None: # type: ignore[no-untyped-def]
vm = _vm_with_pattern()
with qtbot.waitSignal(vm.modelMutated, timeout=500):
vm.apply_command(
AddAnalysisCasesCommand(
vm,
[
StaticCase(id=1, pattern_ids=[1]),
],
)
)
@pytest.mark.gui
def test_add_duplicate_id_raises(qtbot) -> None: # type: ignore[no-untyped-def]
vm = _vm_with_pattern()
vm.apply_command(AddAnalysisCasesCommand(vm, [StaticCase(id=1, pattern_ids=[1])]))
with pytest.raises(ValueError, match="already exists"):
vm.apply_command(AddAnalysisCasesCommand(vm, [StaticCase(id=1, pattern_ids=[1])]))
# ──────────────────────── Update ────────────────────────
@pytest.mark.gui
def test_update_static_changes_n_steps(qtbot) -> None: # type: ignore[no-untyped-def]
vm = _vm_with_pattern()
vm.apply_command(
AddAnalysisCasesCommand(
vm,
[
StaticCase(id=1, name="A", pattern_ids=[1], n_steps=1),
],
)
)
new = StaticCase(id=1, name="A", pattern_ids=[1], n_steps=10)
vm.apply_command(UpdateAnalysisCaseCommand(vm, new))
assert vm.project.analyses[0].n_steps == 10
vm.undo_stack.undo()
assert vm.project.analyses[0].n_steps == 1
@pytest.mark.gui
def test_update_changes_case_type(qtbot) -> None: # type: ignore[no-untyped-def]
vm = _vm_with_pattern()
vm.apply_command(
AddAnalysisCasesCommand(
vm,
[
StaticCase(id=1, pattern_ids=[1]),
],
)
)
swapped = ModalCase(id=1, name="Mode swap", n_modes=5)
vm.apply_command(UpdateAnalysisCaseCommand(vm, swapped))
assert isinstance(vm.project.analyses[0], ModalCase)
vm.undo_stack.undo()
assert isinstance(vm.project.analyses[0], StaticCase)
@pytest.mark.gui
def test_update_unknown_id_raises(qtbot) -> None: # type: ignore[no-untyped-def]
vm = _vm_with_pattern()
bogus = ModalCase(id=99)
with pytest.raises(KeyError):
vm.apply_command(UpdateAnalysisCaseCommand(vm, bogus))
# ──────────────────────── Delete ────────────────────────
@pytest.mark.gui
def test_delete_round_trip(qtbot) -> None: # type: ignore[no-untyped-def]
vm = _vm_with_pattern()
vm.apply_command(
AddAnalysisCasesCommand(
vm,
[
StaticCase(id=1, pattern_ids=[1]),
ModalCase(id=2),
],
)
)
vm.apply_command(DeleteAnalysisCasesCommand(vm, {1}))
assert {c.id for c in vm.project.analyses} == {2}
vm.undo_stack.undo()
assert {c.id for c in vm.project.analyses} == {1, 2}
# ──────────────────────── Transient defaults ────────────────────────
@pytest.mark.gui
def test_transient_case_default_integrator_params(qtbot) -> None: # type: ignore[no-untyped-def]
"""Newmark γ=0.5, β=0.25 (average acceleration) — unconditionally stable."""
vm = _vm_with_pattern()
case = TransientCase(id=1, pattern_ids=[1], dt=0.01, n_steps=100)
vm.apply_command(AddAnalysisCasesCommand(vm, [case]))
assert vm.project.analyses[0].integrator == "Newmark"
assert vm.project.analyses[0].integrator_params == (0.5, 0.25)