"""Stage 3 acceptance: sampling, 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: an invalid tap must invalidate the *vertex* rather than read as zero, 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 threading import time from concurrent.futures import ThreadPoolExecutor 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 ( ColorScale, DisplaySettings, FieldDescriptor, FrameIdentity, Quality, RenderFrame, SurfaceFieldFrame, ThreeDError, freeze, rest_descriptor, ) from orisys_app_shared.scene3d.sampling import ( SurfaceSampler, remap_to_domain, sample_field, ) # --------------------------------------------------------------------- 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] return SurfaceBinding( part_id="sensing_surface", geometry_id=scene.geometry_id, field_domain_id=scene.field_domain_id, shape_hw=shape, sample_indices=freeze(np.asarray(indices, dtype=np.int32), dtype=np.int32), sample_weights=freeze(np.asarray(weights, dtype=np.float32), dtype=np.float32), ) # --------------------------------------------------------------------- sampling def test_a_constant_field_samples_to_the_same_constant(): scene = synthetic.poc_scene() fields = sample_field(_frame(scene, np.full((3, 3), 7.5)), scene.binding) assert np.allclose(np.asarray(fields.scalar), 7.5) assert np.asarray(fields.valid).all() def test_a_ramp_field_samples_exactly_like_the_reference(): """Every vertex samples its own cell, so the sampled value is the cell value.""" scene = synthetic.poc_scene() ramp = np.arange(9, dtype=np.float32).reshape(3, 3) fields = sample_field(_frame(scene, ramp), scene.binding) assert np.array_equal(np.asarray(fields.scalar), ramp.reshape(-1)) def test_a_single_sample_spike_reaches_exactly_one_vertex(): scene = synthetic.poc_scene() field = np.zeros((3, 3), dtype=np.float32) field[1, 1] = 5.0 fields = sample_field(_frame(scene, field), scene.binding) scalar = np.asarray(fields.scalar) assert int((scalar > 0).sum()) == 1 assert scalar[4] == 5.0 def test_bilinear_sampling_averages_the_four_taps(): """A vertex between pixels gets the weighted average, computed by hand.""" scene = synthetic.poc_scene() indices = np.array([[0, 1, 3, 4]], dtype=np.int32) weights = np.array([[0.25, 0.25, 0.25, 0.25]], dtype=np.float32) binding = _binding(indices=indices, weights=weights) field = np.array([[1.0, 2.0, 0.0], [3.0, 4.0, 0.0], [0.0, 0.0, 0.0]], dtype=np.float32) fields = sample_field(_frame(scene, field), binding) assert np.asarray(fields.scalar)[0] == pytest.approx((1 + 2 + 3 + 4) / 4) assert bool(np.asarray(fields.valid)[0]) def test_one_invalid_tap_invalidates_the_vertex_and_is_not_read_as_zero(): """A gap is not a measurement of zero: the value must be absent.""" scene = synthetic.poc_scene() indices = np.array([[0, 1, 3, 4]], dtype=np.int32) weights = np.array([[0.25, 0.25, 0.25, 0.25]], dtype=np.float32) binding = _binding(indices=indices, weights=weights) field = np.array([[1.0, 2.0, 0.0], [3.0, 4.0, 0.0], [0.0, 0.0, 0.0]], dtype=np.float32) valid = np.ones((3, 3), dtype=bool) valid[1, 1] = False # index 4 is a positive-weight tap fields = sample_field(_frame(scene, field, valid), binding) assert not bool(np.asarray(fields.valid)[0]) assert np.isnan(np.asarray(fields.scalar)[0]), ( "an invalid vertex must carry no value at all, not a sum with a zero in it" ) def test_an_unused_tap_is_neither_a_contribution_nor_a_reason_to_invalidate(): scene = synthetic.poc_scene() indices = np.array([[4, -1, -1, -1]], dtype=np.int32) weights = np.array([[1.0, 0.0, 0.0, 0.0]], dtype=np.float32) binding = _binding(indices=indices, weights=weights) field = np.zeros((3, 3), dtype=np.float32) field[1, 1] = 3.0 valid = np.ones((3, 3), dtype=bool) valid[0, 0] = False # unused, so it must not matter fields = sample_field(_frame(scene, field, valid), binding) assert bool(np.asarray(fields.valid)[0]) assert np.asarray(fields.scalar)[0] == pytest.approx(3.0) def test_a_non_finite_scalar_invalidates_rather_than_colouring_nan(): scene = synthetic.poc_scene() field = np.full((3, 3), 1.0, dtype=np.float32) field[2, 2] = np.nan fields = sample_field(_frame(scene, field), scene.binding) assert not bool(np.asarray(fields.valid)[8]) assert np.isnan(np.asarray(fields.scalar)[8]) 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() fields = sample_field(_frame(scene, np.zeros((3, 3), dtype=np.float32)), scene.binding) assert np.asarray(fields.valid).all() assert np.all(np.asarray(fields.scalar) == 0.0) assert not np.isnan(np.asarray(fields.scalar)).any() def test_the_sampler_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: sample_field(frame, scene.binding) assert excinfo.value.diagnostic.code == "field_domain_mismatch" def test_the_sampler_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: sample_field(frame, scene.binding) assert "shape does not match" in excinfo.value.diagnostic.message def test_sampling_is_vectorised_enough_for_a_large_mesh(): """A full-resolution binding is ~10^5 vertices; sampling must not crawl.""" scene = synthetic.poc_scene() count = 20_000 rng = np.random.default_rng(7) indices = rng.integers(0, 9, size=(count, 4)).astype(np.int32) weights = rng.random((count, 4)).astype(np.float32) weights /= weights.sum(axis=1, keepdims=True) binding = _binding(indices=indices, weights=weights) frame = _frame(scene, rng.random((3, 3)).astype(np.float32)) started = time.perf_counter() fields = sample_field(frame, binding) elapsed = (time.perf_counter() - started) * 1000.0 assert np.asarray(fields.scalar).shape == (count,) assert elapsed < 150.0, f"sampling {count} vertices took {elapsed:.1f} ms" # ----------------------------------------------------------------------- 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.scalar), np.asarray(small.scalar)) assert np.array_equal(np.asarray(small.scalar), np.asarray(large.scalar)) assert np.array_equal(np.asarray(quiet.valid), np.asarray(large.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.valid).any() assert np.isnan(np.asarray(rest.scalar)).all() assert rest.scalar_descriptor.quantity == "unavailable" assert rest.scalar_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", "scalar", "valid"): value = getattr(composed, name) assert not value.flags["WRITEABLE"], f"{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.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