yes fuking shit
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"""覆盖计划(CoveragePlan)与运行期校核验收(方案 §P0-2)。
|
||||
|
||||
要点:
|
||||
- 计划是"可离线搜索、可在运行期校验"的一等对象,运行期必须重新校核;
|
||||
- 校核必须能**拒绝**非法点集(删点即被检出),否则校核是装饰;
|
||||
- 两类覆盖语义不得混用:第三问 full_disk、第四问 forward_half_disk;
|
||||
- 第四问最小命中集必须在同一采样空间上与原 49 点表等价覆盖。
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from mcm26b.config import ProblemConfig
|
||||
from mcm26b.coverage import coverage_points
|
||||
from mcm26b.guarantee import (plan_instance, plan_names, seven_plan,
|
||||
square_49_plan, verify_full_disk,
|
||||
verify_forward_half_disk, verify_plan)
|
||||
|
||||
|
||||
class CoveragePlanTests(unittest.TestCase):
|
||||
def test_q4_mincover_plan_is_certified(self):
|
||||
self.assertIn("q4_mincover_v1", plan_names())
|
||||
plan = plan_instance("q4_mincover_v1")
|
||||
self.assertEqual(plan.problem, 4)
|
||||
self.assertEqual(plan.coverage, "forward_half_disk")
|
||||
certificate = verify_plan(plan)
|
||||
self.assertEqual(certificate.uncovered, 0, certificate.as_dict())
|
||||
self.assertTrue(certificate.valid)
|
||||
# 最小命中集必须显著小于旧的充分条件表
|
||||
self.assertLess(len(plan.points), len(square_49_plan().points))
|
||||
self.assertLess(certificate.nn_tour_m, 25_000.0)
|
||||
|
||||
def test_hardened_plan_passes_fine_grid(self):
|
||||
"""S2:现役计划必须在 15 m/72 档(325.6 万个 (p,h))零缺口。
|
||||
|
||||
75 m/24 档的"零缺口"曾是采样过拟合(v1 在 25 m/48 档有 125 个缺口)。
|
||||
"""
|
||||
self.assertIn("q4_mincover_v2", plan_names())
|
||||
plan = plan_instance("q4_mincover_v2")
|
||||
certificate = plan.certificate
|
||||
self.assertIsNotNone(certificate)
|
||||
self.assertEqual(certificate.target_step_m, 15.0)
|
||||
self.assertEqual(certificate.headings, 72)
|
||||
verified = verify_plan(plan)
|
||||
self.assertEqual(verified.uncovered, 0, verified.as_dict())
|
||||
self.assertLess(verified.worst_hit_distance_m, 1000.0)
|
||||
self.assertTrue(verified.valid)
|
||||
|
||||
def test_legacy_overfit_plan_fails_fine_grid(self):
|
||||
"""负例:旧的 31 点表在细网格上有缺口,硬化不是装饰。"""
|
||||
legacy = plan_instance("q4_mincover_v1", verify=False)
|
||||
certificate = verify_forward_half_disk(list(legacy.points), 25, 48)
|
||||
self.assertGreater(certificate.uncovered, 0)
|
||||
|
||||
def test_square49_and_mincover_cover_the_same_space(self):
|
||||
for name in ("square49", "q4_mincover_v1"):
|
||||
certificate = verify_forward_half_disk(plan_instance(name).points)
|
||||
self.assertEqual(certificate.uncovered, 0, name)
|
||||
|
||||
def test_verifier_rejects_a_broken_plan(self):
|
||||
"""删掉关键点必须被检出——否则运行期校核没有意义。"""
|
||||
points = list(plan_instance("q4_mincover_v1").points)
|
||||
broken = points[:-1]
|
||||
certificate = verify_forward_half_disk(broken)
|
||||
self.assertGreater(certificate.uncovered, 0)
|
||||
self.assertFalse(certificate.valid)
|
||||
|
||||
def test_hardened_plan_rejects_a_removed_point(self):
|
||||
"""硬化计划同样必须能拒绝删点(在证书分辨率上复核)。"""
|
||||
plan = plan_instance("q4_mincover_v2")
|
||||
points = list(plan.points)
|
||||
certificate = verify_forward_half_disk(
|
||||
points[:-1], plan.certificate.target_step_m,
|
||||
plan.certificate.headings)
|
||||
self.assertGreater(certificate.uncovered, 0)
|
||||
|
||||
def test_seven_plan_full_disk_semantics(self):
|
||||
plan = seven_plan(1200.0)
|
||||
certificate = verify_full_disk(plan.points)
|
||||
self.assertEqual(certificate.kind, "full_disk")
|
||||
self.assertEqual(certificate.uncovered, 0)
|
||||
|
||||
def test_seven_plan_too_small_ring_is_rejected(self):
|
||||
"""环半径过小则 Ω 边缘覆盖不全,full_disk 校核必须报缺口。"""
|
||||
certificate = verify_full_disk(seven_plan(800.0).points)
|
||||
self.assertGreater(certificate.uncovered, 0)
|
||||
|
||||
def test_square49_order_matches_legacy_ledger(self):
|
||||
"""历史基线 grid49_immediate 必须逐位可复现(点集与顺序都一致)。"""
|
||||
self.assertEqual(list(square_49_plan().points),
|
||||
list(coverage_points(ProblemConfig())))
|
||||
|
||||
def test_coverage_semantics_are_not_interchangeable(self):
|
||||
"""第三问七站点集不满足第四问"前向半盘命中"语义(朝向遮挡)。"""
|
||||
certificate = verify_forward_half_disk(seven_plan(1200.0).points)
|
||||
self.assertGreater(certificate.uncovered, 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,124 @@
|
||||
"""第四问 (位置 × 朝向) 联合信念验收(docs/第三四问图论与动态规划优化方案.md §3.1)。
|
||||
|
||||
要点:
|
||||
- 真源 (位置, 朝向) 在与真值一致的观测序列后**永不**被删(保守性二维推广);
|
||||
- 期望后验 MEC 半径是合法的选点值函数:远处被朝向遮挡的 no_signal 不删
|
||||
**空间**候选,因此不应被误当成"已经收缩";
|
||||
- numpy 向量路径与纯 Python 回退路径在同一输入下结果一致。
|
||||
"""
|
||||
|
||||
import math
|
||||
import unittest
|
||||
from dataclasses import replace
|
||||
from random import Random
|
||||
|
||||
from mcm26b.belief.joint import JointBelief, _np
|
||||
from mcm26b.config import ProblemConfig
|
||||
from mcm26b.protocol.rules import DIRECTION, NEAR, NO_SIGNAL
|
||||
|
||||
CONFIG = replace(ProblemConfig(),
|
||||
search=replace(ProblemConfig().search,
|
||||
bearing_rounding_margin_deg=0.005))
|
||||
HALF_ANGLE = CONFIG.field.directional_half_angle_deg
|
||||
|
||||
|
||||
def _visible(source, heading, reader, limit_m):
|
||||
if math.dist(reader, source) > limit_m:
|
||||
return False
|
||||
bearing = math.degrees(math.atan2(reader[1] - source[1],
|
||||
reader[0] - source[0]))
|
||||
return abs((bearing - heading + 180.0) % 360.0 - 180.0) <= HALF_ANGLE - 5.0
|
||||
|
||||
|
||||
class JointBeliefRetentionTests(unittest.TestCase):
|
||||
def test_true_source_survives_consistent_observations(self):
|
||||
"""与真值一致的观测序列后,真源所在格仍必须存在。"""
|
||||
rng = Random(11)
|
||||
checked = 0
|
||||
while checked < 10:
|
||||
source = (rng.uniform(-1700, 1700), rng.uniform(-1700, 1700))
|
||||
if math.hypot(*source) > 1700:
|
||||
continue
|
||||
heading = rng.uniform(0.0, 360.0)
|
||||
detection = (rng.uniform(-2100, 2100), rng.uniform(-2100, 2100))
|
||||
if not _visible(source, heading, detection,
|
||||
CONFIG.field.r_eff_min_m):
|
||||
continue
|
||||
checked += 1
|
||||
joint = JointBelief(CONFIG)
|
||||
bearing = math.degrees(math.atan2(source[1] - detection[1],
|
||||
source[0] - detection[0])) % 360.0
|
||||
joint.apply(detection, DIRECTION,
|
||||
round(bearing + rng.uniform(-0.9, 0.9), 2) % 360.0)
|
||||
for _ in range(5):
|
||||
probe = (rng.uniform(-1800, 1800), rng.uniform(-1800, 1800))
|
||||
if _visible(source, heading, probe, CONFIG.field.r_eff_min_m):
|
||||
pb = math.degrees(math.atan2(source[1] - probe[1],
|
||||
source[0] - probe[0])) % 360.0
|
||||
joint.apply(probe, DIRECTION,
|
||||
round(pb + rng.uniform(-0.9, 0.9), 2) % 360.0)
|
||||
else:
|
||||
joint.apply(probe, NO_SIGNAL)
|
||||
self.assertFalse(joint.is_absent(), "真源可行集被观测清空")
|
||||
assert_source = (round(source[0], 6), round(source[1], 6))
|
||||
nearest = min((math.dist(joint.position.cell_center(index), assert_source)
|
||||
for index in joint.position.alive),
|
||||
default=float("inf"))
|
||||
self.assertLessEqual(nearest, joint.position.margin_m + 1e-6,
|
||||
"真源所在格被联合信念删除")
|
||||
|
||||
def test_repeated_directions_shrink_but_never_grow(self):
|
||||
"""方向读数只会收缩联合集;贴测点的格按保守设计保留。"""
|
||||
joint = JointBelief(CONFIG)
|
||||
joint.apply((0.0, 0.0), DIRECTION, 0.0)
|
||||
first = len(joint)
|
||||
joint.apply((0.0, 0.0), DIRECTION, 180.0)
|
||||
self.assertLessEqual(len(joint), first)
|
||||
self.assertGreater(len(joint), 0)
|
||||
|
||||
def test_near_keeps_only_a_small_disk(self):
|
||||
joint = JointBelief(CONFIG)
|
||||
joint.apply((100.0, 100.0), NEAR)
|
||||
center, radius = joint.position.mec()
|
||||
# 观测更新内缩 Voronoi 裕量,故上界为 near + margin
|
||||
self.assertLessEqual(radius, CONFIG.field.near_radius_m
|
||||
+ joint.position.margin_m + 1.0)
|
||||
self.assertLess(math.dist(center, (100.0, 100.0)), 15.0)
|
||||
|
||||
|
||||
class ExpectedPosteriorRadiusTests(unittest.TestCase):
|
||||
def _joint(self):
|
||||
joint = JointBelief(CONFIG)
|
||||
joint.apply((0.0, 0.0), DIRECTION, 0.0)
|
||||
return joint
|
||||
|
||||
def test_value_is_finite_and_positive(self):
|
||||
joint = self._joint()
|
||||
for probe in ((800.0, 0.0), (0.0, 800.0), (-500.0, -500.0)):
|
||||
value = joint.expected_posterior_radius(probe)
|
||||
self.assertTrue(math.isfinite(value))
|
||||
self.assertGreater(value, 0.0)
|
||||
|
||||
def test_blocked_reading_keeps_spatial_radius(self):
|
||||
"""no_signal 不删空间候选:期望半径不应被误当作已收缩。"""
|
||||
joint = self._joint()
|
||||
before = joint.position.mec()[1]
|
||||
joint.apply((1500.0, 0.0), NO_SIGNAL)
|
||||
self.assertAlmostEqual(joint.position.mec()[1], before, places=6)
|
||||
self.assertGreater(joint.expected_posterior_radius((1500.0, 0.0)),
|
||||
0.5 * before)
|
||||
|
||||
def test_vector_and_python_paths_agree(self):
|
||||
if _np is None:
|
||||
self.skipTest("环境无 numpy:纯 Python 路径即默认实现")
|
||||
joint = self._joint()
|
||||
for probe in ((600.0, 100.0), (-300.0, 700.0)):
|
||||
indices = joint.sample(200)
|
||||
representatives = indices[::max(len(indices) // 7, 1)]
|
||||
vector = joint._epr_vector(probe, indices, representatives)
|
||||
python = joint._epr_python(probe, indices, representatives)
|
||||
self.assertAlmostEqual(vector, python, places=6)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+58
-1
@@ -13,7 +13,7 @@ from dataclasses import replace
|
||||
from random import Random
|
||||
|
||||
from mcm26b.config import ProblemConfig
|
||||
from mcm26b.particles import ChannelParticles, angular_gap
|
||||
from mcm26b.particles import ChannelParticles, angular_gap, as_sample_array
|
||||
from mcm26b.protocol.rules import DIRECTION, NEAR, NO_SIGNAL
|
||||
from mcm26b.planner.seven import seven_points
|
||||
|
||||
@@ -169,3 +169,60 @@ class QueryPrimitiveTests(unittest.TestCase):
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
class VectorizedScoringParityTests(unittest.TestCase):
|
||||
"""numpy 快路径与纯 Python 路径必须逐位同语义(方案 §P0-4)。
|
||||
|
||||
评分只用于选点,不参与安全证明;但两条路径若给出不同排序,会导致
|
||||
同一动作日志在不同环境下不可复现,因此必须对齐。
|
||||
"""
|
||||
|
||||
def _fixture(self):
|
||||
config = ProblemConfig()
|
||||
particles = ChannelParticles(config)
|
||||
rng = Random(20260912)
|
||||
subsample = [(rng.uniform(-1800, 1800), rng.uniform(-1800, 1800))
|
||||
for _ in range(64)]
|
||||
representatives = subsample[::8]
|
||||
return particles, subsample, representatives
|
||||
|
||||
def test_expected_survival_matches(self):
|
||||
particles, subsample, representatives = self._fixture()
|
||||
for point in ((0.0, 0.0), (700.0, -300.0), (-1200.0, 900.0)):
|
||||
fast = particles.expected_survival(point, as_sample_array(subsample),
|
||||
as_sample_array(representatives))
|
||||
slow = particles.expected_survival(point, subsample, representatives)
|
||||
self.assertAlmostEqual(fast, slow, places=9)
|
||||
|
||||
def test_expected_posterior_radius_matches(self):
|
||||
particles, subsample, representatives = self._fixture()
|
||||
for point in ((0.0, 0.0), (700.0, -300.0), (-1200.0, 900.0)):
|
||||
fast = particles.expected_posterior_radius(
|
||||
point, as_sample_array(subsample),
|
||||
as_sample_array(representatives))
|
||||
slow = particles.expected_posterior_radius(
|
||||
point, subsample, representatives)
|
||||
self.assertAlmostEqual(fast, slow, places=9)
|
||||
|
||||
def test_fraction_within_and_branch_radius_match(self):
|
||||
particles, subsample, _ = self._fixture()
|
||||
array = as_sample_array(subsample)
|
||||
for radius in (100.0, 1000.0, 1500.0):
|
||||
self.assertAlmostEqual(
|
||||
particles.fraction_within(array, (10.0, 20.0), radius),
|
||||
particles.fraction_within(subsample, (10.0, 20.0), radius),
|
||||
places=12)
|
||||
self.assertAlmostEqual(particles._branch_radius(array),
|
||||
particles._branch_radius(subsample), places=9)
|
||||
|
||||
def test_empty_samples_are_safe(self):
|
||||
particles, _, _ = self._fixture()
|
||||
self.assertEqual(particles.expected_survival((0.0, 0.0), [], []), 0.0)
|
||||
self.assertEqual(
|
||||
particles.expected_posterior_radius(
|
||||
(0.0, 0.0), as_sample_array([]), as_sample_array([])), 0.0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
"""图论 / DP 工具箱验收(docs/第三四问图论与动态规划优化方案.md §3.2、§3.4)。
|
||||
|
||||
要点:
|
||||
- Held-Karp 精确开放路径必须**真的**最优(与全排列暴力对拍);
|
||||
- 启发式(NN + 2-opt / Or-opt)必须是可接受的上界,且不劣于 NN;
|
||||
- MST 权重必须是最优开放路径的合法下界(在线后悔值审计依赖它);
|
||||
- 路由度量口径可复算:actual == offline 时 gap 为 0,且 gap 不越界。
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import math
|
||||
import random
|
||||
import unittest
|
||||
|
||||
from mcm26b.routing import (EXACT_LIMIT, held_karp_open, improve_open_path,
|
||||
mst_weight, nearest_neighbor_order,
|
||||
optimal_open_path, or_opt_open, path_length,
|
||||
route_metrics, two_opt_open)
|
||||
|
||||
|
||||
def _random_points(count: int, seed: int, radius: float = 1500.0):
|
||||
rng = random.Random(seed)
|
||||
return [(rng.uniform(-radius, radius), rng.uniform(-radius, radius))
|
||||
for _ in range(count)]
|
||||
|
||||
|
||||
class HeldKarpTests(unittest.TestCase):
|
||||
def test_matches_brute_force_on_small_instances(self):
|
||||
for seed in range(6):
|
||||
points = _random_points(6, seed)
|
||||
order = held_karp_open(points)
|
||||
best = min(path_length(list(perm))
|
||||
for perm in itertools.permutations(points))
|
||||
self.assertAlmostEqual(path_length(order), best, places=6,
|
||||
msg=f"seed={seed}")
|
||||
|
||||
def test_returns_all_points_once(self):
|
||||
points = _random_points(9, 42)
|
||||
order = held_karp_open(points)
|
||||
self.assertEqual(sorted(order), sorted(points))
|
||||
|
||||
def test_single_and_empty(self):
|
||||
self.assertEqual(held_karp_open([]), [])
|
||||
point = [(3.0, 4.0)]
|
||||
self.assertEqual(held_karp_open(point), point)
|
||||
|
||||
|
||||
class HeuristicTests(unittest.TestCase):
|
||||
def test_two_opt_and_or_opt_never_worse_than_nn(self):
|
||||
for seed in range(5):
|
||||
points = _random_points(30, seed)
|
||||
base = path_length(nearest_neighbor_order(points))
|
||||
self.assertLessEqual(path_length(two_opt_open(
|
||||
nearest_neighbor_order(points))), base + 1e-9)
|
||||
self.assertLessEqual(path_length(improve_open_path(points)),
|
||||
base + 1e-9)
|
||||
|
||||
def test_optimal_open_path_beats_heuristics_small(self):
|
||||
points = _random_points(10, 7)
|
||||
optimal = path_length(optimal_open_path(points))
|
||||
self.assertLessEqual(optimal,
|
||||
path_length(improve_open_path(points)) + 1e-9)
|
||||
|
||||
def test_or_opt_accepts_only_improvements(self):
|
||||
points = _random_points(20, 3)
|
||||
order = nearest_neighbor_order(points)
|
||||
improved = or_opt_open(order)
|
||||
self.assertLessEqual(path_length(improved), path_length(order) + 1e-9)
|
||||
|
||||
|
||||
class LowerBoundTests(unittest.TestCase):
|
||||
def test_mst_is_a_lower_bound(self):
|
||||
for seed in range(8):
|
||||
points = _random_points(24, seed)
|
||||
optimum = path_length(optimal_open_path(points))
|
||||
self.assertLessEqual(mst_weight(points), optimum + 1e-6,
|
||||
msg=f"seed={seed}")
|
||||
|
||||
def test_mst_without_start(self):
|
||||
points = _random_points(12, 1)
|
||||
self.assertLessEqual(mst_weight(points, start=None),
|
||||
path_length(optimal_open_path(points)) + 1e-6)
|
||||
|
||||
|
||||
class RouteMetricsTests(unittest.TestCase):
|
||||
def test_gap_zero_when_actual_matches_offline(self):
|
||||
points = _random_points(8, 5)
|
||||
offline = path_length(optimal_open_path(points))
|
||||
metrics = route_metrics(points, offline)
|
||||
self.assertAlmostEqual(metrics["route_gap_vs_offline"], 0.0, places=6)
|
||||
self.assertEqual(metrics["distinct_service_points"], 8)
|
||||
|
||||
def test_dedupes_points(self):
|
||||
points = [(1.0, 1.0), (1.0, 1.0), (2.0, 2.0)]
|
||||
metrics = route_metrics(points, 10.0)
|
||||
self.assertEqual(metrics["distinct_service_points"], 2)
|
||||
|
||||
def test_empty_is_safe(self):
|
||||
metrics = route_metrics([], 0.0)
|
||||
self.assertEqual(metrics["distinct_service_points"], 0)
|
||||
self.assertEqual(metrics["route_gap_vs_offline"], 0.0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,91 @@
|
||||
"""P1-4 安全阀与退出可解释性验收。
|
||||
|
||||
覆盖 docs/第三四问架构优化与运行提速方案.md §P1-4:
|
||||
- summary 必须显式给出完成依据 completion_basis(数量上界 vs 逐频道证明);
|
||||
- 逐频道缺席必须带证据来源 absence_evidence(pf_empty / coverage);
|
||||
- 重复复扫计数 redundant_rescan_measures 是历史 98.84% 空复扫问题的回归指标;
|
||||
- 虚拟安全余量不小于单动作耗时上界(配置被调小时仍成立)。
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from dataclasses import replace
|
||||
|
||||
from mcm26b.config import ProblemConfig
|
||||
from mcm26b.planner import DirectionalExtension, OmnidirectionalLoop
|
||||
from mcm26b.planner.bayes_loop import BayesLoop
|
||||
from mcm26b.protocol.client import SimulatorClient
|
||||
from mcm26b.harness import LocalSimTransport, LocalSimulator
|
||||
from mcm26b.harness import mixed_scene, random_omni_scene
|
||||
|
||||
CONFIG = replace(ProblemConfig(),
|
||||
search=replace(ProblemConfig().search,
|
||||
bearing_rounding_margin_deg=0.005,
|
||||
persistent_search=False))
|
||||
|
||||
|
||||
def _run(loop_cls, sources, config=CONFIG):
|
||||
simulator = LocalSimulator(sources, robot_id="T-SAFE", config=config)
|
||||
client = SimulatorClient("T-SAFE", config=config,
|
||||
transport=LocalSimTransport(simulator))
|
||||
return loop_cls(client, config).run(), simulator
|
||||
|
||||
|
||||
class SafetyValveTests(unittest.TestCase):
|
||||
def test_summary_completion_and_absence_evidence(self):
|
||||
sources = random_omni_scene(seed=5, count=10, config=CONFIG)
|
||||
summary, _ = _run(BayesLoop, sources)
|
||||
truth = sorted(s.channel for s in sources)
|
||||
self.assertEqual(sorted(summary["cleared_channels"]), truth)
|
||||
self.assertEqual(summary["completion_basis"], "per_channel_proof")
|
||||
self.assertEqual(sorted(summary["absent_channels"]),
|
||||
sorted(set(range(1, 21)) - set(truth)))
|
||||
evidence = summary["absence_evidence"]
|
||||
self.assertEqual(sorted(int(c) for c in evidence),
|
||||
sorted(summary["absent_channels"]))
|
||||
self.assertTrue(set(evidence.values()) <= {"pf_empty", "coverage"})
|
||||
self.assertIsInstance(summary["redundant_rescan_measures"], int)
|
||||
self.assertGreaterEqual(summary["virtual_reserve_s"], 10_000.0)
|
||||
|
||||
def test_redundant_rescan_measure_is_counted(self):
|
||||
"""同一测点重复检测必须被计数(正常模式应为 0)。"""
|
||||
sources = random_omni_scene(seed=7, count=10, config=CONFIG)
|
||||
summary, _ = _run(BayesLoop, sources)
|
||||
self.assertEqual(summary["redundant_rescan_measures"], 0)
|
||||
|
||||
def test_count_cap_is_distinguished_from_proof(self):
|
||||
"""16 源时按数量上界停止,completion_basis 不得写成逐频道证明。"""
|
||||
sources = random_omni_scene(seed=11, count=16, config=CONFIG)
|
||||
summary, _ = _run(BayesLoop, sources)
|
||||
self.assertGreaterEqual(len(summary["cleared_channels"]), 16)
|
||||
self.assertEqual(summary["completion_basis"], "count_cap")
|
||||
|
||||
def test_q4_completion_basis_present(self):
|
||||
sources = mixed_scene(seed=3, count=10, directional=3, config=CONFIG)
|
||||
summary, simulator = _run(DirectionalExtension, sources)
|
||||
truth = sorted(s.channel for s in sources)
|
||||
self.assertEqual(sorted(summary["cleared_channels"]), truth)
|
||||
self.assertIn(summary["completion_basis"],
|
||||
{"per_channel_proof", "count_cap"})
|
||||
self.assertEqual(sorted(simulator.cleared), truth)
|
||||
|
||||
|
||||
class VirtualReserveTests(unittest.TestCase):
|
||||
def test_reserve_covers_single_action_bound(self):
|
||||
"""配置余量被调小到 0 时,有效余量仍不得小于单动作上界。"""
|
||||
tight = replace(ProblemConfig(),
|
||||
search=replace(ProblemConfig().search,
|
||||
virtual_reserve_s=0.0))
|
||||
sources = random_omni_scene(seed=1, count=1, config=tight)
|
||||
simulator = LocalSimulator(sources, robot_id="T-RES", config=tight)
|
||||
client = SimulatorClient("T-RES", config=tight,
|
||||
transport=LocalSimTransport(simulator))
|
||||
loop = OmnidirectionalLoop(client, tight)
|
||||
reach = max((x * x + y * y) ** 0.5 for x, y in loop.ledger.points)
|
||||
bound = 2.0 * reach / tight.cost.speed_mps \
|
||||
+ tight.cost.measure_s + tight.cost.switch_s
|
||||
self.assertGreaterEqual(loop._min_virtual_reserve(), bound)
|
||||
self.assertLess(loop._min_virtual_reserve(), 10_000.0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user