This commit is contained in:
Horldsence
2026-09-11 01:19:38 +08:00
parent 83d2e24448
commit a6308fd40c
12 changed files with 1631 additions and 87 deletions
+123
View File
@@ -0,0 +1,123 @@
"""P3 选点器验收:保证接收、后验收敛、清除证书阈值、与解析证书衔接。"""
import math
import unittest
from dataclasses import replace
from unittest import TestCase
from mcm26b.belief import CandidateRegion
from mcm26b.config import ProblemConfig
from mcm26b.geometry.localization import Measurement
from mcm26b.geometry.planning import first_bearing_candidate_disk
from mcm26b.planner import (bearing_deg, clear_certificate, select_measurement_point,
select_second_point)
from mcm26b.protocol.client import SimulatorClient
from mcm26b.sim import LocalSimTransport, LocalSimulator
CONFIG = replace(ProblemConfig(),
search=replace(ProblemConfig().search,
bearing_rounding_margin_deg=0.005))
FIRST_STATION = (0.0, 0.0)
FIRST_BEARING = 30.0
def _k1_region() -> CandidateRegion:
region = CandidateRegion.initial(FIRST_STATION, CONFIG)
region.clip_wedge(FIRST_STATION, FIRST_BEARING,
CONFIG.field.bearing_error_half_deg
+ CONFIG.search.bearing_rounding_margin_deg)
return region
class SelectionGuaranteeTests(TestCase):
def test_selected_point_receives_every_sector_candidate(self):
"""安全盘内选点对解析角域 K1 内任意候选源的距离 <= 1000m。
直接对截断角域 K1 = Ω ∩ W(s1, θ±δ) ∩ B(s1,1500) 稠密采样验证,
不依赖外包多边形本身。
"""
region = _k1_region()
plan = select_measurement_point(region, FIRST_STATION, CONFIG)
half = 1.005
for step_phi in range(41):
phi = math.radians(FIRST_BEARING - half + 2 * half * step_phi / 40)
for step_t in range(60):
t = 5.0 + (1500.0 - 5.0) * step_t / 59
g = (t * math.cos(phi), t * math.sin(phi))
self.assertLessEqual(
math.dist(plan.position, g), 1000.0 + 1e-6,
f"候选源 {g} 距选点超过最小接收半径,将浪费一次 no_signal")
def test_worst_posterior_shrinks_region(self):
region = _k1_region()
plan = select_measurement_point(region, FIRST_STATION, CONFIG)
self.assertLess(plan.worst_posterior_radius_m, region.mec()[1],
"最坏后验半径必须优于当前候选集,否则选点无进展")
self.assertGreater(plan.worst_posterior_radius_m, 0.0)
def test_posterior_probe_retains_witness(self):
"""后验探针(读数取区间端点)仍包含生成它的边界点:保守性抽查。"""
region = _k1_region()
plan = select_measurement_point(region, FIRST_STATION, CONFIG)
half = CONFIG.field.bearing_error_half_deg \
+ CONFIG.search.bearing_rounding_margin_deg
witnesses = region.witnesses(300.0)
self.assertGreater(len(witnesses), 4)
for g in witnesses:
phi = bearing_deg(plan.position, g)
for reading in (phi - half, phi + half):
probe = CandidateRegion(list(region.polygon), region.slack_m)
probe.clip_wedge(plan.position, reading, half)
self.assertTrue(probe.polygon and probe.contains(g, tol_m=0.05),
f"后验剪掉了生成读数的边界点 {g}")
def test_move_cost_recorded_and_reason_explains(self):
region = _k1_region()
plan = select_measurement_point(region, (500.0, 500.0), CONFIG)
expected = math.dist((500.0, 500.0), plan.position) / CONFIG.cost.speed_mps
self.assertAlmostEqual(plan.move_cost_s, expected, places=6)
self.assertIn("安全盘", plan.reason)
class ClearCertificateTests(TestCase):
def test_near_region_is_certified(self):
region = CandidateRegion.disk((500.0, 500.0), 5.0, 192)
cert = clear_certificate(region, 7, CONFIG)
self.assertTrue(cert.certified)
self.assertLess(cert.limit_m, CONFIG.field.clear_radius_m)
self.assertGreater(cert.limit_m, 19.0)
def test_first_bearing_region_is_not_certified(self):
cert = clear_certificate(_k1_region(), 7, CONFIG)
self.assertFalse(cert.certified)
self.assertGreater(cert.radius_m, cert.limit_m)
class SelectSecondPointTests(TestCase):
def test_plan_lies_in_analytic_safe_disk_or_is_equivalent(self):
"""P3 入口输出与 geometry 解析证书一致(或提供同等保证)。"""
simulator = LocalSimulator([], robot_id="T001")
client = SimulatorClient("T001", transport=LocalSimTransport(simulator))
client.enter()
plan = select_second_point(client, FIRST_STATION, FIRST_BEARING, CONFIG)
# 逐点保证:对解析角域采样,接收距离全部成立(测试见上);此处只
# 核对输出位置确实落在解析证书安全盘的合理邻域内(两者中心口径不同,
# 允许差一个外包裕量)。
cert = first_bearing_candidate_disk(
Measurement(FIRST_STATION[0], FIRST_STATION[1], FIRST_BEARING,
1.0 + CONFIG.search.bearing_rounding_margin_deg))
disk = cert["sufficient_candidate_disk"]
self.assertLessEqual(math.dist(plan.position, tuple(disk["center"])),
disk["radius_m"] + 10.0)
def test_exploration_fallback_when_rho_over_1000(self):
"""ρ>1000 的一般候选集退化为探索 MEC 圆心,并如实标注可能 no_signal。"""
wide = CandidateRegion.disk((0.0, 0.0), 1200.0, 192)
plan = select_measurement_point(wide, (0.0, 0.0), CONFIG)
self.assertAlmostEqual(plan.position[0], 0.0, places=6)
self.assertAlmostEqual(plan.position[1], 0.0, places=6)
self.assertIn("探索", plan.reason)
if __name__ == "__main__":
unittest.main()