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:
smill 2026-09-11 13:19:59 -04:00
commit f361fee969
560 changed files with 178701 additions and 0 deletions

View file

View file

@ -0,0 +1,153 @@
"""
Tests for the auto-generated Pydantic v2 catalog stubs.
Fast unit tests only — no Qt, no openseespy, no file I/O.
"""
from __future__ import annotations
import pytest
from pydantic import BaseModel
from otko.core.catalog import CATALOG
from otko.core.catalog.generated.concrete04_popovics_concrete import (
Concrete04PopovicsConcreteSpec,
)
from otko.core.catalog.generated.elastic import ElasticSpec
from otko.core.catalog.generated.steel02 import Steel02Spec
# ---------------------------------------------------------------------------
# CATALOG registry
# ---------------------------------------------------------------------------
class TestCatalogRegistry:
def test_catalog_has_58_entries(self) -> None:
assert len(CATALOG) == 58
def test_all_entries_are_basemodel_subclasses(self) -> None:
for name, cls in CATALOG.items():
assert issubclass(cls, BaseModel), f"{name!r} is not a BaseModel subclass"
def test_steel02_in_catalog(self) -> None:
assert "Steel02" in CATALOG
assert CATALOG["Steel02"] is Steel02Spec
def test_elastic_in_catalog(self) -> None:
assert "Elastic" in CATALOG
assert CATALOG["Elastic"] is ElasticSpec
def test_concrete04_in_catalog(self) -> None:
assert "Concrete04_(Popovics_concrete)" in CATALOG
assert CATALOG["Concrete04_(Popovics_concrete)"] is Concrete04PopovicsConcreteSpec
# ---------------------------------------------------------------------------
# Instantiation — every spec must accept zero arguments
# ---------------------------------------------------------------------------
class TestInstantiationWithDefaults:
@pytest.mark.parametrize("gid_name,cls", list(CATALOG.items()))
def test_instantiate_with_defaults(self, gid_name: str, cls: type[BaseModel]) -> None:
instance = cls()
assert instance is not None
# ---------------------------------------------------------------------------
# Steel02Spec — spot-check defaults against the source .mat VALUE lines
# ---------------------------------------------------------------------------
class TestSteel02Spec:
def setup_method(self) -> None:
self.spec = Steel02Spec()
def test_parameter_r0_default(self) -> None:
assert self.spec.parameter_r0 == 20
def test_parameter_cr1_default(self) -> None:
assert self.spec.parameter_cr1 == 0.925
def test_parameter_cr2_default(self) -> None:
assert self.spec.parameter_cr2 == 0.15
def test_strain_hardening_ratio_b_default(self) -> None:
assert self.spec.strain_hardening_ratio_b == 0.02
def test_yield_stress_fy_default(self) -> None:
# UNITS field — stored as string including unit label
assert self.spec.yield_stress_fy == "500 MPa"
def test_initial_elastic_tangent_e0_default(self) -> None:
assert self.spec.initial_elastic_tangent_e0 == "200 GPa"
def test_steel_grade_default(self) -> None:
assert self.spec.steel_grade == "Custom"
def test_formulation_default(self) -> None:
assert self.spec.formulation == "Stress-Strain"
def test_isotropic_hardening_parameter_a1_default(self) -> None:
assert self.spec.isotropic_hardening_parameter_a1 == 0
def test_isotropic_hardening_parameter_a2_default(self) -> None:
assert self.spec.isotropic_hardening_parameter_a2 == 1
def test_dependencies_in_model_config(self) -> None:
extra = Steel02Spec.model_config.get("json_schema_extra") or {}
assert isinstance(extra, dict)
assert "dependencies" in extra
assert len(extra["dependencies"]) > 0
def test_gid_name_in_model_config(self) -> None:
extra = Steel02Spec.model_config.get("json_schema_extra") or {}
assert extra.get("x-gid-name") == "Steel02"
def test_book_in_model_config(self) -> None:
extra = Steel02Spec.model_config.get("json_schema_extra") or {}
assert extra.get("x-book") == "Uniaxial_Steel_Materials"
# ---------------------------------------------------------------------------
# Round-trip: serialize -> deserialize -> equal
# ---------------------------------------------------------------------------
class TestRoundTrip:
def _roundtrip(self, cls: type[BaseModel]) -> None:
original = cls()
json_str = original.model_dump_json()
restored = cls.model_validate_json(json_str)
assert original == restored, f"{cls.__name__} round-trip failed"
def test_steel02_roundtrip(self) -> None:
self._roundtrip(Steel02Spec)
def test_elastic_roundtrip(self) -> None:
self._roundtrip(ElasticSpec)
def test_concrete04_roundtrip(self) -> None:
self._roundtrip(Concrete04PopovicsConcreteSpec)
# ---------------------------------------------------------------------------
# Book union types are importable
# ---------------------------------------------------------------------------
class TestBookUnions:
def test_standard_uniaxial_materials_union(self) -> None:
from otko.core.catalog.generated import StandardUniaxialMaterials
assert StandardUniaxialMaterials is not None
def test_uniaxial_steel_materials_union(self) -> None:
from otko.core.catalog.generated import UniaxialSteelMaterials
assert UniaxialSteelMaterials is not None
def test_conditions_restraints_union(self) -> None:
from otko.core.catalog.generated.conditions import Restraints
assert Restraints is not None