This commit is contained in:
Horldsence
2026-09-11 02:32:31 +08:00
parent 8c18a1f499
commit 4f91f242fe
19 changed files with 971 additions and 45 deletions
+17 -1
View File
@@ -109,10 +109,26 @@ class BeliefDirectionTests(TestCase):
with_margin = BeliefState(CONFIG)
with_margin.update_measure(1, (0.0, 0.0), DIRECTION, 2.01)
self.assertTrue(with_margin.candidate_set(1).contains(source, tol_m=0.5))
no_margin = BeliefState(ProblemConfig())
no_margin_config = replace(ProblemConfig(),
search=replace(ProblemConfig().search,
bearing_rounding_margin_deg=0.0))
no_margin = BeliefState(no_margin_config)
no_margin.update_measure(1, (0.0, 0.0), DIRECTION, 2.01)
self.assertFalse(no_margin.candidate_set(1).contains(source, tol_m=0.5))
def test_default_config_carries_rounding_margin(self):
"""库默认配置必须自带 0.005° 舍入裕量,与 CLI run 默认一致。
在线 API 若按默认配置消费官方读数,"加误差再舍入"规则下
合法真源可能被裁掉;默认口径必须保守。
"""
self.assertEqual(ProblemConfig().search.bearing_rounding_margin_deg, 0.005)
default_belief = BeliefState(ProblemConfig())
source = (1200 * math.cos(math.radians(1.005)),
1200 * math.sin(math.radians(1.005)))
default_belief.update_measure(1, (0.0, 0.0), DIRECTION, 2.01)
self.assertTrue(default_belief.candidate_set(1).contains(source, tol_m=0.5))
def test_near_shrinks_to_five_meters(self):
belief = BeliefState(CONFIG)
belief.update_measure(2, (100.0, 100.0), NEAR, None)
+17
View File
@@ -58,6 +58,23 @@ class GeometryTests(unittest.TestCase):
self.assertEqual(r['status'], 'polygon')
self.assertGreater(r['diameter_m'], 1e9)
def test_near_antiparallel_thin_wedge_keeps_far_vertex(self):
"""近平行细长区域不得丢失远端顶点(审核复现案例)。
测站相距 1cm、示向度 1 与 358.99999999999:两条近平行边界相交出
~5.7e10 m 处的远端顶点。旧实现按绝对阈值 PARALLEL_EPS 跳过该平面
对,漏掉顶点后错误报出 0.143m 直径并给出清除保证。
"""
r = solve([Measurement(0, 0, 1), Measurement(0, 0.01, 358.99999999999)])
self.assertEqual(r['status'], 'polygon')
self.assertGreater(r['diameter_m'], 5.7e10)
self.assertFalse(r['guaranteed_clearable_at_mec_center'],
"细长区域的清除保证必须被拒绝")
# 远端方向上的可行源 (1000, 0) 必须仍落在最小包围圆内
mec = r['minimum_enclosing_circle']
self.assertLessEqual(distance(mec['center'], (1000.0, 0.0)),
mec['radius_m'] + 1e-6)
def test_circle_known_shapes(self):
c, r = smallest_enclosing_circle([(0,0),(2,0),(2,2),(0,2)])
self.assertEqual(c, (1,1))
+32
View File
@@ -112,6 +112,38 @@ class LoopAcceptanceTests(unittest.TestCase):
self.assertEqual(sorted(simulator.cleared), truth)
class AbortFinalizationTests(unittest.TestCase):
"""断连超过重试上限:仍须 /exit、产出未完成 summary,不得静默崩溃。"""
def test_transport_failure_aborts_with_summary_and_exit(self):
"""首个检测动作三次断连:run() 正常返回,/exit 成功,stop_reason 留档。"""
summary, _, _ = _run_scene(
random_omni_scene(seed=5, count=2),
fault_script=[None, "drop_before", "drop_before", "drop_before"])
self.assertFalse(summary["completed"])
self.assertEqual(summary["stop_reason"], "transport_failure")
self.assertEqual(summary["exit_reason"], "user_exit",
"接口仍可用时必须尝试退出")
self.assertEqual(summary["abort_detail"]["type"], "TransportError")
self.assertLessEqual(summary["measures"] + summary["clears"],
summary["total_requests"])
self.assertEqual(len(summary["channel_status"]), 20,
"未完成状态须有逐频道快照")
def test_exit_failure_still_produces_summary(self):
"""检测与 /exit 均三次断连:产出 exit_unreachable 的未完成 summary。"""
summary, _, _ = _run_scene(
random_omni_scene(seed=7, count=2),
fault_script=[None,
"drop_before", "drop_before", "drop_before",
"drop_before", "drop_before", "drop_before"])
self.assertFalse(summary["completed"])
self.assertEqual(summary["stop_reason"], "transport_failure")
self.assertEqual(summary["exit_reason"], "exit_unreachable")
self.assertEqual(summary["exit_error"]["type"], "TransportError")
self.assertIn("channel_status", summary)
class CoverageLedgerIntegrationTests(unittest.TestCase):
def test_grid_matches_plan_derivation(self):
points = coverage_points(CONFIG)
+37
View File
@@ -81,6 +81,43 @@ class FallbackClearCellsTests(unittest.TestCase):
self.assertLessEqual(len(cells), bound)
class ProbeLadderGeometryTests(unittest.TestCase):
"""对跖复核探点几何:圆心对称点裁进保证接收盘,不得塌缩到圆心附近。"""
def _probe_after_block(self, region, blocked_point):
simulator = LocalSimulator([], robot_id="T001", config=CONFIG)
client = SimulatorClient("T001", config=CONFIG,
transport=LocalSimTransport(simulator))
client.enter()
loop = DirectionalExtension(client, CONFIG)
loop.blocked_probes[7] = [blocked_point]
return loop._next_probe(region, 7)
def test_antipode_reflects_blocked_probe_through_center(self):
"""圆心为原点、原探点 (200,0):对跖复核应返回 (-200,0)。"""
region = CandidateRegion.disk((0.0, 0.0), 100.0, 192) # safe≈900>=200
probe = self._probe_after_block(region, (200.0, 0.0))
self.assertAlmostEqual(probe[0], -200.0, places=6)
self.assertAlmostEqual(probe[1], 0.0, places=6)
def test_antipode_clipped_into_safe_disk(self):
"""原探点距离超过安全盘半径时:落在距圆心 safe 处的对跖方向上。"""
region = CandidateRegion.disk((0.0, 0.0), 900.0, 192)
safe = safe_disk_radius(region, CONFIG)
self.assertLess(safe, 200.0)
probe = self._probe_after_block(region, (200.0, 0.0))
self.assertAlmostEqual(math.hypot(probe[0], probe[1]), safe, places=6)
self.assertLess(probe[0], 0.0, "应位于原探点的对跖方向")
def test_degenerate_center_block_walks_full_safe_radius(self):
"""被遮挡探点恰在圆心:沿 +x 走满安全盘半径,不再钳到 1m。"""
region = CandidateRegion.disk((0.0, 0.0), 100.0, 192)
probe = self._probe_after_block(region, (0.0, 0.0))
safe = safe_disk_radius(region, CONFIG)
self.assertAlmostEqual(probe[0], safe, places=6)
self.assertAlmostEqual(probe[1], 0.0, places=6)
class MixedLoopAcceptanceTests(unittest.TestCase):
def test_mixed_ten_sources_all_cleared(self):
sources = mixed_scene(seed=5, count=10, directional=3)
+20 -1
View File
@@ -9,7 +9,8 @@ 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,
from mcm26b.planner import (_worst_posterior_radius, bearing_deg,
clear_certificate, select_measurement_point,
select_second_point)
from mcm26b.protocol.client import SimulatorClient
from mcm26b.sim import LocalSimTransport, LocalSimulator
@@ -71,6 +72,24 @@ class SelectionGuaranteeTests(TestCase):
self.assertTrue(probe.polygon and probe.contains(g, tol_m=0.05),
f"后验剪掉了生成读数的边界点 {g}")
def test_posterior_scores_near_branch(self):
"""后验评分必须包含 near 分支,否则低报最坏后验半径。
候选集为线段 [-4,4]×{0}、测点为原点时,任意合法读数必为 near
(源距原点 <=4 < 5m),后验仍为整段线段(半径 4m);只枚举
direction 端点会错误报出 2m。
"""
region = CandidateRegion([(-4.0, 0.0), (4.0, 0.0)])
half = 1.005
witnesses = region.witnesses(60.0)
self.assertEqual(witnesses, [(-4.0, 0.0), (4.0, 0.0)])
worst = _worst_posterior_radius(region, (0.0, 0.0), witnesses, half,
CONFIG.field.near_radius_m,
CONFIG.search.polygon_sides)
self.assertGreaterEqual(worst, 4.0 - 1e-6,
"near 分支后验半径应逼近候选集半径 4m")
self.assertLessEqual(worst, 4.0 + region.slack_m + 0.1)
def test_move_cost_recorded_and_reason_explains(self):
region = _k1_region()
plan = select_measurement_point(region, (500.0, 500.0), CONFIG)
+26
View File
@@ -227,6 +227,32 @@ class LocalSimObservationTests(unittest.TestCase):
# 误差在 ±1° 内均匀分布,返回值再保留两位小数。
self.assertLessEqual(abs(outcome.svd_deg - true_bearing), 1.005)
def test_same_position_error_is_fixed(self):
"""同一(源,位置)重复测量读数不变 [题面/官方演练实测]。"""
source = Source(channel=1, x=1000, y=300, r_eff_m=1500)
simulator = LocalSimulator([source], robot_id="T001")
client = SimulatorClient("T001", transport=LocalSimTransport(simulator))
client.enter()
first = client.measure((0, 0), 1)
second = client.measure((0, 0), 1) # 新 request_id,同位置
self.assertEqual(first.result, "direction")
self.assertEqual(first.svd_deg, second.svd_deg,
"同位置重复测量的示向度必须相同")
def test_error_field_deterministic_per_seed(self):
"""同种子误差场逐点一致;不同种子得到不同误差场(逐点白噪声)。"""
source = Source(channel=1, x=1000, y=0, r_eff_m=1500)
readings = {}
for label, seed in (("a", 1), ("b", 1), ("c", 2)):
simulator = LocalSimulator([source], error_seed=seed, robot_id="T001")
client = SimulatorClient("T001", transport=LocalSimTransport(simulator))
client.enter()
readings[label] = [client.measure((float(x), 0.0), 1).svd_deg
for x in (0, 100, 200, 300, 400)]
self.assertEqual(readings["a"], readings["b"])
self.assertNotEqual(readings["a"], readings["c"],
"不同种子应在至少一个测点产生不同读数")
class SessionLogTests(unittest.TestCase):
def test_log_records_attempts_and_accepted(self):