otko/tests/integration/test_combinations.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

90 lines
2.8 KiB
Python
Raw Permalink 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.

"""Load-combination integration: 1.2×D + 1.6×L by superposition == one combined run."""
from __future__ import annotations
import math
import pytest
ops = pytest.importorskip("openseespy.opensees") # skip if OpenSeesPy not installed
from otko.core import ( # noqa: E402
ElasticBeamColumn,
ElasticSection,
LinearTimeSeries,
LoadCombination,
LoadCombinationItem,
NodalLoad,
Node,
PlainLoadPattern,
Project,
StaticCase,
)
from otko.services import OpenSeesRunner, evaluate_combination # noqa: E402
from otko.services.results import StaticResults # noqa: E402
pytestmark = pytest.mark.slow
def _two_pattern_cantilever() -> Project:
elastic_mod = 200e9
area = 0.01
inertia = 8.333e-6
return 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=(5.0, 0.0, 0.0)),
],
sections=[ElasticSection(id=1, E=elastic_mod, A=area, Iz=inertia)],
elements=[ElasticBeamColumn(id=1, nodes=(1, 2), section_id=1)],
time_series=[LinearTimeSeries(id=1), LinearTimeSeries(id=2)],
load_patterns=[
PlainLoadPattern(
id=1,
name="Dead",
time_series_id=1,
nodal_loads=[NodalLoad(node_id=2, forces=(0.0, -1000.0, 0.0, 0.0, 0.0, 0.0))],
),
PlainLoadPattern(
id=2,
name="Live",
time_series_id=2,
nodal_loads=[NodalLoad(node_id=2, forces=(0.0, -500.0, 0.0, 0.0, 0.0, 0.0))],
),
],
analyses=[
StaticCase(id=1, name="Dead", pattern_ids=[1]),
StaticCase(id=2, name="Live", pattern_ids=[2]),
],
)
def test_superposition_matches_combined_run() -> None:
"""1.2*Dead + 1.6*Live via evaluate_combination == single StaticCase run."""
proj = _two_pattern_cantilever()
dead = OpenSeesRunner(proj).run(proj.analyses[0])
live = OpenSeesRunner(proj).run(proj.analyses[1])
combo = LoadCombination(
id=1,
name="1.2D+1.6L",
kind="Linear",
items=[
LoadCombinationItem(case_id=1, factor=1.2),
LoadCombinationItem(case_id=2, factor=1.6),
],
)
combined = evaluate_combination({1: dead, 2: live}, combo)
assert isinstance(combined, StaticResults)
# Single run with both patterns factored identically.
direct = OpenSeesRunner(proj).run(
StaticCase(id=3, name="direct", pattern_ids=[1, 2], pattern_factors={1: 1.2, 2: 1.6})
)
assert math.isclose(
combined.disp(node_id=2, dof=2), direct.disp(node_id=2, dof=2), rel_tol=1e-9
)