mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-05-23 19: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>
424 lines
16 KiB
Python
424 lines
16 KiB
Python
"""约束体系测试。"""
|
||
|
||
import math
|
||
|
||
import pytest
|
||
|
||
from ..constraints import (
|
||
evaluate_constraints,
|
||
evaluate_default_hard_constraints,
|
||
)
|
||
from ..mock_checkers import MockCollisionChecker, MockReachabilityChecker
|
||
from ..models import Constraint, Device, Lab, Placement
|
||
|
||
|
||
def _make_devices():
|
||
return [
|
||
Device(id="a", name="Device A", bbox=(0.5, 0.5)),
|
||
Device(id="b", name="Device B", bbox=(0.5, 0.5)),
|
||
]
|
||
|
||
|
||
def _make_lab():
|
||
return Lab(width=5.0, depth=4.0)
|
||
|
||
|
||
class TestDefaultHardConstraints:
|
||
def test_no_collision_passes(self):
|
||
"""无碰撞的布局应返回 0。"""
|
||
devices = _make_devices()
|
||
placements = [
|
||
Placement("a", 1.0, 1.0, 0.0),
|
||
Placement("b", 3.0, 3.0, 0.0),
|
||
]
|
||
checker = MockCollisionChecker()
|
||
cost = evaluate_default_hard_constraints(devices, placements, _make_lab(), checker)
|
||
assert cost == 0.0
|
||
|
||
def test_collision_returns_graduated_penalty(self):
|
||
"""碰撞布局应返回正的graduated penalty(非inf)。"""
|
||
devices = _make_devices()
|
||
placements = [
|
||
Placement("a", 1.0, 1.0, 0.0),
|
||
Placement("b", 1.2, 1.0, 0.0),
|
||
]
|
||
checker = MockCollisionChecker()
|
||
cost = evaluate_default_hard_constraints(devices, placements, _make_lab(), checker)
|
||
assert cost > 0
|
||
assert not math.isinf(cost)
|
||
|
||
def test_collision_returns_inf_binary_mode(self):
|
||
"""Binary mode: 碰撞布局应返回 inf。"""
|
||
devices = _make_devices()
|
||
placements = [
|
||
Placement("a", 1.0, 1.0, 0.0),
|
||
Placement("b", 1.2, 1.0, 0.0),
|
||
]
|
||
checker = MockCollisionChecker()
|
||
cost = evaluate_default_hard_constraints(
|
||
devices, placements, _make_lab(), checker, graduated=False,
|
||
)
|
||
assert math.isinf(cost)
|
||
|
||
def test_out_of_bounds_returns_graduated_penalty(self):
|
||
"""越界布局应返回正的graduated penalty(非inf)。"""
|
||
devices = _make_devices()
|
||
placements = [
|
||
Placement("a", 0.1, 0.1, 0.0), # 左下角越界
|
||
Placement("b", 3.0, 3.0, 0.0),
|
||
]
|
||
checker = MockCollisionChecker()
|
||
cost = evaluate_default_hard_constraints(devices, placements, _make_lab(), checker)
|
||
assert cost > 0
|
||
assert not math.isinf(cost)
|
||
|
||
def test_out_of_bounds_returns_inf_binary_mode(self):
|
||
"""Binary mode: 越界布局应返回 inf。"""
|
||
devices = _make_devices()
|
||
placements = [
|
||
Placement("a", 0.1, 0.1, 0.0),
|
||
Placement("b", 3.0, 3.0, 0.0),
|
||
]
|
||
checker = MockCollisionChecker()
|
||
cost = evaluate_default_hard_constraints(
|
||
devices, placements, _make_lab(), checker, graduated=False,
|
||
)
|
||
assert math.isinf(cost)
|
||
|
||
def test_worse_collision_higher_cost(self):
|
||
"""Deeper penetration should produce higher cost."""
|
||
devices = _make_devices()
|
||
checker = MockCollisionChecker()
|
||
lab = _make_lab()
|
||
# Small overlap
|
||
cost_small = evaluate_default_hard_constraints(
|
||
devices, [Placement("a", 1.0, 1.0, 0.0), Placement("b", 1.4, 1.0, 0.0)],
|
||
lab, checker,
|
||
)
|
||
# Large overlap
|
||
cost_large = evaluate_default_hard_constraints(
|
||
devices, [Placement("a", 1.0, 1.0, 0.0), Placement("b", 1.1, 1.0, 0.0)],
|
||
lab, checker,
|
||
)
|
||
assert cost_large > cost_small > 0
|
||
|
||
|
||
class TestUserConstraints:
|
||
def test_distance_less_than_satisfied(self):
|
||
"""距离约束满足时 cost=0。"""
|
||
devices = _make_devices()
|
||
placements = [
|
||
Placement("a", 1.0, 1.0, 0.0),
|
||
Placement("b", 1.5, 1.0, 0.0),
|
||
]
|
||
constraints = [
|
||
Constraint(type="hard", rule_name="distance_less_than",
|
||
params={"device_a": "a", "device_b": "b", "distance": 1.0})
|
||
]
|
||
checker = MockCollisionChecker()
|
||
reachability = MockReachabilityChecker()
|
||
cost = evaluate_constraints(
|
||
devices, placements, _make_lab(), constraints, checker, reachability
|
||
)
|
||
assert cost == 0.0
|
||
|
||
def test_distance_less_than_violated_hard(self):
|
||
"""硬距离约束违反:graduated模式返回有限惩罚,binary模式返回inf。"""
|
||
devices = _make_devices()
|
||
placements = [
|
||
Placement("a", 1.0, 1.0, 0.0),
|
||
Placement("b", 4.0, 3.0, 0.0),
|
||
]
|
||
constraints = [
|
||
Constraint(type="hard", rule_name="distance_less_than",
|
||
params={"device_a": "a", "device_b": "b", "distance": 1.0})
|
||
]
|
||
checker = MockCollisionChecker()
|
||
# graduated=True (default): 有限惩罚
|
||
cost = evaluate_constraints(
|
||
devices, placements, _make_lab(), constraints, checker
|
||
)
|
||
assert cost > 0
|
||
assert not math.isinf(cost)
|
||
# graduated=False: binary inf
|
||
cost_binary = evaluate_constraints(
|
||
devices, placements, _make_lab(), constraints, checker,
|
||
graduated=False,
|
||
)
|
||
assert math.isinf(cost_binary)
|
||
|
||
def test_minimize_distance_cost(self):
|
||
"""minimize_distance 约束应返回正比于距离的 cost。"""
|
||
devices = _make_devices()
|
||
placements = [
|
||
Placement("a", 1.0, 1.0, 0.0),
|
||
Placement("b", 3.0, 1.0, 0.0),
|
||
]
|
||
constraints = [
|
||
Constraint(type="soft", rule_name="minimize_distance",
|
||
params={"device_a": "a", "device_b": "b"}, weight=2.0)
|
||
]
|
||
checker = MockCollisionChecker()
|
||
cost = evaluate_constraints(
|
||
devices, placements, _make_lab(), constraints, checker
|
||
)
|
||
# edge-to-edge distance = 2.0 - 0.25 - 0.25 = 1.5, weight = 2.0 → cost = 3.0
|
||
assert abs(cost - 3.0) < 0.01
|
||
|
||
def test_reachability_constraint(self):
|
||
"""可达性约束:目标在臂展内应通过(不返回 inf)。
|
||
|
||
Opening-faces-arm penalty may add a small soft cost when the
|
||
target's opening doesn't face the arm, but it must not cause
|
||
hard failure (inf).
|
||
"""
|
||
devices = [
|
||
Device(id="arm", name="Arm", bbox=(0.2, 0.2), device_type="articulation"),
|
||
Device(id="target", name="Target", bbox=(0.5, 0.5)),
|
||
]
|
||
placements = [
|
||
Placement("arm", 1.0, 1.0, 0.0),
|
||
Placement("target", 1.5, 1.0, 0.0),
|
||
]
|
||
constraints = [
|
||
Constraint(type="hard", rule_name="reachability",
|
||
params={"arm_id": "arm", "target_device_id": "target"})
|
||
]
|
||
checker = MockCollisionChecker()
|
||
reachability = MockReachabilityChecker(arm_reach={"arm": 1.0})
|
||
cost = evaluate_constraints(
|
||
devices, placements, _make_lab(), constraints, checker, reachability
|
||
)
|
||
assert not math.isinf(cost) # reachable → no hard failure
|
||
|
||
def test_reachability_constraint_violated(self):
|
||
"""可达性约束:目标超出臂展 — graduated返回有限惩罚,binary返回inf。"""
|
||
devices = [
|
||
Device(id="arm", name="Arm", bbox=(0.2, 0.2), device_type="articulation"),
|
||
Device(id="target", name="Target", bbox=(0.5, 0.5)),
|
||
]
|
||
placements = [
|
||
Placement("arm", 1.0, 1.0, 0.0),
|
||
Placement("target", 4.0, 3.0, 0.0),
|
||
]
|
||
constraints = [
|
||
Constraint(type="hard", rule_name="reachability",
|
||
params={"arm_id": "arm", "target_device_id": "target"})
|
||
]
|
||
checker = MockCollisionChecker()
|
||
reachability = MockReachabilityChecker(arm_reach={"arm": 1.0})
|
||
# graduated=True (default): 有限惩罚
|
||
cost = evaluate_constraints(
|
||
devices, placements, _make_lab(), constraints, checker, reachability
|
||
)
|
||
assert cost > 0
|
||
assert not math.isinf(cost)
|
||
# graduated=False: binary inf
|
||
cost_binary = evaluate_constraints(
|
||
devices, placements, _make_lab(), constraints, checker, reachability,
|
||
graduated=False,
|
||
)
|
||
assert math.isinf(cost_binary)
|
||
|
||
|
||
def test_distance_less_than_uses_edge_to_edge():
|
||
"""distance_less_than should measure edge-to-edge, not center-to-center.
|
||
|
||
Two devices: centers 3m apart, each 2m wide → edge gap = 1m.
|
||
Constraint: distance_less_than 1.5m (edge-to-edge).
|
||
Old center-to-center: 3m > 1.5m → violation.
|
||
New edge-to-edge: 1m < 1.5m → satisfied.
|
||
"""
|
||
devices = [
|
||
Device(id="a", name="A", bbox=(2.0, 1.0)),
|
||
Device(id="b", name="B", bbox=(2.0, 1.0)),
|
||
]
|
||
placements = [
|
||
Placement(device_id="a", x=1.0, y=1.0, theta=0.0),
|
||
Placement(device_id="b", x=4.0, y=1.0, theta=0.0),
|
||
]
|
||
lab = Lab(width=10, depth=10)
|
||
constraint = Constraint(
|
||
type="soft", rule_name="distance_less_than",
|
||
params={"device_a": "a", "device_b": "b", "distance": 1.5},
|
||
weight=1.0,
|
||
)
|
||
checker = MockCollisionChecker()
|
||
cost = evaluate_constraints(devices, placements, lab, [constraint], checker)
|
||
assert cost == pytest.approx(0.0)
|
||
|
||
|
||
def test_prefer_aligned_zero_at_cardinal():
|
||
"""prefer_aligned cost = 0 when all devices at 0/90/180/270°."""
|
||
devices = [Device(id="a", name="A", bbox=(1.0, 1.0))]
|
||
lab = Lab(width=10, depth=10)
|
||
checker = MockCollisionChecker()
|
||
for angle in [0, math.pi / 2, math.pi, 3 * math.pi / 2]:
|
||
placements = [Placement(device_id="a", x=5, y=5, theta=angle)]
|
||
constraint = Constraint(type="soft", rule_name="prefer_aligned", weight=1.0)
|
||
cost = evaluate_constraints(devices, placements, lab, [constraint], checker)
|
||
assert cost == pytest.approx(0.0, abs=1e-9)
|
||
|
||
|
||
def test_prefer_aligned_max_at_45():
|
||
"""prefer_aligned cost is maximum when device at 45°."""
|
||
devices = [Device(id="a", name="A", bbox=(1.0, 1.0))]
|
||
placements = [Placement(device_id="a", x=5, y=5, theta=math.pi / 4)]
|
||
lab = Lab(width=10, depth=10)
|
||
constraint = Constraint(type="soft", rule_name="prefer_aligned", weight=1.0)
|
||
checker = MockCollisionChecker()
|
||
cost = evaluate_constraints(devices, placements, lab, [constraint], checker)
|
||
# (1 - cos(4 * pi/4)) / 2 = (1 - cos(pi)) / 2 = (1 - (-1)) / 2 = 1.0
|
||
assert cost == pytest.approx(1.0)
|
||
|
||
|
||
def test_prefer_aligned_sums_over_devices():
|
||
"""Cost sums across all devices."""
|
||
devices = [
|
||
Device(id="a", name="A", bbox=(1.0, 1.0)),
|
||
Device(id="b", name="B", bbox=(1.0, 1.0)),
|
||
]
|
||
placements = [
|
||
Placement(device_id="a", x=2, y=2, theta=math.pi / 4), # cost = 1.0
|
||
Placement(device_id="b", x=7, y=7, theta=math.pi / 4), # cost = 1.0
|
||
]
|
||
lab = Lab(width=10, depth=10)
|
||
constraint = Constraint(type="soft", rule_name="prefer_aligned", weight=2.0)
|
||
checker = MockCollisionChecker()
|
||
cost = evaluate_constraints(devices, placements, lab, [constraint], checker)
|
||
# 2 devices × 1.0 × weight 2.0 = 4.0
|
||
assert cost == pytest.approx(4.0)
|
||
|
||
|
||
class TestGraduatedHardConstraints:
|
||
"""graduated 模式下硬约束返回比例惩罚而非 inf。"""
|
||
|
||
def test_hard_reachability_graduated_finite(self):
|
||
"""graduated=True: 硬可达性返回有限惩罚。"""
|
||
devices = [
|
||
Device(id="arm", name="Arm", bbox=(0.2, 0.2), device_type="articulation"),
|
||
Device(id="t", name="Target", bbox=(0.5, 0.5)),
|
||
]
|
||
placements = [
|
||
Placement("arm", 1.0, 1.0, 0.0),
|
||
Placement("t", 4.0, 3.0, 0.0),
|
||
]
|
||
constraints = [
|
||
Constraint(type="hard", rule_name="reachability",
|
||
params={"arm_id": "arm", "target_device_id": "t"}, weight=1.0)
|
||
]
|
||
checker = MockCollisionChecker()
|
||
reach = MockReachabilityChecker(arm_reach={"arm": 1.0})
|
||
cost = evaluate_constraints(
|
||
devices, placements, _make_lab(), constraints, checker, reach,
|
||
graduated=True,
|
||
)
|
||
assert cost > 0
|
||
assert not math.isinf(cost)
|
||
|
||
def test_hard_reachability_binary_inf(self):
|
||
"""graduated=False: 硬可达性返回 inf。"""
|
||
devices = [
|
||
Device(id="arm", name="Arm", bbox=(0.2, 0.2), device_type="articulation"),
|
||
Device(id="t", name="Target", bbox=(0.5, 0.5)),
|
||
]
|
||
placements = [
|
||
Placement("arm", 1.0, 1.0, 0.0),
|
||
Placement("t", 4.0, 3.0, 0.0),
|
||
]
|
||
constraints = [
|
||
Constraint(type="hard", rule_name="reachability",
|
||
params={"arm_id": "arm", "target_device_id": "t"}, weight=1.0)
|
||
]
|
||
checker = MockCollisionChecker()
|
||
reach = MockReachabilityChecker(arm_reach={"arm": 1.0})
|
||
cost = evaluate_constraints(
|
||
devices, placements, _make_lab(), constraints, checker, reach,
|
||
graduated=False,
|
||
)
|
||
assert math.isinf(cost)
|
||
|
||
def test_hard_min_spacing_graduated_sums_all_pairs(self):
|
||
"""graduated模式:min_spacing 对所有违规对求和(不只第一对)。"""
|
||
devices = [
|
||
Device(id="a", name="A", bbox=(0.5, 0.5)),
|
||
Device(id="b", name="B", bbox=(0.5, 0.5)),
|
||
Device(id="c", name="C", bbox=(0.5, 0.5)),
|
||
]
|
||
# 三个设备间距都小于 min_gap=1.0
|
||
placements = [
|
||
Placement("a", 1.0, 2.0, 0.0),
|
||
Placement("b", 1.3, 2.0, 0.0), # OBB 边缘距 a 约 0.3
|
||
Placement("c", 1.6, 2.0, 0.0), # OBB 边缘距 b 约 0.3, 距 a 约 0.6
|
||
]
|
||
constraints = [
|
||
Constraint(type="hard", rule_name="min_spacing",
|
||
params={"min_gap": 1.0}, weight=1.0)
|
||
]
|
||
checker = MockCollisionChecker()
|
||
cost = evaluate_constraints(
|
||
devices, placements, _make_lab(), constraints, checker,
|
||
graduated=True,
|
||
)
|
||
# 应大于 0 且有限(累加多对违规)
|
||
assert cost > 0
|
||
assert not math.isinf(cost)
|
||
|
||
def test_hard_min_spacing_binary_inf(self):
|
||
"""graduated=False: min_spacing 违规返回 inf。"""
|
||
devices = _make_devices()
|
||
placements = [
|
||
Placement("a", 1.0, 2.0, 0.0),
|
||
Placement("b", 1.3, 2.0, 0.0),
|
||
]
|
||
constraints = [
|
||
Constraint(type="hard", rule_name="min_spacing",
|
||
params={"min_gap": 1.0}, weight=1.0)
|
||
]
|
||
checker = MockCollisionChecker()
|
||
cost = evaluate_constraints(
|
||
devices, placements, _make_lab(), constraints, checker,
|
||
graduated=False,
|
||
)
|
||
assert math.isinf(cost)
|
||
|
||
def test_hard_distance_less_than_graduated(self):
|
||
"""graduated模式:distance_less_than 硬约束返回比例惩罚。"""
|
||
devices = _make_devices()
|
||
placements = [
|
||
Placement("a", 1.0, 2.0, 0.0),
|
||
Placement("b", 4.0, 2.0, 0.0),
|
||
]
|
||
constraints = [
|
||
Constraint(type="hard", rule_name="distance_less_than",
|
||
params={"device_a": "a", "device_b": "b", "distance": 0.5},
|
||
weight=2.0)
|
||
]
|
||
checker = MockCollisionChecker()
|
||
cost = evaluate_constraints(
|
||
devices, placements, _make_lab(), constraints, checker,
|
||
graduated=True,
|
||
)
|
||
# HARD_MULTIPLIER(5) × weight(2) × overshoot > 0
|
||
assert cost > 0
|
||
assert not math.isinf(cost)
|
||
|
||
def test_graduated_default_is_true(self):
|
||
"""不传 graduated 参数时默认使用 graduated 模式。"""
|
||
devices = _make_devices()
|
||
placements = [
|
||
Placement("a", 1.0, 2.0, 0.0),
|
||
Placement("b", 4.0, 2.0, 0.0),
|
||
]
|
||
constraints = [
|
||
Constraint(type="hard", rule_name="distance_less_than",
|
||
params={"device_a": "a", "device_b": "b", "distance": 0.5},
|
||
weight=1.0)
|
||
]
|
||
checker = MockCollisionChecker()
|
||
# 不指定 graduated — 默认应为 True → 有限惩罚
|
||
cost = evaluate_constraints(
|
||
devices, placements, _make_lab(), constraints, checker,
|
||
)
|
||
assert not math.isinf(cost)
|