mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-05-23 18:49:56 +00:00
Replace scipy differential_evolution with custom DE loop for per-device crossover, circular θ wrapping, and configurable mutation strategy (currenttobest1bin default, best1bin as turbo mode). Key improvements: - Graduate ALL hard constraints during DE (proportional penalty instead of flat inf), giving DE smooth gradient for reachability, min_spacing, etc. Binary inf preserved for final pass/fail reporting. - 2-axis sweep-and-prune AABB broad phase for collision pair pruning - Multi-seed injection from multiple seeder presets + Gaussian variants - snap_theta_safe: collision-check after angle snapping, revert on violation - Weight normalization (100 distance / 60 angle / 5× hard multiplier) - Constraint priority field (critical/high/normal/low → weight multiplier) with LLM intent interpreter setting priority per constraint type - Final success field now checks user hard constraints in binary mode - arm_slider added to mock checker reach table (1.07m) Tests: 202 passed, 24 new tests added (optimizer 7, constraints 6, broad_phase 11) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
242 lines
9.2 KiB
Python
242 lines
9.2 KiB
Python
"""Tests for broad_phase.py — 2 轴 sweep-and-prune 宽相碰撞检测。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import math
|
||
import random
|
||
|
||
import pytest
|
||
|
||
from ..broad_phase import broad_phase_device_pairs, sweep_and_prune_pairs
|
||
from ..models import Device, Placement
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 测试用辅助函数
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def _make_device(device_id: str, w: float = 0.6, d: float = 0.4) -> Device:
|
||
"""创建简单测试设备。"""
|
||
return Device(id=device_id, name=device_id, bbox=(w, d))
|
||
|
||
|
||
def _make_placement(
|
||
device_id: str, x: float, y: float, theta: float = 0.0
|
||
) -> Placement:
|
||
"""创建简单测试放置。"""
|
||
return Placement(device_id=device_id, x=x, y=y, theta=theta)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 测试类
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class TestNoOverlap:
|
||
"""两台设备距离足够远,宽相不返回候选对。"""
|
||
|
||
def test_no_overlap_returns_empty(self):
|
||
"""水平方向间距远大于 AABB 尺寸 → 0 候选对。"""
|
||
devices = [_make_device("A", 1.0, 1.0), _make_device("B", 1.0, 1.0)]
|
||
placements = [
|
||
_make_placement("A", 0.0, 0.0),
|
||
_make_placement("B", 10.0, 0.0),
|
||
]
|
||
pairs = sweep_and_prune_pairs(devices, placements)
|
||
assert pairs == []
|
||
|
||
|
||
class TestOverlapping:
|
||
"""两台设备 AABB 明显重叠。"""
|
||
|
||
def test_overlapping_devices_returned(self):
|
||
"""两台 1×1 设备中心距 0.5m → 1 候选对。"""
|
||
devices = [_make_device("A", 1.0, 1.0), _make_device("B", 1.0, 1.0)]
|
||
placements = [
|
||
_make_placement("A", 0.0, 0.0),
|
||
_make_placement("B", 0.5, 0.0),
|
||
]
|
||
pairs = sweep_and_prune_pairs(devices, placements)
|
||
assert len(pairs) == 1
|
||
assert pairs[0] == (0, 1)
|
||
|
||
|
||
class TestXOverlapYNoOverlap:
|
||
"""x 轴投影交叠但 y 轴不交叠,应被 y 轴检查剪枝。"""
|
||
|
||
def test_x_overlap_y_no_overlap(self):
|
||
"""水平接近但垂直方向偏移足够大 → 0 候选对。"""
|
||
devices = [_make_device("A", 2.0, 1.0), _make_device("B", 2.0, 1.0)]
|
||
placements = [
|
||
_make_placement("A", 0.0, 0.0),
|
||
_make_placement("B", 0.5, 5.0), # x 轴交叠但 y 轴相距很远
|
||
]
|
||
pairs = sweep_and_prune_pairs(devices, placements)
|
||
assert pairs == []
|
||
|
||
|
||
class TestTouchingDevices:
|
||
"""AABB 恰好接触(边缘距离 = 0)应作为候选对返回。"""
|
||
|
||
def test_touching_devices_included(self):
|
||
"""两个 1×1 设备中心距恰好为 1.0(半宽 0.5 + 0.5 = 1.0),
|
||
AABB 边界接触 → 应包含在候选对中(<= 判定)。"""
|
||
devices = [_make_device("A", 1.0, 1.0), _make_device("B", 1.0, 1.0)]
|
||
placements = [
|
||
_make_placement("A", 0.0, 0.0),
|
||
_make_placement("B", 1.0, 0.0), # xmax_A = 0.5, xmin_B = 0.5 → 接触
|
||
]
|
||
pairs = sweep_and_prune_pairs(devices, placements)
|
||
# 接触算作潜在碰撞,安全起见需保留
|
||
assert len(pairs) == 1
|
||
assert pairs[0] == (0, 1)
|
||
|
||
|
||
class TestMultipleDevices:
|
||
"""4 台设备验证精确的候选对列表。"""
|
||
|
||
def test_multiple_devices_correct_pairs(self):
|
||
"""排列 4 台设备,只有特定配对 AABB 交叠。
|
||
|
||
布局(1×1 设备):
|
||
A(0,0) B(0.8,0) — A-B 交叠(中心距 0.8 < 1.0)
|
||
C(0,5) — 远离 A、B
|
||
D(0.9,5) — C-D 交叠(中心距 0.9 < 1.0)
|
||
|
||
期望候选对: (A,B) 和 (C,D)。
|
||
"""
|
||
devices = [
|
||
_make_device("A", 1.0, 1.0),
|
||
_make_device("B", 1.0, 1.0),
|
||
_make_device("C", 1.0, 1.0),
|
||
_make_device("D", 1.0, 1.0),
|
||
]
|
||
placements = [
|
||
_make_placement("A", 0.0, 0.0),
|
||
_make_placement("B", 0.8, 0.0),
|
||
_make_placement("C", 0.0, 5.0),
|
||
_make_placement("D", 0.9, 5.0),
|
||
]
|
||
pairs = sweep_and_prune_pairs(devices, placements)
|
||
pair_set = set(pairs)
|
||
assert (0, 1) in pair_set # A-B
|
||
assert (2, 3) in pair_set # C-D
|
||
assert len(pair_set) == 2
|
||
|
||
|
||
class TestRotatedDeviceAabb:
|
||
"""旋转设备导致 AABB 变大,命中候选对。"""
|
||
|
||
def test_rotated_device_aabb(self):
|
||
"""一台窄长设备 (2.0×0.2):
|
||
- 未旋转时 AABB 半宽 = 1.0,两设备中心距 2.5 → 不交叠
|
||
- 旋转 90° 后 AABB 半宽 = 0.1,半深 = 1.0 → 仍不交叠
|
||
- 旋转 45° 后 AABB 半宽 ≈ (2*cos45 + 0.2*sin45)/2 ≈ 0.778
|
||
但另一台放在 x=1.6,半宽 = 1.0
|
||
所以 xmax_A = 0 + 0.778 = 0.778 < 1.6 - 1.0 = 0.6 → 不够
|
||
|
||
更好的方案:用中心距 1.5,未旋转时不交叠,旋转后交叠。
|
||
未旋转: half_w_A = 0.3 (bbox 0.6x2.0), half_w_B = 0.3
|
||
A: xmax = 0 + 0.3 = 0.3, B: xmin = 1.5 - 0.3 = 1.2 → 不交叠
|
||
旋转 90°: A 的 bbox (0.6, 2.0) → half_w = (0.6*0 + 2.0*1)/2 = 1.0
|
||
A: xmax = 0 + 1.0 = 1.0, B: xmin = 1.5 - 0.3 = 1.2 → 仍不交叠
|
||
|
||
用 bbox (0.4, 2.0),间距 1.2:
|
||
未旋转: half_w = 0.2, xmax_A = 0.2, xmin_B = 1.2 - 0.2 = 1.0 → 不交叠
|
||
旋转 45°: half_w = (0.4*cos45 + 2.0*sin45)/2 = (0.283+1.414)/2 = 0.849
|
||
xmax_A = 0.849, xmin_B = 1.2 - 0.2 = 1.0 → 不交叠
|
||
|
||
间距 0.8:
|
||
未旋转: xmax_A = 0.2, xmin_B = 0.8 - 0.2 = 0.6 → 不交叠 ✓
|
||
旋转 45°: xmax_A = 0.849, xmin_B = 0.6 → 交叠 ✓ (0.849 > 0.6)
|
||
y 轴: half_d_A_rot = (0.4*sin45 + 2.0*cos45)/2 = 0.849, half_d_B = 1.0
|
||
ymax_A = 0.849, ymin_B = -1.0 → 交叠 ✓
|
||
"""
|
||
dev_narrow = _make_device("narrow", 0.4, 2.0)
|
||
dev_normal = _make_device("normal", 0.4, 2.0)
|
||
# 未旋转:不交叠
|
||
placements_no_rot = [
|
||
_make_placement("narrow", 0.0, 0.0, theta=0.0),
|
||
_make_placement("normal", 0.8, 0.0, theta=0.0),
|
||
]
|
||
assert sweep_and_prune_pairs([dev_narrow, dev_normal], placements_no_rot) == []
|
||
|
||
# narrow 旋转 45° → AABB 变大 → 交叠
|
||
placements_rot = [
|
||
_make_placement("narrow", 0.0, 0.0, theta=math.pi / 4),
|
||
_make_placement("normal", 0.8, 0.0, theta=0.0),
|
||
]
|
||
pairs = sweep_and_prune_pairs([dev_narrow, dev_normal], placements_rot)
|
||
assert len(pairs) == 1
|
||
|
||
|
||
class TestOriginalIndices:
|
||
"""验证返回的索引对应 placements 原始顺序而非排序后顺序。"""
|
||
|
||
def test_sorted_output_preserves_original_indices(self):
|
||
"""故意让 placements 按 x 坐标逆序排列,
|
||
验证返回的索引仍是原始顺序。"""
|
||
devices = [
|
||
_make_device("A", 1.0, 1.0),
|
||
_make_device("B", 1.0, 1.0),
|
||
_make_device("C", 1.0, 1.0),
|
||
]
|
||
# 逆序排列:C 在最左,A 在最右
|
||
placements = [
|
||
_make_placement("A", 5.0, 0.0), # idx 0, 最右
|
||
_make_placement("B", 4.5, 0.0), # idx 1, 中间(与 A 交叠)
|
||
_make_placement("C", 0.0, 0.0), # idx 2, 最左(独立)
|
||
]
|
||
pairs = sweep_and_prune_pairs(devices, placements)
|
||
# A(idx=0) 和 B(idx=1) AABB 交叠,索引应为 (0, 1)
|
||
assert len(pairs) == 1
|
||
assert pairs[0] == (0, 1)
|
||
|
||
# 同时验证 broad_phase_device_pairs 返回正确 device_id
|
||
id_pairs = broad_phase_device_pairs(devices, placements)
|
||
assert id_pairs == [("A", "B")]
|
||
|
||
|
||
class TestPairCountReduction:
|
||
"""大规模随机测试:宽相候选对数应远小于 N*(N-1)/2。"""
|
||
|
||
def test_pair_count_reduction(self):
|
||
"""N=15 台设备随机放置在 10×10 实验室 → 候选对数显著少于全量。"""
|
||
random.seed(42)
|
||
n = 15
|
||
devices = [_make_device(f"D{i}", 0.5, 0.5) for i in range(n)]
|
||
placements = [
|
||
_make_placement(f"D{i}", random.uniform(0, 10), random.uniform(0, 10))
|
||
for i in range(n)
|
||
]
|
||
pairs = sweep_and_prune_pairs(devices, placements)
|
||
full_pairs = n * (n - 1) // 2 # = 105
|
||
# 在 10×10 区域放 15 台 0.5×0.5 设备,交叠率应很低
|
||
assert len(pairs) < full_pairs
|
||
# 额外断言:候选对数不超过全量的一半(保守判定)
|
||
assert len(pairs) < full_pairs * 0.5
|
||
|
||
|
||
class TestEdgeCases:
|
||
"""边界情况。"""
|
||
|
||
def test_empty_input(self):
|
||
"""空列表 → 空结果。"""
|
||
assert sweep_and_prune_pairs([], []) == []
|
||
|
||
def test_single_device(self):
|
||
"""单台设备 → 无候选对。"""
|
||
devices = [_make_device("A")]
|
||
placements = [_make_placement("A", 0.0, 0.0)]
|
||
assert sweep_and_prune_pairs(devices, placements) == []
|
||
|
||
def test_identical_positions(self):
|
||
"""两台设备完全重叠 → 1 候选对。"""
|
||
devices = [_make_device("A", 1.0, 1.0), _make_device("B", 1.0, 1.0)]
|
||
placements = [
|
||
_make_placement("A", 0.0, 0.0),
|
||
_make_placement("B", 0.0, 0.0),
|
||
]
|
||
pairs = sweep_and_prune_pairs(devices, placements)
|
||
assert len(pairs) == 1
|