Files
mcm-26-b/tests/test_planner.py
T
2026-09-11 01:19:38 +08:00

124 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()