"""Observation builder — shape, normalisation, order invariance.""" import math import numpy as np import pytest from herding.perception.obs import OBS_DIM, build_obs def test_obs_shape_and_dtype(): obs = build_obs((0.0, 0.0), 0.0, [(5.0, 5.0)], [False]) assert obs.shape == (OBS_DIM,) assert obs.dtype == np.float32 def test_obs_no_active_sheep_terminal(): # All sheep penned → flock-summary fields zero, count zero. obs = build_obs((0.0, 0.0), 0.0, [(1.0, 1.0), (2.0, 2.0)], [True, True]) assert obs[19] == 0.0 # Aggregate fields (CoM, radius, std, vectors) should all be zero. assert np.allclose(obs[4:12], 0.0) def test_obs_dog_pose_normalised(): obs = build_obs((15.0, -15.0), math.pi / 2, [(0.0, 0.0)], [False]) assert math.isclose(obs[0], 1.0) assert math.isclose(obs[1], -1.0) assert math.isclose(obs[2], math.cos(math.pi / 2), abs_tol=1e-6) assert math.isclose(obs[3], math.sin(math.pi / 2), abs_tol=1e-6) def test_obs_order_invariance(): """Sheep order in the input list must not affect the observation.""" sheep = [(3.0, 2.0), (-5.0, 1.0), (0.0, 8.0)] p = [False] * 3 a = build_obs((0.0, 0.0), 0.0, sheep, p) b = build_obs((0.0, 0.0), 0.0, list(reversed(sheep)), list(reversed(p))) assert np.allclose(a, b) def test_obs_count_field_normalised_by_n_max(): sheep = [(1.0, 1.0)] * 5 p = [False] * 5 obs = build_obs((0.0, 0.0), 0.0, sheep, p, n_max=10) assert math.isclose(obs[19], 0.5) def test_obs_polar_histogram_sums_to_one(): sheep = [(1.0, 0.0), (-1.0, 0.0), (0.0, 1.0), (0.0, -1.0)] obs = build_obs((0.0, 0.0), 0.0, sheep, [False] * 4) assert math.isclose(float(obs[20:28].sum()), 1.0, abs_tol=1e-6) def test_obs_named_channels_closest_rearmost(): # Channels 28..29 = (closest_to_pen - dog) / 15 # Channels 30..31 = (rearmost - dog) / 15 pen_x, pen_y = 11.5, -15.0 near = (pen_x + 1.0, pen_y + 1.0) far = (-10.0, 10.0) obs = build_obs((0.0, 0.0), 0.0, [near, far], [False, False]) tol = 1e-5 assert math.isclose(obs[28], near[0] / 15.0, abs_tol=tol) assert math.isclose(obs[29], near[1] / 15.0, abs_tol=tol) assert math.isclose(obs[30], far[0] / 15.0, abs_tol=tol) assert math.isclose(obs[31], far[1] / 15.0, abs_tol=tol) def test_obs_pen_vector_zero_at_pen_entry(): obs = build_obs((11.5, -15.0), 0.0, [(0.0, 0.0)], [False]) assert math.isclose(obs[14], 0.0) # distance to pen