otko/tests/unit/test_diagram_renderer.py
smillmorel 61b850bf46
Some checks failed
CI / lint (pull_request) Has been cancelled
CI / type (pull_request) Has been cancelled
CI / test-headless (macos-latest, 3.10) (pull_request) Has been cancelled
CI / test-headless (macos-latest, 3.11) (pull_request) Has been cancelled
CI / test-headless (macos-latest, 3.12) (pull_request) Has been cancelled
CI / test-headless (ubuntu-latest, 3.10) (pull_request) Has been cancelled
CI / test-headless (ubuntu-latest, 3.11) (pull_request) Has been cancelled
CI / test-headless (ubuntu-latest, 3.12) (pull_request) Has been cancelled
CI / test-headless (windows-latest, 3.10) (pull_request) Has been cancelled
CI / test-headless (windows-latest, 3.11) (pull_request) Has been cancelled
CI / test-headless (windows-latest, 3.12) (pull_request) Has been cancelled
CI / test-gui (pull_request) Has been cancelled
CI / test-integration (macos-latest) (pull_request) Has been cancelled
CI / test-integration (ubuntu-latest) (pull_request) Has been cancelled
CI / test-integration (windows-latest) (pull_request) Has been cancelled
style: resolve the remaining ruff findings
Auto-fixes (UP037, UP038, F401, I001, SIM105, RUF100) plus hand-fixes:
drop the now-unused `Union` imports, annotate mutable class attributes
with ClassVar, raise ValidationError instead of blind Exception in the
zero-length test, delete two dead locals, and bind the per-iteration
grid-builder closures to their loop variables (B023).
2026-09-16 13:20:36 -04:00

135 lines
4.6 KiB
Python

"""Smoke tests for the force-diagram overlay renderer.
These verify the renderer doesn't crash on representative inputs and
produces an actor when there's data to draw. Geometry/colour assertions
are kept minimal — they'd be brittle and the visual is the spec anyway.
"""
from __future__ import annotations
import os
import numpy as np
import pytest
# Force pyvista off-screen before any pyvista import in this module's chain.
os.environ.setdefault("PYVISTA_OFF_SCREEN", "true")
import pyvista as pv
from otko.core import (
ElasticBeamColumn,
ElasticSection,
Node,
Project,
)
from otko.services.element_forces import (
DiagramData,
ForceComponent,
extract_diagram_data,
)
from otko.services.results import StaticResults
from otko.views.canvas3d.diagram_renderer import DiagramRenderer
pv.OFF_SCREEN = True
@pytest.fixture
def offscreen_plotter(): # type: ignore[no-untyped-def]
p = pv.Plotter(off_screen=True)
yield p
p.close()
@pytest.fixture
def project_3d() -> Project:
return Project(
nodes=[
Node(id=1, coords=(0.0, 0.0, 0.0)),
Node(id=2, coords=(3.0, 0.0, 0.0)),
Node(id=3, coords=(6.0, 0.0, 0.0)),
],
sections=[ElasticSection(id=1, E=200e9, A=0.01, Iz=1e-5, Iy=1e-5)],
elements=[
ElasticBeamColumn(id=10, nodes=(1, 2), section_id=1),
ElasticBeamColumn(id=20, nodes=(2, 3), section_id=1),
],
)
@pytest.fixture
def static_results() -> StaticResults:
f10 = np.array([[100.0, 5.0, 0.0, 0.0, 0.0, 9.0, -100.0, -5.0, 0.0, 0.0, 0.0, -9.0]])
f20 = np.array([[-50.0, 2.0, 0.0, 0.0, 0.0, 3.0, 50.0, -2.0, 0.0, 0.0, 0.0, -3.0]])
return StaticResults(case_id=1, case_name="t", n_steps=1, element_forces={10: f10, 20: f20})
def test_render_axial_creates_actor(offscreen_plotter, project_3d, static_results) -> None: # type: ignore[no-untyped-def]
r = DiagramRenderer(offscreen_plotter)
data = extract_diagram_data(project_3d, static_results, ForceComponent.N)
r.render(project_3d, data, scale=0.01)
assert r._actor is not None
def test_render_shear_creates_actor(offscreen_plotter, project_3d, static_results) -> None: # type: ignore[no-untyped-def]
r = DiagramRenderer(offscreen_plotter)
data = extract_diagram_data(project_3d, static_results, ForceComponent.V2)
r.render(project_3d, data, scale=0.05)
assert r._actor is not None
def test_render_moment_creates_actor(offscreen_plotter, project_3d, static_results) -> None: # type: ignore[no-untyped-def]
r = DiagramRenderer(offscreen_plotter)
data = extract_diagram_data(project_3d, static_results, ForceComponent.M3)
r.render(project_3d, data, scale=0.05)
assert r._actor is not None
def test_render_empty_data_does_not_create_actor(offscreen_plotter, project_3d) -> None: # type: ignore[no-untyped-def]
r = DiagramRenderer(offscreen_plotter)
empty = DiagramData(
component=ForceComponent.N,
element_ids=np.empty(0, dtype=int),
values_i=np.empty(0),
values_j=np.empty(0),
abs_max=0.0,
)
r.render(project_3d, empty, scale=1.0)
assert r._actor is None
def test_render_zero_magnitude_data_does_not_create_actor(
offscreen_plotter,
project_3d,
) -> None: # type: ignore[no-untyped-def]
"""A diagram for a component that's identically zero shouldn't render
an empty mesh + scalar bar — that's misleading visual noise."""
r = DiagramRenderer(offscreen_plotter)
zero_for_two_elems = DiagramData(
component=ForceComponent.T,
element_ids=np.array([10, 20], dtype=int),
values_i=np.zeros(2),
values_j=np.zeros(2),
abs_max=0.0,
)
r.render(project_3d, zero_for_two_elems, scale=1.0)
assert r._actor is None
def test_clear_removes_actor(offscreen_plotter, project_3d, static_results) -> None: # type: ignore[no-untyped-def]
r = DiagramRenderer(offscreen_plotter)
data = extract_diagram_data(project_3d, static_results, ForceComponent.N)
r.render(project_3d, data, scale=0.01)
assert r._actor is not None
r.clear()
assert r._actor is None
def test_render_replaces_previous_overlay(offscreen_plotter, project_3d, static_results) -> None: # type: ignore[no-untyped-def]
r = DiagramRenderer(offscreen_plotter)
data_n = extract_diagram_data(project_3d, static_results, ForceComponent.N)
r.render(project_3d, data_n, scale=0.01)
first_actor = r._actor
data_v = extract_diagram_data(project_3d, static_results, ForceComponent.V2)
r.render(project_3d, data_v, scale=0.05)
assert r._actor is not first_actor