2
This commit is contained in:
+17
-1
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user