Files
orisys-app/shared/tests/test_scene3d_live.py
2026-09-27 19:20:43 +08:00

483 lines
21 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.
"""Stage 3 acceptance: the field path, composition, and the frame schedule.
The three rows that matter most are the ones that are easy to get subtly wrong and
impossible to notice on screen: the *colour* path must never resample the field onto
the vertices (that is what painted 30.15 % of the island grey), the display gain must
not touch the measurement, and a producer must never be able to make the display queue
grow.
"""
from __future__ import annotations
import numpy as np
import pytest
from orisys_app_shared.scene3d import synthetic
from orisys_app_shared.scene3d.composer import DISPLAY_ILLUSTRATIVE, SceneComposer
from orisys_app_shared.scene3d.contracts import (
DisplaySettings,
FieldDescriptor,
FrameIdentity,
SurfaceFieldFrame,
ThreeDError,
freeze,
)
from orisys_app_shared.scene3d.sampling import (
remap_to_domain,
sample_vertex_displacement,
)
# --------------------------------------------------------------------- helpers
def _frame(scene, scalar, valid=None, *, displacement=None, quality=None,
identity=None, domain=None, shape=None):
scalar = np.asarray(scalar, dtype=np.float32)
if valid is None:
valid = np.ones(scalar.shape, dtype=bool)
frame = SurfaceFieldFrame(
identity=identity or FrameIdentity("live", 1, 1, 0.1, generation=1),
geometry_id=scene.geometry_id,
field_domain_id=domain or scene.field_domain_id,
scalar=freeze(scalar, dtype=np.float32),
scalar_descriptor=FieldDescriptor("flow_magnitude", "px", "approximate", False),
valid=freeze(np.asarray(valid, dtype=bool), dtype=np.bool_),
displacement_sensor_m=(
freeze(np.asarray(displacement, dtype=np.float32), dtype=np.float32)
if displacement is not None
else None
),
displacement_quality=quality,
)
if shape is not None:
object.__setattr__(frame, "scalar", freeze(np.asarray(shape, dtype=np.float32), dtype=np.float32))
return frame
def _binding(shape=(3, 3), indices=None, weights=None):
"""The identity binding of the PoC scene, optionally overridden."""
scene = synthetic.poc_scene()
binding = scene.binding
if indices is None and weights is None and shape == (3, 3):
return binding
from orisys_app_shared.scene3d.contracts import SurfaceBinding
count = int(indices.shape[0]) if indices is not None else shape[0] * shape[1]
uvs = np.zeros((count, 2), dtype=np.float32)
if count == shape[0] * shape[1]:
grid = np.stack(
np.meshgrid(np.arange(shape[1]), np.arange(shape[0])), axis=-1
).reshape(-1, 2)
uvs = grid.astype(np.float32)
return SurfaceBinding(
part_id="sensing_surface",
geometry_id=scene.geometry_id,
field_domain_id=scene.field_domain_id,
shape_hw=shape,
uvs=freeze(uvs, dtype=np.float32),
sample_indices=freeze(np.asarray(indices, dtype=np.int32), dtype=np.int32),
sample_weights=freeze(np.asarray(weights, dtype=np.float32), dtype=np.float32),
)
# -------------------------------------------------------- the colour field path
def test_the_colour_path_never_gathers_the_field_onto_the_vertices():
"""The property the whole texture-first change exists for.
The render frame must carry the acquisition frame's *own* ``[H,W]`` grid — not a
per-vertex resampling of it. This is asserted by identity, which is the strongest
form available: a gather cannot produce the same object.
"""
scene = synthetic.poc_scene()
frame = _frame(scene, np.arange(9, dtype=np.float32).reshape(3, 3))
composed = SceneComposer().compose(scene, frame, DisplaySettings())
assert composed.field.scalar is frame.scalar
assert composed.field.valid is frame.valid
assert composed.field.shape_hw == (3, 3)
assert composed.field.domain_id == scene.field_domain_id
assert composed.field.descriptor is frame.scalar_descriptor
# ...and there is no per-vertex array left to read by mistake.
assert not hasattr(composed, "scalar")
assert not hasattr(composed, "valid")
def test_a_hole_in_the_field_stays_a_hole_and_is_never_a_vertex_nan():
"""A gap is not a measurement of zero, and it is not a grey *triangle* either.
Under the old sampler one invalid tap made a whole vertex ``NaN``, and the shader
painted every triangle touching it grey. Now the hole travels as a hole: exactly
the pixels the device called invalid are invalid, and a value of 0.0 inside them
is not read at all.
"""
scene = synthetic.poc_scene()
valid = np.ones((3, 3), dtype=bool)
valid[1, 1] = False
field = np.zeros((3, 3), dtype=np.float32)
field[1, 1] = 99.0
composed = SceneComposer().compose(scene, _frame(scene, field, valid), DisplaySettings())
assert composed.field.valid.tolist() == valid.tolist()
assert int(composed.field.valid.sum()) == 8
# The scalar is carried through untouched, `NaN` or not: nothing rewrites it into
# a per-vertex absence, so the fragment reads the *device's* answer.
assert np.array_equal(np.asarray(composed.field.scalar), field)
def test_a_zero_measurement_is_valid_and_stays_zero():
"""Zero is a value. Only `valid` decides whether it means anything."""
scene = synthetic.poc_scene()
composed = SceneComposer().compose(
scene, _frame(scene, np.zeros((3, 3), dtype=np.float32)), DisplaySettings()
)
assert composed.field.valid.all()
assert np.all(np.asarray(composed.field.scalar) == 0.0)
assert not np.isnan(np.asarray(composed.field.scalar)).any()
def test_the_composer_refuses_a_frame_from_another_domain():
scene = synthetic.poc_scene()
frame = _frame(scene, np.zeros((3, 3), dtype=np.float32), domain="some-other-domain")
with pytest.raises(ThreeDError) as excinfo:
SceneComposer().compose(scene, frame, DisplaySettings())
assert excinfo.value.diagnostic.code == "field_domain_mismatch"
def test_the_composer_refuses_a_frame_of_the_wrong_shape():
scene = synthetic.poc_scene()
frame = _frame(scene, np.zeros((4, 4), dtype=np.float32), shape=np.zeros((4, 4), dtype=np.float32))
with pytest.raises(ThreeDError) as excinfo:
SceneComposer().compose(scene, frame, DisplaySettings())
assert excinfo.value.diagnostic.code == "field_domain_mismatch"
def test_composing_a_frame_costs_no_per_vertex_work():
"""The measured CPU cost of the colour path, over a realistic grid.
The old path gathered 1032 vertices x 4 taps every frame *and* the panel's own
tests measured it as the bulk of a compose. The field path is a shape check and a
dataclass: the number below is the evidence.
"""
scene = synthetic.poc_scene()
composer = SceneComposer()
frame = _frame(scene, np.zeros((3, 3), dtype=np.float32))
composer.compose(scene, frame, DisplaySettings())
samples = []
for _ in range(20):
composer.compose(scene, frame, DisplaySettings())
samples.append(composer.last_stats.sample_ms)
assert max(samples) < 2.0, f"the colour path still does per-vertex work: {samples}"
# -------------------------------------------- the vertex displacement (geometry)
def test_a_constant_displacement_gathers_to_the_same_constant():
scene = synthetic.poc_scene()
displacement = np.full((3, 3, 3), 0.25, dtype=np.float32)
frame = _frame(
scene, np.zeros((3, 3), dtype=np.float32), displacement=displacement
)
values = sample_vertex_displacement(frame, scene.binding)
assert values is not None
assert np.allclose(np.asarray(values), 0.25)
def test_an_invalid_tap_leaves_that_vertex_with_no_displacement():
"""Absent is absent: the vertex does not move at all, and the composer reads NaN."""
scene = synthetic.poc_scene()
displacement = np.ones((3, 3, 3), dtype=np.float32) * 0.5
valid = np.ones((3, 3), dtype=bool)
valid[1, 1] = False
frame = _frame(
scene, np.zeros((3, 3), dtype=np.float32), valid, displacement=displacement
)
values = np.asarray(sample_vertex_displacement(frame, scene.binding))
assert np.isnan(values[4]).all()
assert np.allclose(values[0], 0.5)
def test_a_frame_without_a_displacement_gathers_nothing():
scene = synthetic.poc_scene()
frame = _frame(scene, np.zeros((3, 3), dtype=np.float32))
assert sample_vertex_displacement(frame, scene.binding) is None
def test_the_displacement_path_never_reads_the_scalar_field():
"""It is geometry, not colour: the gathered value may not depend on the scalar."""
scene = synthetic.poc_scene()
displacement = np.ones((3, 3, 3), dtype=np.float32) * 0.125
first = sample_vertex_displacement(
_frame(scene, np.full((3, 3), 1.0, dtype=np.float32), displacement=displacement),
scene.binding,
)
second = sample_vertex_displacement(
_frame(scene, np.full((3, 3), 900.0, dtype=np.float32), displacement=displacement),
scene.binding,
)
assert np.array_equal(np.asarray(first), np.asarray(second))
# ----------------------------------------------------------------------- remap
def test_a_remap_gathers_the_source_pixels_the_map_names():
field = np.arange(12, dtype=np.float32).reshape(3, 4)
map_x = np.array([[0, 1], [2, 3]])
map_y = np.array([[0, 0], [2, 2]])
values, valid = remap_to_domain(field, map_x=map_x, map_y=map_y, shape_hw=(2, 2))
assert np.array_equal(np.asarray(values), [[0.0, 1.0], [10.0, 11.0]])
assert np.asarray(valid).all()
def test_a_remap_that_has_no_source_pixel_leaves_a_hole_not_a_zero():
field = np.full((3, 3), 5.0, dtype=np.float32)
map_x = np.array([[0, -1], [1, 1]])
map_y = np.array([[0, -1], [1, 1]])
values, valid = remap_to_domain(field, map_x=map_x, map_y=map_y, shape_hw=(2, 2))
assert np.asarray(valid).tolist() == [[True, False], [True, True]]
assert np.isnan(np.asarray(values)[0, 1]), "a hole must not read as zero"
assert np.asarray(values)[0, 0] == 5.0
def test_a_remap_keeps_a_trailing_axis_for_vector_fields():
flow = np.zeros((3, 3, 2), dtype=np.float32)
flow[..., 0] = 1.0
flow[..., 1] = 2.0
map_x = np.array([[0, 1]])
map_y = np.array([[1, 2]])
values, valid = remap_to_domain(flow, map_x=map_x, map_y=map_y, shape_hw=(1, 2))
assert np.asarray(values).shape == (1, 2, 2)
assert np.asarray(values)[0, 0].tolist() == [1.0, 2.0]
assert np.asarray(valid).all()
def test_a_remap_of_the_wrong_shape_is_refused():
field = np.zeros((3, 3), dtype=np.float32)
with pytest.raises(ThreeDError) as excinfo:
remap_to_domain(
field, map_x=np.zeros((2, 2)), map_y=np.zeros((2, 2)), shape_hw=(3, 3)
)
assert excinfo.value.diagnostic.code == "field_domain_mismatch"
def test_a_remap_leaves_the_result_read_only():
field = np.zeros((2, 2), dtype=np.float32)
values, valid = remap_to_domain(
field, map_x=np.zeros((2, 2)), map_y=np.zeros((2, 2)), shape_hw=(2, 2)
)
assert not np.asarray(values).flags["WRITEABLE"]
assert not np.asarray(valid).flags["WRITEABLE"]
# ------------------------------------------------------------------- composition
def test_the_gain_moves_positions_and_never_the_measurement():
"""The row the whole colour scale depends on."""
scene = synthetic.poc_scene()
count = 9
field = np.linspace(0.0, 1.0, count, dtype=np.float32).reshape(3, 3)
displacement = np.zeros((3, 3, 3), dtype=np.float32)
displacement[:, :, 2] = 0.001 # a millimetre of illustrative relief
frame = _frame(scene, field, displacement=displacement, quality="approximate")
composer = SceneComposer()
quiet = composer.compose(scene, frame, DisplaySettings(deform_enabled=False))
small = composer.compose(
scene, frame, DisplaySettings(deform_enabled=True, display_gain=1.0)
)
large = composer.compose(
scene, frame, DisplaySettings(deform_enabled=True, display_gain=50.0)
)
# The measurement is identical in all three.
assert np.array_equal(np.asarray(quiet.field.scalar), np.asarray(small.field.scalar))
assert np.array_equal(np.asarray(small.field.scalar), np.asarray(large.field.scalar))
assert np.array_equal(np.asarray(quiet.field.valid), np.asarray(large.field.valid))
# The positions are not.
rest = np.asarray(small.positions_sensor_m)
moved = np.asarray(large.positions_sensor_m)
rest_scene = np.asarray(scene.sensing_part().positions_sensor_m)
assert np.allclose((rest - rest_scene)[:, :2], 0.0, atol=1e-7), "the gain moved sideways"
assert np.allclose((rest - rest_scene)[:, 2], 0.001, atol=1e-7)
assert np.allclose((moved - rest_scene)[:, 2], 0.05, atol=1e-7)
def test_an_illustrative_deformation_says_so():
scene = synthetic.poc_scene()
displacement = np.zeros((3, 3, 3), dtype=np.float32)
displacement[:, :, 2] = 0.001
frame = _frame(scene, np.zeros((3, 3), dtype=np.float32), displacement=displacement,
quality="approximate")
composed = SceneComposer().compose(
scene, frame, DisplaySettings(deform_enabled=True, display_gain=3.0)
)
codes = [item.code for item in composed.diagnostics]
assert DISPLAY_ILLUSTRATIVE in codes, codes
note = next(item for item in composed.diagnostics if item.code == DISPLAY_ILLUSTRATIVE)
assert note.severity == "info"
assert "illustration" in note.message and "×3" in note.message
def test_a_calibrated_displacement_does_not_need_the_caveat():
scene = synthetic.poc_scene()
displacement = np.zeros((3, 3, 3), dtype=np.float32)
displacement[:, :, 2] = 0.0005
frame = _frame(scene, np.zeros((3, 3), dtype=np.float32), displacement=displacement,
quality="calibrated")
composed = SceneComposer().compose(
scene, frame, DisplaySettings(deform_enabled=True, display_gain=2.0)
)
assert DISPLAY_ILLUSTRATIVE not in [item.code for item in composed.diagnostics]
def test_deformation_without_a_displacement_colours_in_place_and_says_so():
scene = synthetic.poc_scene()
frame = _frame(scene, np.full((3, 3), 2.0, dtype=np.float32))
composed = SceneComposer().compose(
scene, frame, DisplaySettings(deform_enabled=True, display_gain=10.0)
)
assert np.allclose(
np.asarray(composed.positions_sensor_m),
np.asarray(scene.sensing_part().positions_sensor_m),
)
assert any(item.code == "field_unavailable" for item in composed.diagnostics)
def test_normals_are_recomputed_from_the_deformed_positions():
"""A bump must change the shading on its slopes."""
scene = synthetic.poc_scene()
displacement = np.zeros((3, 3, 3), dtype=np.float32)
# Only the centre vertex is lifted: the slopes around it have to tilt. (The
# apex itself stays vertical, which is the correct normal for a symmetric
# peak — asserting otherwise would be asserting a bug.)
displacement[1, 1, 2] = 0.004
frame = _frame(scene, np.zeros((3, 3), dtype=np.float32), displacement=displacement,
quality="approximate")
composed = SceneComposer().compose(
scene, frame, DisplaySettings(deform_enabled=True, display_gain=1.0)
)
normals = np.asarray(composed.normals_sensor)
assert np.allclose(np.linalg.norm(normals, axis=1), 1.0, atol=1e-5)
rest_normals = np.asarray(scene.sensing_part().normals_sensor)
assert not np.allclose(normals, rest_normals), "the normals ignored the deformation"
# Only the vertices whose triangles touch the lifted one change; the two
# diagonal corners (2 and 6) share no triangle with it and correctly stay flat.
neighbours = [0, 1, 3, 5, 7, 8]
untouched = [2, 6]
tilted = np.array([float(normals[index, 2]) for index in neighbours])
assert (tilted < 0.999).all(), f"the slopes did not tilt: {tilted}"
flat = np.array([float(normals[index, 2]) for index in untouched])
assert np.allclose(flat, 1.0, atol=1e-6), flat
assert normals[4, 2] == pytest.approx(1.0, abs=1e-3), (
"the apex of a symmetric bump is vertical"
)
# ...and the reverse bump tilts the other way.
upside_down = -displacement
flipped = SceneComposer().compose(
scene,
_frame(scene, np.zeros((3, 3), dtype=np.float32), displacement=upside_down,
quality="approximate"),
DisplaySettings(deform_enabled=True, display_gain=1.0),
)
flipped_normals = np.asarray(flipped.normals_sensor)
assert np.allclose(
flipped_normals[neighbours, 0], -normals[neighbours, 0], atol=1e-6
), "the reverse bump must tilt the slopes the other way"
def test_the_composer_refuses_a_frame_from_another_geometry():
scene = synthetic.poc_scene()
frame = _frame(scene, np.zeros((3, 3), dtype=np.float32))
object.__setattr__(frame, "geometry_id", "someone-elses")
with pytest.raises(ThreeDError) as excinfo:
SceneComposer().compose(scene, frame, DisplaySettings())
assert excinfo.value.diagnostic.code == "geometry_mismatch"
def test_a_reference_frame_measures_nothing():
scene = synthetic.poc_scene()
rest = SceneComposer().rest_frame(scene, FrameIdentity("live", 1, 0, 0.0, 1))
assert not np.asarray(rest.field.valid).any()
assert np.isnan(np.asarray(rest.field.scalar)).all()
assert rest.field.descriptor.quantity == "unavailable"
assert rest.field.descriptor.quality == "display_only"
assert np.allclose(
np.asarray(rest.positions_sensor_m),
np.asarray(scene.sensing_part().positions_sensor_m),
)
def test_the_composed_frame_carries_read_only_arrays():
scene = synthetic.poc_scene()
composed = SceneComposer().compose(
scene, _frame(scene, np.zeros((3, 3), dtype=np.float32)), DisplaySettings()
)
for name in ("positions_sensor_m", "normals_sensor"):
value = getattr(composed, name)
assert not value.flags["WRITEABLE"], f"{name} is writable"
assert value.flags["C_CONTIGUOUS"]
for name in ("scalar", "valid"):
value = getattr(composed.field, name)
assert not value.flags["WRITEABLE"], f"field.{name} is writable"
assert value.flags["C_CONTIGUOUS"]
def test_the_composer_does_not_rebuild_the_binding_or_the_indices():
"""Composition must be cheap enough to run per frame."""
scene = synthetic.poc_scene()
composer = SceneComposer()
frame = _frame(scene, np.zeros((3, 3), dtype=np.float32))
for _ in range(3):
composer.compose(scene, frame, DisplaySettings())
assert composer.last_stats.total_ms < 50.0, composer.last_stats
# --------------------------------------------- stage 9: reuse and real numbers
def test_colouring_in_place_reuses_the_assets_own_geometry():
"""No displacement to draw means the picture *is* the asset: nothing to recompute.
Normals were recomputed from the same untouched positions on every frame, and the
measured cost was the bulk of a 20k-vertex compose. The reused arrays are the
asset's own (frozen) ones, so nothing is copied either.
"""
scene = synthetic.poc_scene()
part = scene.sensing_part()
composer = SceneComposer()
frame = _frame(scene, np.zeros((3, 3), dtype=np.float32))
composed = composer.compose(scene, frame, DisplaySettings())
assert composed.positions_sensor_m is part.positions_sensor_m
assert composed.normals_sensor is part.normals_sensor
# A deform request with nothing to deform is the same case, and says so.
composed = composer.compose(
scene, frame, DisplaySettings(deform_enabled=True, display_gain=5.0)
)
assert composed.positions_sensor_m is part.positions_sensor_m
assert any(item.code == "field_unavailable" for item in composed.diagnostics)
def test_a_calibrated_displacement_still_recomputes_positions_and_normals():
"""The cache is for the colour-in-place case only, never for a deformed surface."""
scene = synthetic.poc_scene()
part = scene.sensing_part()
displacement = np.zeros((3, 3, 3), dtype=np.float32)
displacement[1, 1, 2] = 0.5
frame = _frame(
scene,
np.ones((3, 3), dtype=np.float32),
displacement=displacement,
quality="calibrated",
)
composed = SceneComposer().compose(
scene, frame, DisplaySettings(deform_enabled=True, display_gain=2.0)
)
assert composed.deformed
assert composed.positions_sensor_m is not part.positions_sensor_m
assert composed.normals_sensor is not part.normals_sensor
assert not np.allclose(
np.asarray(composed.positions_sensor_m), np.asarray(part.positions_sensor_m)
)
def test_the_composed_frame_carries_its_own_cost():
"""The panel's "compose ms" reads this, not the composer's shared counter."""
scene = synthetic.poc_scene()
composer = SceneComposer()
frame = _frame(scene, np.zeros((3, 3), dtype=np.float32))
composed = composer.compose(scene, frame, DisplaySettings())
assert composed.compose_ms > 0.0
assert composed.compose_ms == pytest.approx(composer.last_stats.total_ms)
# A second compose rewrites ``last_stats``; the first frame's number must not move.
first = composed.compose_ms
composer.compose(scene, frame, DisplaySettings())
assert composed.compose_ms == first