mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-05-23 22:12:28 +00:00
114 lines
4.6 KiB
Python
114 lines
4.6 KiB
Python
"""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
|