autocal layout first design
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
"""Display-only force baseline / deadband (value cards)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from orisys_app_shared.force.presentation import ForcePresentation, ZeroEstimate
|
||||
|
||||
|
||||
def test_fit_zero_rejects_insufficient_samples() -> None:
|
||||
presentation = ForcePresentation(enabled=True, min_samples=30)
|
||||
estimate = presentation.fit_zero([(1.0, 2.0, 3.0), (1.1, 2.1, 3.1)])
|
||||
assert estimate.quality == "insufficient"
|
||||
|
||||
|
||||
def test_fit_zero_and_accept_deadband() -> None:
|
||||
rng = np.random.default_rng(0)
|
||||
bias = np.array([1262.0, -11.0, -353.0])
|
||||
noise = rng.normal(0.0, 20.0, size=(80, 3))
|
||||
samples = (bias + noise).tolist()
|
||||
presentation = ForcePresentation(
|
||||
enabled=True,
|
||||
min_samples=30,
|
||||
baseline_seconds=0.0,
|
||||
min_deadband=50.0,
|
||||
deadband_percentile=99.0,
|
||||
)
|
||||
estimate = presentation.fit_zero(samples)
|
||||
assert estimate.quality == "ok"
|
||||
presentation.apply_estimate(estimate)
|
||||
idle = presentation.accept(tuple(bias), unit="raw")
|
||||
assert idle.zero_zone
|
||||
assert idle.values == (0.0, 0.0, 0.0)
|
||||
# Curves / CSV keep the measurement: accept never mutates the input tuple.
|
||||
measured = (1262.0, -11.0, -353.0)
|
||||
presentation.accept(measured, unit="raw")
|
||||
assert measured == (1262.0, -11.0, -353.0)
|
||||
|
||||
|
||||
def test_observe_skips_contact_contamination() -> None:
|
||||
presentation = ForcePresentation(
|
||||
enabled=True,
|
||||
min_samples=5,
|
||||
baseline_seconds=0.1,
|
||||
require_no_contact=True,
|
||||
)
|
||||
for i in range(10):
|
||||
out = presentation.observe(
|
||||
(1000.0, 0.0, 0.0),
|
||||
t_rel_s=i * 0.05,
|
||||
contact_found=True,
|
||||
flow_p95=0.1,
|
||||
)
|
||||
assert out is None
|
||||
assert not presentation.ready
|
||||
|
||||
|
||||
def test_observe_freezes_after_quiet_window() -> None:
|
||||
presentation = ForcePresentation(
|
||||
enabled=True,
|
||||
min_samples=10,
|
||||
baseline_seconds=0.5,
|
||||
min_deadband=10.0,
|
||||
)
|
||||
estimate: ZeroEstimate | None = None
|
||||
for i in range(40):
|
||||
estimate = presentation.observe(
|
||||
(100.0 + (i % 3), -5.0, 2.0),
|
||||
t_rel_s=i * 0.02,
|
||||
flow_p95=0.1,
|
||||
contact_found=False,
|
||||
)
|
||||
if estimate is not None:
|
||||
break
|
||||
assert estimate is not None
|
||||
assert estimate.quality == "ok"
|
||||
assert presentation.ready
|
||||
# Further observes do not chase a new zero once frozen.
|
||||
later = presentation.observe((5000.0, 0.0, 0.0), t_rel_s=10.0, flow_p95=0.0)
|
||||
assert later is None
|
||||
|
||||
|
||||
def test_format_force_stable_digits() -> None:
|
||||
presentation = ForcePresentation(enabled=True)
|
||||
assert presentation.format_force(0.0, unit="raw") == "0"
|
||||
assert presentation.format_force(1262.4, unit="raw") == "1262"
|
||||
assert presentation.format_force(1.2345, unit="N") == "1.234"
|
||||
@@ -127,6 +127,7 @@ def test_the_backend_renders_what_the_primitives_rendered_before():
|
||||
background="captured" if mode == "arrows_stitched" else "pure_color",
|
||||
draw_style="finger",
|
||||
valid_mask=mask,
|
||||
colormap=flow_colormap("dark"),
|
||||
)
|
||||
got = backend.render_image(frame, plan, mode=mode, draw_style="finger")
|
||||
assert np.array_equal(got, expected), mode
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Pure-NumPy overlay geometry for Finger 3D arrows."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from orisys_app_shared.scene3d.overlays import (
|
||||
arrow_instances_to_mesh,
|
||||
build_flow_overlay,
|
||||
build_normal_overlay,
|
||||
force_visual_strength,
|
||||
)
|
||||
from orisys_app_shared.scene3d.synthetic import poc_scene
|
||||
from orisys_app_shared.scene3d.visual_policy import VisualPolicy, select_flow_samples
|
||||
|
||||
|
||||
def test_force_visual_strength_is_capped() -> None:
|
||||
assert force_visual_strength(0.0) == 0.0
|
||||
strong = force_visual_strength(1e9)
|
||||
mild = force_visual_strength(100.0)
|
||||
assert strong >= mild > 0.0
|
||||
assert strong <= 1.0 + 1e-9
|
||||
|
||||
|
||||
def test_flow_and_normal_overlays_on_poc_mapping() -> None:
|
||||
assets = poc_scene()
|
||||
mapping = assets.surface_mapping
|
||||
assert mapping is not None
|
||||
h, w = mapping.shape_hw
|
||||
flow = np.zeros((h, w, 2), dtype=np.float64)
|
||||
flow[1, 1] = (0.8, 0.0)
|
||||
mask = np.asarray(mapping.valid, dtype=bool)
|
||||
policy = VisualPolicy(
|
||||
flow_on_px=0.4,
|
||||
flow_grid_spacing=1,
|
||||
flow_edge_margin=0,
|
||||
max_arrows=50,
|
||||
)
|
||||
samples = select_flow_samples(flow, mask, policy)
|
||||
arrows, diags = build_flow_overlay(samples, mapping, style=policy)
|
||||
assert not diags
|
||||
assert arrows is not None and arrows.origins.shape[0] >= 1
|
||||
contact, cdiags = build_normal_overlay(
|
||||
contact_uv_xy=(1.0, 1.0),
|
||||
contact_found=True,
|
||||
contact_valid=True,
|
||||
fn=500.0,
|
||||
mapping=mapping,
|
||||
)
|
||||
assert not cdiags
|
||||
assert contact is not None and contact.origins.shape[0] == 1
|
||||
# Yellow-ish contact colour.
|
||||
assert float(contact.colors[0, 0]) > 0.7
|
||||
assert float(contact.colors[0, 1]) > 0.7
|
||||
|
||||
positions, triangles, colors = arrow_instances_to_mesh(arrows)
|
||||
assert positions.ndim == 2 and positions.shape[1] == 3
|
||||
assert triangles.ndim == 2 and triangles.shape[1] == 3
|
||||
assert colors.shape[0] == positions.shape[0]
|
||||
|
||||
|
||||
def test_missing_contact_yields_no_arrow() -> None:
|
||||
assets = poc_scene()
|
||||
mapping = assets.surface_mapping
|
||||
assert mapping is not None
|
||||
arrows, _ = build_normal_overlay(
|
||||
contact_uv_xy=(1.0, 1.0),
|
||||
contact_found=False,
|
||||
contact_valid=True,
|
||||
fn=100.0,
|
||||
mapping=mapping,
|
||||
)
|
||||
assert arrows.origins.shape[0] == 0
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Shared 2D/3D flow sampling and domain-mask rules."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from orisys_app_shared.scene3d.contracts import ThreeDError
|
||||
from orisys_app_shared.scene3d.visual_policy import (
|
||||
VisualPolicy,
|
||||
build_domain_mask,
|
||||
paint_domain_background,
|
||||
select_flow_samples,
|
||||
)
|
||||
|
||||
|
||||
def test_build_domain_mask_rejects_shape_mismatch() -> None:
|
||||
geo = np.ones((4, 5), dtype=bool)
|
||||
with pytest.raises(ThreeDError):
|
||||
build_domain_mask(geo, shape_hw=(3, 5))
|
||||
|
||||
|
||||
def test_build_domain_mask_intersects_roi() -> None:
|
||||
geo = np.ones((3, 3), dtype=bool)
|
||||
roi = np.zeros((3, 3), dtype=bool)
|
||||
roi[1, 1] = True
|
||||
mask, diags = build_domain_mask(geo, roi)
|
||||
assert mask is not None
|
||||
assert bool(mask[1, 1]) and not bool(mask[0, 0])
|
||||
assert diags == ()
|
||||
|
||||
|
||||
def test_select_flow_samples_uses_shared_grid_and_mask() -> None:
|
||||
h, w = 40, 40
|
||||
flow = np.zeros((h, w, 2), dtype=np.float64)
|
||||
flow[15, 15] = (0.5, 0.0) # |2*flow| = 1.0
|
||||
mask = np.ones((h, w), dtype=bool)
|
||||
mask[:, :5] = False
|
||||
policy = VisualPolicy(flow_on_px=0.4, flow_grid_spacing=10, flow_edge_margin=0)
|
||||
samples = select_flow_samples(flow, mask, policy)
|
||||
assert samples.rows.size >= 1
|
||||
assert all(c >= 5 for c in samples.cols.tolist())
|
||||
pairs = set(zip(samples.rows.tolist(), samples.cols.tolist()))
|
||||
assert (15, 15) in pairs
|
||||
|
||||
|
||||
def test_select_flow_samples_hysteresis_keeps_weaker_motion() -> None:
|
||||
h, w = 30, 30
|
||||
flow = np.zeros((h, w, 2), dtype=np.float64)
|
||||
# |2*flow| = 0.7: below on (0.8) but above off (0.5)
|
||||
flow[15, 15] = (0.35, 0.0)
|
||||
policy = VisualPolicy(
|
||||
flow_on_px=0.8,
|
||||
flow_off_px=0.5,
|
||||
flow_grid_spacing=10,
|
||||
flow_edge_margin=0,
|
||||
)
|
||||
previously = np.zeros((h, w), dtype=bool)
|
||||
previously[15, 15] = True
|
||||
off = select_flow_samples(flow, None, policy, previously_on=None)
|
||||
on = select_flow_samples(flow, None, policy, previously_on=previously)
|
||||
assert off.rows.size == 0
|
||||
assert on.rows.size == 1
|
||||
|
||||
|
||||
def test_paint_domain_background_colours() -> None:
|
||||
mask = np.zeros((4, 4), dtype=bool)
|
||||
mask[1:3, 1:3] = True
|
||||
canvas = paint_domain_background(
|
||||
(4, 4), mask, island_bgr=(10, 20, 30), exterior_bgr=(1, 2, 3)
|
||||
)
|
||||
assert tuple(canvas[0, 0]) == (1, 2, 3)
|
||||
assert tuple(canvas[1, 1]) == (10, 20, 30)
|
||||
Reference in New Issue
Block a user