Files
Uni-Lab-OS/unilabos/layout_optimizer/tests/test_seeders.py
2026-03-31 09:30:40 +08:00

114 lines
4.6 KiB
Python
Raw 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.
"""Tests for the force-directed seeder engine."""
import math
import pytest
from ..seeders import SeederParams, PRESETS, seed_layout
from ..models import Device, Lab, Placement
class TestSeederParams:
def test_presets_exist(self):
assert "compact_outward" in PRESETS
assert "spread_inward" in PRESETS
assert "row_fallback" in PRESETS
def test_compact_has_negative_boundary(self):
assert PRESETS["compact_outward"].boundary_attraction < 0
def test_spread_has_positive_boundary(self):
assert PRESETS["spread_inward"].boundary_attraction > 0
class TestSeedLayout:
"""seed_layout must return valid placements: within bounds, one per device."""
def _make_devices(self, n: int) -> list[Device]:
return [Device(id=f"d{i}", name=f"Device {i}", bbox=(0.6, 0.4)) for i in range(n)]
def test_returns_one_placement_per_device(self):
devices = self._make_devices(5)
lab = Lab(width=5.0, depth=4.0)
result = seed_layout(devices, lab, PRESETS["compact_outward"])
assert len(result) == 5
ids = {p.device_id for p in result}
assert ids == {f"d{i}" for i in range(5)}
def test_placements_within_bounds(self):
devices = self._make_devices(5)
lab = Lab(width=5.0, depth=4.0)
for preset_name in ["compact_outward", "spread_inward"]:
result = seed_layout(devices, lab, PRESETS[preset_name])
for p in result:
assert 0 <= p.x <= lab.width, f"{preset_name}: x={p.x} out of bounds"
assert 0 <= p.y <= lab.depth, f"{preset_name}: y={p.y} out of bounds"
def test_empty_devices(self):
result = seed_layout([], Lab(width=5, depth=4), PRESETS["compact_outward"])
assert result == []
def test_single_device(self):
devices = self._make_devices(1)
lab = Lab(width=5.0, depth=4.0)
result = seed_layout(devices, lab, PRESETS["compact_outward"])
assert len(result) == 1
assert 0 <= result[0].x <= lab.width
assert 0 <= result[0].y <= lab.depth
def test_row_fallback_delegates(self):
"""row_fallback preset uses generate_fallback, not force engine."""
devices = self._make_devices(3)
lab = Lab(width=5.0, depth=4.0)
# row_fallback is None in PRESETS; seed_layout detects and delegates
result = seed_layout(devices, lab, None) # None = row_fallback
assert len(result) == 3
def test_lab_too_small_returns_results_not_crash(self):
"""When space is insufficient, seeder still returns placements (may have collisions)."""
devices = [Device(id=f"d{i}", name=f"D{i}", bbox=(1.0, 1.0)) for i in range(20)]
lab = Lab(width=2.0, depth=2.0) # Way too small for 20 1m×1m devices
result = seed_layout(devices, lab, PRESETS["compact_outward"])
assert len(result) == 20 # All placed, even if overlapping
for p in result:
assert 0 <= p.x <= lab.width
assert 0 <= p.y <= lab.depth
def test_compact_clusters_toward_center(self):
"""compact_outward should place devices closer to center than spread_inward."""
devices = self._make_devices(4)
lab = Lab(width=8.0, depth=8.0)
center_x, center_y = lab.width / 2, lab.depth / 2
compact = seed_layout(devices, lab, PRESETS["compact_outward"])
spread = seed_layout(devices, lab, PRESETS["spread_inward"])
avg_dist_compact = sum(
math.sqrt((p.x - center_x)**2 + (p.y - center_y)**2) for p in compact
) / len(compact)
avg_dist_spread = sum(
math.sqrt((p.x - center_x)**2 + (p.y - center_y)**2) for p in spread
) / len(spread)
assert avg_dist_compact < avg_dist_spread
class TestOrientation:
"""Orientation modes should set theta based on position relative to center."""
def test_outward_orientation_sets_theta(self):
"""compact_outward: devices should have non-zero theta."""
devices = [
Device(id="a", name="A", bbox=(0.6, 0.4)),
Device(id="b", name="B", bbox=(0.6, 0.4)),
]
lab = Lab(width=5.0, depth=4.0)
result = seed_layout(devices, lab, PRESETS["compact_outward"])
thetas = [p.theta for p in result]
assert any(t != 0.0 for t in thetas) or len(devices) == 1
def test_none_orientation_keeps_zero(self):
"""orientation_mode='none': all thetas stay 0."""
devices = [Device(id="a", name="A", bbox=(0.6, 0.4))]
lab = Lab(width=5.0, depth=4.0)
params = SeederParams(boundary_attraction=0.0, orientation_mode="none")
result = seed_layout(devices, lab, params)
assert result[0].theta == 0.0