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
This commit is contained in:
commit
f361fee969
560 changed files with 178701 additions and 0 deletions
90
tests/integration/test_runner_modal.py
Normal file
90
tests/integration/test_runner_modal.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
"""Modal analysis verification.
|
||||
|
||||
A 1-DOF lumped-mass cantilever pole. The fundamental natural frequency
|
||||
of the lateral mode is ω = √(k/m), where k = 3EI/L³ for a tip-mass
|
||||
cantilever flexural spring.
|
||||
|
||||
The runner auto-falls back to ``-fullGenLapack`` for small models —
|
||||
ARPACK can't allocate enough Arnoldi workspace when the active DOF
|
||||
count is tiny, which is exactly the SDOF case.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
ops = pytest.importorskip("openseespy.opensees") # noqa: F401
|
||||
|
||||
from otko.core import ( # noqa: E402
|
||||
ElasticBeamColumn,
|
||||
ElasticSection,
|
||||
ModalCase,
|
||||
Node,
|
||||
Project,
|
||||
)
|
||||
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³))."""
|
||||
L = 3.0
|
||||
E = 200e9
|
||||
A = 0.01
|
||||
I = 8.333e-6
|
||||
m_tip = 1000.0
|
||||
|
||||
project = Project(
|
||||
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)),
|
||||
],
|
||||
sections=[ElasticSection(id=1, E=E, A=A, Iz=I)],
|
||||
elements=[ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1)],
|
||||
)
|
||||
|
||||
case = ModalCase(id=1, name="SDOF-Pole", n_modes=1)
|
||||
results = OpenSeesRunner(project).run(case)
|
||||
|
||||
# Lateral cantilever spring stiffness:
|
||||
k = 3.0 * E * I / L**3
|
||||
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"
|
||||
|
||||
|
||||
def test_runner_falls_back_to_lapack_for_small_models() -> None:
|
||||
"""Verify the fallback rule: small n_free triggers Lapack instead of ARPACK."""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
project = Project(
|
||||
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)),
|
||||
],
|
||||
sections=[ElasticSection(id=1, E=200e9, A=0.01, Iz=8.333e-6)],
|
||||
elements=[ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1)],
|
||||
)
|
||||
mock_ops = MagicMock()
|
||||
mock_ops.eigen.return_value = [1.0]
|
||||
mock_ops.nodeEigenvector.return_value = 0.0
|
||||
|
||||
runner = OpenSeesRunner(project, ops_module=mock_ops)
|
||||
runner.run(ModalCase(id=1, name="t", n_modes=1)) # default solver = 'genBandArpack'
|
||||
|
||||
# n_free = (3 - 2) + (3 - 1) = 3, and 2 * n_modes = 2 >= n_free? No, 2 < 3.
|
||||
# Adjust: ask for 2 modes — 2*2 = 4 >= 3 → must trigger fallback.
|
||||
mock_ops.reset_mock()
|
||||
mock_ops.eigen.return_value = [1.0, 4.0]
|
||||
runner.run(ModalCase(id=2, name="t2", n_modes=2))
|
||||
mock_ops.eigen.assert_called_with("-fullGenLapack", 2)
|
||||
Loading…
Reference in a new issue