NEUROGRAFT / tests /test_navigation_controller.py
Kai9987kai's picture
Release NEUROGRAFT v0.10.0: matched learned-navigation study with all-source residual readout and symmetry augmentation
9563b12 verified
Raw History Blame Contribute Delete
12.2 kB
"""Observation-only navigation separates policy proposals from controller aid."""
import copy
import json
import random
import pytest
ACTIONS = ((0, -1), (1, 0), (0, 1), (-1, 0))
def observation(size=7, agent=(3, 3), goal=(5, 1), walls=(0, 0, 0, 0)):
scale = size - 1
x, z = agent
gx, gz = goal
return [[x / scale, z / scale, 0.0, -1.0],
[gx / scale, gz / scale, (gx - x) / scale, (gz - z) / scale],
list(walls), [0.0, 0.0, 0.0, 1.0]]
class TinyGrid:
def __init__(self, size, free, start, goal):
self.size, self.free, self.agent, self.goal = size, set(free), start, goal
self.trail = [start]
def observe(self):
x, z = self.agent
walls = [(x + dx, z + dz) not in self.free for dx, dz in ACTIONS]
return observation(self.size, self.agent, self.goal, tuple(int(v) for v in walls))
def move(self, action):
dx, dz = ACTIONS[action]
target = (self.agent[0] + dx, self.agent[1] + dz)
assert target in self.free, 'controller attempted a collision'
self.agent = target
self.trail.append(target)
def test_legal_mask_selects_best_available_action_and_preserves_inputs():
from neurograft.navigation_controller import MODES, legal_action
obs = observation(walls=(1, 0, 0, 1))
logits = [100.0, 2.0, 3.0, 20.0]
before = copy.deepcopy((obs, logits))
assert MODES == ('policy', 'masked_policy', 'observed_map')
assert legal_action(obs, logits) == 2
assert (obs, logits) == before
assert legal_action(obs, [0, 0, 0, 0]) == 1
@pytest.mark.parametrize('change', [
lambda obs: obs.pop(),
lambda obs: obs[0].pop(),
lambda obs: obs[0].__setitem__(0, float('nan')),
lambda obs: obs[1].__setitem__(2, float('inf')),
lambda obs: obs[2].__setitem__(1, 0.5),
lambda obs: obs[2].__setitem__(1, True),
lambda obs: obs.__setitem__(2, [1, 1, 1, 1]),
])
def test_mask_rejects_invalid_sensor_data(change):
from neurograft.navigation_controller import legal_action
obs = observation()
change(obs)
with pytest.raises(ValueError):
legal_action(obs, [0, 1, 2, 3])
@pytest.mark.parametrize('logits', [[1, 2, 3], [0, 1, 2, float('nan')],
[0, 1, 2, float('inf')], [0, 1, 2, True], '0123'])
def test_invalid_prior_is_rejected(logits):
from neurograft.navigation_controller import legal_action
with pytest.raises(ValueError):
legal_action(observation(), logits)
def test_mapping_records_only_current_and_four_neighbors():
from neurograft.navigation_controller import ObservedMapNavigator
navigator = ObservedMapNavigator(7)
obs = observation(walls=(1, 0, 1, 0))
before = copy.deepcopy(obs)
rng = random.getstate()
action = navigator.choose(obs, [9, 8, 7, 6])
assert action == 1
assert navigator.known_free == {(3, 3), (4, 3), (2, 3)}
assert navigator.known_blocked == {(3, 2), (3, 4)}
assert navigator.visits == {(3, 3): 1}
assert obs == before and random.getstate() == rng
snapshot = navigator.snapshot()
assert snapshot['mode'] == 'observed_map'
assert snapshot['policy_action'] == 0 and snapshot['last_action'] == 1
assert snapshot['overridden'] is True
assert snapshot['known_free'] == 3 and snapshot['known_blocked'] == 2
assert snapshot['visited'] == 1 and snapshot['frontiers'] == 2
assert snapshot['path'][0] == {'x': 3, 'z': 3}
assert snapshot['size'] == 7
assert snapshot['position'] == [3, 3] and snapshot['goal'] == [5, 1]
assert snapshot['free_cells'] == [[2, 3], [3, 3], [4, 3]]
assert snapshot['blocked_cells'] == [[3, 2], [3, 4]]
assert snapshot['visited_cells'] == [[3, 3]]
snapshot['free_cells'].clear()
assert len(navigator.known_free) == 3
json.dumps(snapshot, allow_nan=False)
def test_equal_frontiers_use_prior_and_map_only_control_ignores_it():
from neurograft.navigation_controller import ObservedMapNavigator
obs = observation(goal=(5, 1))
north = ObservedMapNavigator(7)
east = ObservedMapNavigator(7)
assert north.choose(obs, [9, 8, 0, 0]) == 0
assert east.choose(obs, [8, 9, 0, 0]) == 1
left = ObservedMapNavigator(7)
right = ObservedMapNavigator(7)
assert left.choose(obs, [9, 8, 0, 0], use_policy=False) == 0
assert right.choose(obs, [8, 9, 0, 0], use_policy=False) == 0
# The raw proposal remains separately visible even when it is not consulted.
assert left.snapshot()['policy_action'] == 0
assert right.snapshot()['policy_action'] == 1
def test_known_goal_route_overrides_a_strong_wrong_prior():
from neurograft.navigation_controller import ObservedMapNavigator
navigator = ObservedMapNavigator(7)
assert navigator.choose(observation(goal=(4, 3)), [100, 0, 0, 0]) == 1
assert navigator.snapshot()['reason'] == 'known_goal'
assert navigator.snapshot()['path'] == [{'x': 3, 'z': 3}, {'x': 4, 'z': 3}]
def test_equal_known_goal_routes_use_prior_but_keep_shortest_path():
from neurograft.navigation_controller import ObservedMapNavigator
navigator = ObservedMapNavigator(7)
for position in ((3, 3), (3, 2), (4, 2)):
navigator.choose(observation(agent=position), [0, 1, 2, 3])
north = ObservedMapNavigator.from_dict(navigator.to_dict())
east = ObservedMapNavigator.from_dict(navigator.to_dict())
obs = observation(agent=(3, 3), goal=(4, 2))
assert north.choose(obs, [9, 8, 0, 0]) == 0
assert east.choose(obs, [8, 9, 0, 0]) == 1
assert len(north.snapshot()['path']) == len(east.snapshot()['path']) == 3
def test_discontinuous_unseen_position_requires_episodic_reset():
from neurograft.navigation_controller import ObservedMapNavigator
navigator = ObservedMapNavigator(7)
navigator.choose(observation(agent=(2, 2)), [0, 1, 2, 3])
before = navigator.to_dict()
with pytest.raises(ValueError, match='position'):
navigator.choose(observation(agent=(4, 4)), [0, 1, 2, 3])
assert navigator.to_dict() == before
def test_goal_cannot_conflict_with_sensed_wall_and_failure_is_atomic():
from neurograft.navigation_controller import ObservedMapNavigator
navigator = ObservedMapNavigator(7)
before = navigator.to_dict()
with pytest.raises(ValueError, match='goal'):
navigator.choose(observation(goal=(4, 3), walls=(0, 1, 0, 0)), [0, 1, 2, 3])
assert navigator.to_dict() == before
def test_map_exploration_reaches_goal_around_a_wall_without_collisions():
from neurograft.navigation_controller import ObservedMapNavigator
free = {(x, z) for x in range(1, 6) for z in range(1, 6)} - {(3, 2), (3, 3), (3, 4)}
world = TinyGrid(7, free, (2, 3), (4, 3))
navigator = ObservedMapNavigator(7)
for _ in range(80):
world.move(navigator.choose(world.observe(), [0, 10, 0, 0]))
if world.agent == world.goal:
break
assert world.agent == world.goal
assert len(world.trail) - 1 > 2
assert navigator.known_free <= free
assert len(navigator.known_free | navigator.known_blocked) <= 49
def test_dead_end_backtracking_keeps_frontier_and_restores_exact_continuation():
from neurograft.navigation_controller import ObservedMapNavigator
free = {(1, 3), (2, 3), (3, 3), (3, 2), (3, 1), (4, 3), (5, 3), (5, 2), (5, 1)}
world = TinyGrid(7, free, (2, 3), (5, 1))
navigator = ObservedMapNavigator(7)
committed = None
restored = None
for _ in range(30):
obs = world.observe()
action = navigator.choose(obs, [10, 9, 0, 0])
if world.agent == (3, 1):
committed = navigator.target
assert committed is not None and committed != world.agent
state = json.loads(json.dumps(navigator.to_dict()))
restored = ObservedMapNavigator.from_dict(state)
assert restored.to_dict() == state
assert restored.snapshot() == navigator.snapshot()
elif restored is not None:
assert restored.choose(obs, [10, 9, 0, 0]) == action
assert restored.to_dict() == navigator.to_dict()
if world.agent == (3, 2):
assert navigator.target == committed
world.move(action)
if world.agent == world.goal:
break
assert world.agent == world.goal
assert (3, 1) in world.trail and world.trail.count((3, 2)) >= 2
def test_hidden_cells_do_not_affect_action_or_memory():
from neurograft.navigation_controller import ObservedMapNavigator
free = {(x, z) for x in range(1, 6) for z in range(1, 6)}
first = TinyGrid(7, free, (3, 3), (5, 1))
second = TinyGrid(7, free - {(1, 1), (5, 5)}, (3, 3), (5, 1))
left, right = ObservedMapNavigator(7), ObservedMapNavigator(7)
assert first.observe() == second.observe()
assert left.choose(first.observe(), [0, 9, 0, 0]) == right.choose(second.observe(), [0, 9, 0, 0])
assert left.to_dict() == right.to_dict()
@pytest.mark.parametrize('size', [True, 4, 32, 7.0, None])
def test_grid_size_is_strict_and_bounded(size):
from neurograft.navigation_controller import ObservedMapNavigator
with pytest.raises(ValueError):
ObservedMapNavigator(size)
@pytest.mark.parametrize('change', [
lambda obs: obs[0].__setitem__(0, 0.49),
lambda obs: obs[1].__setitem__(0, 1.0),
lambda obs: obs[2].__setitem__(0, 0), # Previously sensed wall cannot become free.
lambda obs: obs[3].__setitem__(3, float('nan')),
])
def test_invalid_observation_leaves_all_controller_state_untouched(change):
from neurograft.navigation_controller import ObservedMapNavigator
navigator = ObservedMapNavigator(7)
navigator.choose(observation(walls=(1, 0, 0, 0)), [0, 1, 2, 3])
before = navigator.to_dict()
obs = observation(walls=(1, 0, 0, 0))
change(obs)
with pytest.raises(ValueError):
navigator.choose(obs, [0, 1, 2, 3])
assert navigator.to_dict() == before
def test_boundary_free_sensor_and_invalid_prior_are_atomic():
from neurograft.navigation_controller import ObservedMapNavigator
navigator = ObservedMapNavigator(7)
before = navigator.to_dict()
for obs, logits in [(observation(agent=(1, 1)), [0, 1, 2, 3]),
(observation(), [0, 1, 2, float('nan')])]:
with pytest.raises(ValueError):
navigator.choose(obs, logits)
assert navigator.to_dict() == before
@pytest.mark.parametrize('mutate', [
lambda state: state.__setitem__('version', 2),
lambda state: state.__setitem__('extra', 1),
lambda state: state['known_free'].append(state['known_free'][0]),
lambda state: state['known_free'].append([-1, 2]),
lambda state: state['known_blocked'].append(state['known_free'][0]),
lambda state: state['visits'][0].__setitem__('count', True),
lambda state: state['visits'][0].__setitem__('count', 0),
lambda state: state.__setitem__('target', [1, 1]),
lambda state: state.__setitem__('last_action', 4),
lambda state: state.__setitem__('policy_action', True),
lambda state: state.__setitem__('reason', 'invented'),
lambda state: state['path'].append([1, 1]),
lambda state: state.__setitem__('use_policy', 1),
])
def test_restoration_rejects_malformed_and_inconsistent_state_without_mutating_input(mutate):
from neurograft.navigation_controller import ObservedMapNavigator
navigator = ObservedMapNavigator(7)
navigator.choose(observation(), [0, 1, 2, 3])
state = navigator.to_dict()
mutate(state)
before = copy.deepcopy(state)
with pytest.raises(ValueError):
ObservedMapNavigator.from_dict(state)
assert state == before
def test_empty_controller_has_exact_json_roundtrip_and_serialization_is_detached():
from neurograft.navigation_controller import ObservedMapNavigator
navigator = ObservedMapNavigator(31)
state = navigator.to_dict()
restored = ObservedMapNavigator.from_dict(json.loads(json.dumps(state)))
assert restored.to_dict() == state
assert restored.snapshot() == navigator.snapshot()
navigator.choose(observation(31, (15, 15), (29, 1)), [0, 1, 2, 3])
detached = navigator.to_dict()
detached['known_free'].clear()
assert navigator.known_free