feat(layout_optimizer): DE optimizer V2 — custom loop, graduated hard constraints, broad phase

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>
This commit is contained in:
yexiaozhou
2026-04-01 00:32:34 +08:00
parent 64eeed56a1
commit 9ef24b7768
12 changed files with 1072 additions and 83 deletions

View File

@@ -41,6 +41,7 @@ def _handle_reachable_by(intent: Intent, result: InterpretResult) -> None:
type="hard",
rule_name="reachability",
params={"arm_id": arm, "target_device_id": target},
priority="critical",
)
result.constraints.append(c)
generated.append({"type": c.type, "rule_name": c.rule_name, "params": c.params, "weight": c.weight})
@@ -64,6 +65,8 @@ def _handle_close_together(intent: Intent, result: InterpretResult) -> None:
return
weight = _PRIORITY_WEIGHTS.get(priority, _DEFAULT_WEIGHT)
# 映射 intent priority 到 constraint priority 等级
constraint_priority = "high" if priority == "high" else "normal"
generated: list[dict] = []
for dev_a, dev_b in itertools.combinations(devices, 2):
c = Constraint(
@@ -71,6 +74,7 @@ def _handle_close_together(intent: Intent, result: InterpretResult) -> None:
rule_name="minimize_distance",
params={"device_a": dev_a, "device_b": dev_b},
weight=weight,
priority=constraint_priority,
)
result.constraints.append(c)
generated.append({"type": c.type, "rule_name": c.rule_name, "params": c.params, "weight": c.weight})
@@ -94,6 +98,8 @@ def _handle_far_apart(intent: Intent, result: InterpretResult) -> None:
return
weight = _PRIORITY_WEIGHTS.get(priority, _DEFAULT_WEIGHT)
# 映射 intent priority 到 constraint priority 等级
constraint_priority = "high" if priority == "high" else "normal"
generated: list[dict] = []
for dev_a, dev_b in itertools.combinations(devices, 2):
c = Constraint(
@@ -101,6 +107,7 @@ def _handle_far_apart(intent: Intent, result: InterpretResult) -> None:
rule_name="maximize_distance",
params={"device_a": dev_a, "device_b": dev_b},
weight=weight,
priority=constraint_priority,
)
result.constraints.append(c)
generated.append({"type": c.type, "rule_name": c.rule_name, "params": c.params, "weight": c.weight})
@@ -131,6 +138,7 @@ def _handle_max_distance(intent: Intent, result: InterpretResult) -> None:
type="hard",
rule_name="distance_less_than",
params={"device_a": device_a, "device_b": device_b, "distance": distance},
priority="normal",
)
result.constraints.append(c)
@@ -160,6 +168,7 @@ def _handle_min_distance(intent: Intent, result: InterpretResult) -> None:
type="hard",
rule_name="distance_greater_than",
params={"device_a": device_a, "device_b": device_b, "distance": distance},
priority="normal",
)
result.constraints.append(c)
@@ -180,6 +189,7 @@ def _handle_min_spacing(intent: Intent, result: InterpretResult) -> None:
type="hard",
rule_name="min_spacing",
params={"min_gap": min_gap},
priority="high",
)
result.constraints.append(c)
@@ -198,6 +208,7 @@ def _handle_face_outward(intent: Intent, result: InterpretResult) -> None:
type="soft",
rule_name="prefer_orientation_mode",
params={"mode": "outward"},
priority="low",
)
result.constraints.append(c)
@@ -216,6 +227,7 @@ def _handle_face_inward(intent: Intent, result: InterpretResult) -> None:
type="soft",
rule_name="prefer_orientation_mode",
params={"mode": "inward"},
priority="low",
)
result.constraints.append(c)
@@ -234,6 +246,7 @@ def _handle_align_cardinal(intent: Intent, result: InterpretResult) -> None:
type="soft",
rule_name="prefer_aligned",
params={},
priority="low",
)
result.constraints.append(c)
@@ -246,6 +259,39 @@ def _handle_align_cardinal(intent: Intent, result: InterpretResult) -> None:
})
def _handle_keep_adjacent(intent: Intent, result: InterpretResult) -> None:
"""keep_adjacent两个设备保持相邻同 close_together 逻辑,支持 priority 映射)。"""
devices: list[str] = intent.params.get("devices", [])
priority: str = intent.params.get("priority", "medium")
if len(devices) < 2:
result.errors.append(f"keep_adjacent: 参数 'devices' 至少需要 2 个设备,当前 {len(devices)}")
return
weight = _PRIORITY_WEIGHTS.get(priority, _DEFAULT_WEIGHT)
# 映射 intent priority 到 constraint priority 等级
constraint_priority = "high" if priority == "high" else "normal"
generated: list[dict] = []
for dev_a, dev_b in itertools.combinations(devices, 2):
c = Constraint(
type="soft",
rule_name="minimize_distance",
params={"device_a": dev_a, "device_b": dev_b},
weight=weight,
priority=constraint_priority,
)
result.constraints.append(c)
generated.append({"type": c.type, "rule_name": c.rule_name, "params": c.params, "weight": c.weight})
result.translations.append({
"source_intent": intent.intent,
"source_description": intent.description,
"source_params": intent.params,
"generated_constraints": generated,
"explanation": f"设备组 {devices} 应保持相邻(优先级: {priority}",
})
def _handle_workflow_hint(intent: Intent, result: InterpretResult) -> None:
"""workflow_hint工作流顺序暗示相邻步骤设备靠近。"""
workflow: str = intent.params.get("workflow", "")
@@ -263,6 +309,7 @@ def _handle_workflow_hint(intent: Intent, result: InterpretResult) -> None:
type="soft",
rule_name="minimize_distance",
params={"device_a": dev_a, "device_b": dev_b},
priority="normal",
)
result.constraints.append(c)
generated.append({"type": c.type, "rule_name": c.rule_name, "params": c.params, "weight": c.weight})