From b04dc8dd4ad465c13a3ff3232039e879405e3adc Mon Sep 17 00:00:00 2001 From: yexiaozhou Date: Thu, 2 Apr 2026 13:48:34 +0800 Subject: [PATCH] feat(layout_optimizer): default cardinal snap and alignment to off align_weight defaults to 0 (was DEFAULT_WEIGHT_ANGLE=60). snap_theta_safe is opt-in via snap_cardinal=True (was always-on). Both remain available when explicitly requested. Co-Authored-By: Claude Opus 4.6 --- unilabos/layout_optimizer/server.py | 10 ++-- .../tests/test_bugfixes_v2.py | 55 +++++++++++++++++++ .../tests/test_e2e_pcr_pipeline.py | 2 + 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/unilabos/layout_optimizer/server.py b/unilabos/layout_optimizer/server.py index f24f68a6..239e884d 100644 --- a/unilabos/layout_optimizer/server.py +++ b/unilabos/layout_optimizer/server.py @@ -368,6 +368,7 @@ class OptimizeRequest(BaseModel): workflow_edges: list[list[str]] = [] maxiter: int = 200 seed: int | None = None + snap_cardinal: bool = False class PositionXYZ(BaseModel): @@ -459,8 +460,8 @@ async def run_optimize(request: OptimizeRequest): params={"mode": orientation_mode}, weight=request.seeder_overrides.get("orientation_weight", DEFAULT_WEIGHT_ANGLE), )) - # prefer_aligned: penalize non-cardinal angles - align_weight = request.seeder_overrides.get("align_weight", DEFAULT_WEIGHT_ANGLE) + # prefer_aligned: penalize non-cardinal angles(默认关闭,用户可通过 align_cardinal intent 或 seeder_overrides 开启) + align_weight = request.seeder_overrides.get("align_weight", 0) if align_weight > 0: constraints.append(Constraint( type="soft", @@ -486,8 +487,9 @@ async def run_optimize(request: OptimizeRequest): else: result_placements = seed_placements - # 5. θ snap post-processing(碰撞安全:snap 后验证,失败则回退) - result_placements = snap_theta_safe(result_placements, devices, lab, checker) + # 5. θ snap post-processing(opt-in,默认关闭) + if request.snap_cardinal: + result_placements = snap_theta_safe(result_placements, devices, lab, checker) # 6. Evaluate final cost (binary mode for pass/fail reporting) final_cost = evaluate_default_hard_constraints( diff --git a/unilabos/layout_optimizer/tests/test_bugfixes_v2.py b/unilabos/layout_optimizer/tests/test_bugfixes_v2.py index 8e63fa31..616a52b6 100644 --- a/unilabos/layout_optimizer/tests/test_bugfixes_v2.py +++ b/unilabos/layout_optimizer/tests/test_bugfixes_v2.py @@ -376,3 +376,58 @@ class TestScenarios: assert not _has_collision(devices, result) for i, p in enumerate(result): assert _facing_dot(p, devices[i], lab) > 0 + + +# ── V2 Stage 1: 默认关闭 cardinal snap/alignment ──────── + +class TestV2Stage1Bugfixes: + """align_weight 默认为 0,snap_cardinal 默认关闭。""" + + def test_default_align_weight_is_zero(self): + """Default request (no seeder_overrides) should NOT inject prefer_aligned.""" + from fastapi.testclient import TestClient + from ..server import app + + client = TestClient(app) + resp = client.post("/optimize", json={ + "devices": [{"id": "opentrons_liquid_handler", "uuid": "u1"}], + "lab": {"width": 3, "depth": 3}, + "seeder": "compact_outward", + "run_de": True, + "maxiter": 50, + "seed": 42, + }) + assert resp.status_code == 200 + + def test_snap_cardinal_off_by_default(self): + """Default request should NOT snap theta to cardinal.""" + from fastapi.testclient import TestClient + from ..server import app + + client = TestClient(app) + resp = client.post("/optimize", json={ + "devices": [{"id": "opentrons_liquid_handler", "uuid": "u1"}], + "lab": {"width": 3, "depth": 3}, + "seeder": "compact_outward", + "run_de": True, + "maxiter": 10, + "seed": 42, + }) + assert resp.status_code == 200 + + def test_snap_cardinal_opt_in(self): + """snap_cardinal=True should be accepted and snap angles.""" + from fastapi.testclient import TestClient + from ..server import app + + client = TestClient(app) + resp = client.post("/optimize", json={ + "devices": [{"id": "opentrons_liquid_handler", "uuid": "u1"}], + "lab": {"width": 3, "depth": 3}, + "seeder": "compact_outward", + "snap_cardinal": True, + "run_de": True, + "maxiter": 10, + "seed": 42, + }) + assert resp.status_code == 200 diff --git a/unilabos/layout_optimizer/tests/test_e2e_pcr_pipeline.py b/unilabos/layout_optimizer/tests/test_e2e_pcr_pipeline.py index 53557722..bd5e20e9 100644 --- a/unilabos/layout_optimizer/tests/test_e2e_pcr_pipeline.py +++ b/unilabos/layout_optimizer/tests/test_e2e_pcr_pipeline.py @@ -199,6 +199,8 @@ class TestStage3VerifyPlacements: "run_de": True, "maxiter": 100, "seed": 42, + "snap_cardinal": True, + "seeder_overrides": {"align_weight": 60}, }) data = optimize_resp.json() assert data["success"] is True