% Generated by scripts/build_code_appendix.py from mcm26b-核心代码.ipynb. % Do not edit by hand: rerun the generator after the notebook changes. \begingroup\linespread{1.0}\selectfont \subsection*{动作计时与开放路径排序} 三段耗时按整数微秒累计;后备清除的走格顺序用最近邻打底并做 2-opt 改进。 \noindent\small \texttt{CostBreakdown}:一次合法动作的三段耗时:移动 + 换频道(仅检测)+ 动作本身。 \begin{lstlisting}[style=corecode] class CostBreakdown: """一次合法动作的三段耗时 [题面]:移动 + 换频道(仅检测)+ 动作本身。""" move_s: float switch_s: float action_s: float @property def total_s(self) -> float: return self.total_us / 1_000_000 @property def total_us(self) -> int: """逐段量化后按整数微秒累计,与官方虚拟时钟一致。""" return sum(round(value * 1_000_000) for value in (self.move_s, self.switch_s, self.action_s)) \end{lstlisting} \noindent\small \texttt{measure\_cost}:合法 /measure 的耗时;移动按直线距离,换频道只看前后频道是否不同。 \begin{lstlisting}[style=corecode] def measure_cost(prev_pos: Vec2, pos: Vec2, prev_channel: int, channel: int, cost: CostConfig = CostConfig()) -> CostBreakdown: """合法 /measure 的耗时。移动按直线距离;换频道只看前后频道是否不同。""" return CostBreakdown( move_s=dist(prev_pos, pos) / cost.speed_mps, switch_s=0.0 if channel == prev_channel else cost.switch_s, action_s=cost.measure_s, ) \end{lstlisting} \noindent\small \texttt{clear\_cost}:合法 /clear 的耗时;清除不切换测向机频道,故不含换频项。 \begin{lstlisting}[style=corecode] def clear_cost(prev_pos: Vec2, pos: Vec2, success: bool, cost: CostConfig = CostConfig()) -> CostBreakdown: """合法 /clear 的耗时。""" return CostBreakdown( move_s=dist(prev_pos, pos) / cost.speed_mps, switch_s=0.0, action_s=cost.clear_success_s if success else cost.clear_fail_s, ) \end{lstlisting} \noindent\small \texttt{move\_time}:任意两点间的移动耗时,供规划使用。 \begin{lstlisting}[style=corecode] def move_time(a: Vec2, b: Vec2, cost: CostConfig = CostConfig()) -> float: """任意两点间的移动耗时(规划用)。""" return dist(a, b) / cost.speed_mps \end{lstlisting} \noindent\small \texttt{nearest\_neighbor\_order}:最近邻开放路径,作为路径排序的启发式上界。 \begin{lstlisting}[style=corecode] def nearest_neighbor_order(points: list[Vec2], start: Vec2 = (0.0, 0.0)) -> list[Vec2]: """最近邻开放路径(O(n²),启发式上界)。""" remaining = list(points) order: list[Vec2] = [] position = start while remaining: nxt = min(remaining, key=lambda q: math.dist(position, q)) remaining.remove(nxt) order.append(nxt) position = nxt return order \end{lstlisting} \noindent\small \texttt{two\_opt\_open}:开放路径 2-opt 改进;终点自由,只反转内部片段。 \begin{lstlisting}[style=corecode] def two_opt_open(order: list[Vec2], start: Vec2 = (0.0, 0.0), max_passes: int = 8) -> list[Vec2]: """开放路径 2-opt(终点自由:只反转内部片段,不做首尾相接)。""" best = list(order) n = len(best) if n < 3: return best for _ in range(max_passes): improved = False for i in range(n - 1): prev = best[i - 1] if i > 0 else start pi = best[i] d_prev_i = math.dist(prev, pi) for j in range(i + 1, n): pj = best[j] nxt = best[j + 1] if j + 1 < n else None d_prev_j = math.dist(prev, pj) removed = d_prev_i added = d_prev_j if nxt is not None: removed += math.dist(pj, nxt) added += math.dist(pi, nxt) if added < removed - 1e-9: best[i:j + 1] = best[i:j + 1][::-1] improved = True pi = best[i] d_prev_i = math.dist(prev, pi) if not improved: break return best \end{lstlisting} \subsection*{问题一:角域交会、直径与最小包围圆} 半平面交求定位区域,顶点对枚举直径,Welzl 算法求最小包围圆,并输出直径圆覆盖判定与清除半径判据。 \noindent\small \texttt{Measurement}:一次示向度观测:测点坐标、示向度与误差半角(题设为 $1^\circ$)。 \begin{lstlisting}[style=corecode] class Measurement: x: float y: float bearing_deg: float half_angle_deg: float = 1.0 \end{lstlisting} \noindent\small \texttt{halfplanes}:一次观测的三个半平面约束:角域两侧边界与前向约束,法向量已单位化。 \begin{lstlisting}[style=corecode] def halfplanes(m: Measurement) -> list[tuple[float, float, float]]: """Return a*x+b*y<=c with unit normals, including the forward…""" theta = m.bearing_deg % 360 lower, upper = radians(theta - m.half_angle_deg), radians(theta + m.half_angle_deg) middle = radians(theta) normals = [(sin(lower), -cos(lower)), (-sin(upper), cos(upper)), (-cos(middle), -sin(middle))] return [(a, b, a * m.x + b * m.y) for a, b in normals] \end{lstlisting} \noindent\small \texttt{convex\_hull}:单调链凸包(逆时针、无重复末点),输入坐标先平移缩放到同一量级。 \begin{lstlisting}[style=corecode] def convex_hull(points: list[Point], eps: float = EPS) -> list[Point]: """Monotone chain, CCW, no repeated final vertex; retain segm…""" ordered = sorted(points) unique = [] for p in ordered: if not unique or distance(p, unique[-1]) > eps: unique.append(p) if len(unique) <= 2: return unique def chain(seq): out = [] for p in seq: while len(out) >= 2: a = (out[-1][0] - out[-2][0], out[-1][1] - out[-2][1]) b = (p[0] - out[-1][0], p[1] - out[-1][1]) # Distance-to-line tolerance, not an unscaled area tolerance. if cross(a, b) > eps * max(hypot(*a), hypot(*b), eps): break out.pop() out.append(p) return out return chain(unique)[:-1] + chain(reversed(unique))[:-1] \end{lstlisting} \noindent\small \texttt{diameter}:顶点对枚举区域直径;有界凸集的最远点必为顶点。 \begin{lstlisting}[style=corecode] def diameter(points: list[Point]): if not points: raise ValueError("空点集没有本程序定义下的直径") a = b = points[0] value = 0.0 for p, q in combinations(points, 2): d = distance(p, q) if d > value: value, a, b = d, p, q return value, (a, b) \end{lstlisting} \noindent\small \texttt{smallest\_enclosing\_circle}:Welzl 随机增量最小包围圆;固定随机种子保证结果可复现。 \begin{lstlisting}[style=corecode] def smallest_enclosing_circle(points: list[Point], eps: float = EPS): """Welzl's randomized incremental algorithm, using an explici…""" if not points: raise ValueError("空集不计算最小包围圆") if not isfinite(eps) or eps <= 0: raise ValueError("eps必须为有限正数") if any(len(p) != 2 or any(isinstance(x, bool) or not isinstance(x, (int, float)) or not isfinite(x) for x in p) for p in points): raise ValueError("点集必须由有限二维坐标组成") origin = points[0] scale = max(distance(origin, p) for p in points) if scale == 0: return tuple(origin), 0.0 local = [((p[0]-origin[0])/scale, (p[1]-origin[1])/scale) for p in points] tolerance = min(eps/scale, EPS) Random(20260910).shuffle(local) # Frame = (remaining prefix length, forced boundary, returning from child). stack = [(len(local), (), False)] circle = None while stack: n, boundary, returning = stack.pop() if n == 0 or len(boundary) == 3: # Forced support points must lie ON the boundary. For three points # use their circumcircle, even when their unconstrained MEC is smaller. circle = _circumcircle(*boundary) if len(boundary) == 3 else None if circle is None and boundary: circle = _enclosing_circle_reference(list(boundary), tolerance) elif not returning: stack.append((n, boundary, True)) stack.append((n-1, boundary, False)) elif not _circle_covers(circle, local[n-1], tolerance): stack.append((n-1, boundary + (local[n-1],), False)) assert circle is not None center = (origin[0]+circle[0][0]*scale, origin[1]+circle[0][1]*scale) # Recompute after transforming back: returned circle contains all input points # to distance() precision, including any roundoff from the large translation. return center, max(distance(center, p) for p in points) \end{lstlisting} \noindent\small \texttt{solve}:问题一主入口:半平面交求定位区域,区分空集、无界与退化情形,再给出直径、直径圆覆盖判定与最小包围圆。 \begin{lstlisting}[style=corecode] def solve(measurements: list[Measurement], eps: float = EPS) -> dict: """Solve Q1's pure angular intersection (no disk/range prior…""" if not isfinite(eps) or eps <= 0: raise ValueError("eps必须为有限正数") if not measurements: return {"status": "unbounded", "vertices": [], "diameter_m": None, "diameter_infinite": True, "recession_direction": [1.0, 0.0], "message": "没有观测,整个平面均可行"} if len(measurements) > 100: raise ValueError("参考实现最多接收100个检测点;更大规模应使用半平面交算法") ox = sum(m.x for m in measurements) / len(measurements) oy = sum(m.y for m in measurements) / len(measurements) scale = max(1.0, max(hypot(m.x - ox, m.y - oy) for m in measurements)) local = [Measurement((m.x - ox) / scale, (m.y - oy) / scale, m.bearing_deg, m.half_angle_deg) for m in measurements] planes = [h for m in local for h in halfplanes(m)] def inside(p): tol = eps * max(1.0, abs(p[0]), abs(p[1])) return all(a * p[0] + b * p[1] <= c + tol for a, b, c in planes) feasible_vertices = [] for (a, b, c), (d, e, f) in combinations(planes, 2): det = a * e - b * d if det == 0.0: continue # 精确平行:无交点(近平行对的交点是合法远端顶点,保留) p = ((c * e - b * f) / det, (a * f - c * d) / det) if isfinite(p[0]) and isfinite(p[1]) and inside(p): feasible_vertices.append(p) if not feasible_vertices: return {"status": "empty", "vertices": [], "diameter_m": None, "diameter_infinite": False, "tolerance_m": eps * scale, "message": "测向角域没有公共点;检查频道、数据及误差界"} # A nontrivial 2D recession cone has a ray tangent to an input halfplane. # Use a much tighter angular threshold than the positional feasibility epsilon. ray = None for a, b, _ in planes: for direction in ((b, -a), (-b, a)): if all(d * direction[0] + e * direction[1] <= 2e-14 for d, e, _ in planes): ray = direction break if ray is not None: break hull = convex_hull(feasible_vertices, eps) def world(p): return [ox + p[0] * scale, oy + p[1] * scale] result = {"status": "unbounded" if ray else ("point" if len(hull) == 1 else "segment" if len(hull) == 2 else "polygon"), "vertices": [world(p) for p in hull], "tolerance_m": eps * scale, "diameter_infinite": ray is not None} if ray is not None: result.update(diameter_m=None, recession_direction=list(ray), message="存在非零衰退方向,区域无界,直径为无穷大") return result d, pair = diameter(hull) midpoint = ((pair[0][0] + pair[1][0]) / 2, (pair[0][1] + pair[1][1]) / 2) distances = [distance(midpoint, p) for p in hull] witness_index = max(range(len(hull)), key=distances.__getitem__) max_distance = distances[witness_index] center, radius = smallest_enclosing_circle(hull, eps) area = abs(sum(cross(hull[i], hull[(i + 1) % len(hull)]) for i in range(len(hull)))) / 2 result.update(diameter_m=d * scale, diameter_pair=[world(p) for p in pair], area_m2=area * scale ** 2, diameter_circle={"center": world(midpoint), "radius_m": d * scale / 2, "max_vertex_distance_m": max_distance * scale, "covers": max_distance <= d / 2 + eps * 4, "coverage_tolerance_m": 4 * eps * scale, "max_excess_m": (max_distance-d/2) * scale, "outside_vertex_indices": [i for i, value in enumerate(distances) if value > d/2 + 4*eps], "witness_vertex": world(hull[witness_index]) if max_distance > d/2 + 4*eps else None}, minimum_enclosing_circle={"center": world(center), "radius_m": radius * scale}, guaranteed_clearable_at_mec_center=radius * scale <= 20 - eps * scale * 4, clearance_certificate={"model": "angular_intersection_only", "clear_radius_m": 20.0, "numerical_margin_m": 4*eps*scale, "radius_with_margin_m": (radius+4*eps)*scale, "remaining_margin_m": 20-(radius+4*eps)*scale, "center": world(center)}) return result \end{lstlisting} \subsection*{问题二:候选集与保证接收的第二检测点} 候选集用圆盘外切多边形与角域半平面裁剪表示;安全盘半径取$1000-\rho$,盘内候选按最坏后验包围半径、同分按移动代价取优。 \noindent\small \texttt{CandidateRegion}:某频道候选集合的保守凸外包多边形(顶点按 CCW;允许退化为 1-2 点)。 \begin{lstlisting}[style=corecode] class CandidateRegion: """某频道候选集合的保守凸外包多边形(顶点按 CCW;允许退化为 1-2 点)。""" @classmethod def disk(cls, center: Vec2, radius_m: float, sides: int, slack_m: float | None = None) -> "CandidateRegion": if slack_m is None: slack_m = radius_m * (1 - math.cos(math.pi / sides)) return cls(circumscribed_polygon(center, radius_m, sides), slack_m) @classmethod def initial(cls, station: Vec2, config: ProblemConfig) -> "CandidateRegion": """首个 direction 后的基础集:Ω ∩ B(station, r_max) 的保守外包。""" field_cfg, sides = config.field, config.search.polygon_sides slack = field_cfg.omega_radius_m * (1 - math.cos(math.pi / sides)) region = cls.disk((0.0, 0.0), field_cfg.omega_radius_m, sides, slack) region.clip_disk(station, field_cfg.r_eff_max_m, sides) return region def clip_disk(self, center: Vec2, radius_m: float, sides: int) -> None: """与圆盘 B(center, radius) 的外切多边形求交(近似误差入账)。""" for a, b, c in polygon_halfplanes(circumscribed_polygon(center, radius_m, sides)): self._clip(a, b, c) self.slack_m += radius_m * (1 - math.cos(math.pi / sides)) def clip_wedge(self, station: Vec2, bearing_deg: float, half_angle_deg: float) -> None: """与测向角域 W(station, bearing±half_angle) 求交(半角须 <90°)。 两个边界半平面与 localization.halfplanes 同口径,0°/360° 跨界由 sin/cos 直接处理,无需分支。 """ if not 0 < half_angle_deg < 90: raise ValueError("角域半角须在 (0, 90) 度") lower = math.radians(bearing_deg - half_angle_deg) upper = math.radians(bearing_deg + half_angle_deg) sx, sy = float(station[0]), float(station[1]) self._clip(math.sin(lower), -math.cos(lower), math.sin(lower) * sx - math.cos(lower) * sy) self._clip(-math.sin(upper), math.cos(upper), -math.sin(upper) * sx + math.cos(upper) * sy) def mec(self) -> tuple[Vec2, float]: """最小包围圆(圆心, 半径)。半径是真实候选集 ρ 的上界。""" if self._mec is None: if not self.polygon: raise InconsistentBelief("候选区域为空") self._mec = smallest_enclosing_circle(self.polygon) return self._mec def witnesses(self, spacing_m: float) -> list[Vec2]: """顶点 + 边界按间距细分的采样点(最坏后验评估用)。""" if len(self.polygon) < 3: return list(self.polygon) points = list(self.polygon) count = len(self.polygon) for index in range(count): p, q = self.polygon[index], self.polygon[(index + 1) % count] length = math.dist(p, q) steps = int(length // spacing_m) for k in range(1, steps + 1): ratio = k / (steps + 1) points.append((p[0] + ratio * (q[0] - p[0]), p[1] + ratio * (q[1] - p[1]))) return _dedupe(points, tol=1e-9) def contains(self, point: Vec2, tol_m: float = 0.05) -> bool: """点是否在候选集内(带容差;用于真源保留性检验,不做删点依据)。""" if not self.polygon: return False if len(self.polygon) == 1: return math.dist(point, self.polygon[0]) <= tol_m + self.slack_m if len(self.polygon) == 2: p, q = self.polygon return _point_segment_distance(point, p, q) <= tol_m + self.slack_m for index, p in enumerate(self.polygon): q = self.polygon[(index + 1) % len(self.polygon)] if (q[0] - p[0]) * (point[1] - p[1]) - (q[1] - p[1]) * (point[0] - p[0]) \ < -tol_m - self.slack_m: return False return True \end{lstlisting} \noindent\small \texttt{ClearCertificate}:清除充分条件:候选集外包半径与近似误差之和不超过清除半径。 \begin{lstlisting}[style=corecode] class ClearCertificate: """清除证书:候选集外包半径(含近似误差)不超过 20m 阈值。""" channel: int center: Vec2 radius_m: float limit_m: float certified: bool \end{lstlisting} \noindent\small \texttt{clear\_certificate}:由候选集最小包围圆判定是否可以就地清除。 \begin{lstlisting}[style=corecode] def clear_certificate(region: CandidateRegion, channel: int, config: ProblemConfig = ProblemConfig()) -> ClearCertificate: """ρ_poly <= 20 - 外包近似误差 - 工程余量 才允许发起清除。""" center, radius = region.mec() limit = (config.field.clear_radius_m - region.slack_m - config.search.clear_margin_m) return ClearCertificate(channel, center, radius, limit, radius <= limit) \end{lstlisting} \noindent\small \texttt{candidate\_points}:保证接收盘内的候选点:盘心加各比例半径上的均匀方向点。 \begin{lstlisting}[style=corecode] def candidate_points(center: Vec2, safe_radius_m: float, rings, angles: int) -> list[Vec2]: """保证接收盘内的候选点:盘心 + 各比例半径 × 均匀方向。""" points = [tuple(center)] for fraction in rings: radius = fraction * safe_radius_m if radius <= 1e-9: continue step = 2 * math.pi / angles for k in range(angles): angle = step * k + step / 2 points.append((center[0] + radius * math.cos(angle), center[1] + radius * math.sin(angle))) return points \end{lstlisting} \noindent\small \texttt{safe\_disk\_radius}:保证接收盘半径 $1000-\rho$;小于零表示没有保证接收的区域。 \begin{lstlisting}[style=corecode] def safe_disk_radius(region: CandidateRegion, config: ProblemConfig = ProblemConfig()) -> float: """保证接收盘 B(mec, safe) 的半径;<0 表示无保证(ρ 超过最小接收半径)。""" return config.field.r_eff_min_m - region.mec()[1] - 1e-3 \end{lstlisting} \noindent\small \texttt{select\_measurement\_point}:在保证接收盘内选下一检测点:主指标为最坏后验包围半径,同分取移动代价较小者。 \begin{lstlisting}[style=corecode] def select_measurement_point(region: CandidateRegion, from_position: Vec2, config: ProblemConfig = ProblemConfig()) -> SecondPointPlan: """在保证接收盘内选择下一检测点(P3 核心;对任意候选集通用)。""" field, search = config.field, config.search center, radius = region.mec() half_angle = field.bearing_error_half_deg + search.bearing_rounding_margin_deg def move_cost(point: Vec2) -> float: return move_time(from_position, point, config.cost) # 1mm 数值余量在 safe_disk_radius 内记账。 safe_radius = safe_disk_radius(region, config) if safe_radius < 0: return SecondPointPlan(tuple(center), radius, move_cost(center), "无保证接收盘(ρ>1000):探索 MEC 圆心,可能 no_signal") coarse = region.witnesses(search.witness_spacing_m) samples = candidate_points(center, safe_radius, search.candidate_rings, search.candidate_angles) near_radius, sides = field.near_radius_m, search.polygon_sides best_key, best_point, best_worst = None, None, None for point in samples: worst = _worst_posterior_radius(region, point, coarse, half_angle, near_radius, sides) key = (round(worst, 6), round(move_cost(point), 6)) if best_key is None or key < best_key: best_key, best_point, best_worst = key, point, worst dense = _worst_posterior_radius(region, best_point, region.witnesses(search.witness_spacing_m / 4), half_angle, near_radius, sides) reason = (f"安全盘 B(centre,{safe_radius:.0f}m) 内 {len(samples)} 候选;" f"粗采样最坏后验 {best_worst:.1f}m,1/4 间距加密复核 {dense:.1f}m") return SecondPointPlan(tuple(best_point), dense, move_cost(best_point), reason) \end{lstlisting} \noindent\small \texttt{select\_second\_point}:问题二入口:由首次示向度构造候选集并给出第二检测点。 \begin{lstlisting}[style=corecode] def select_second_point(client: SimulatorClient, first_position: Vec2, first_bearing_deg: float, config: ProblemConfig = ProblemConfig()) -> SecondPointPlan: """P3 入口:由首次 direction 构造 K1,返回保证接收的第二检测点方案。""" region = CandidateRegion.initial(first_position, config) half_angle = config.field.bearing_error_half_deg \ + config.search.bearing_rounding_margin_deg region.clip_wedge(first_position, first_bearing_deg, half_angle) return select_measurement_point(region, client.state.position, config) \end{lstlisting} \subsection*{问题三:覆盖计划、检测记录与清除证书} 覆盖计划与运行期抽样校验、逐频道检测记录,以及七点解析覆盖与全向/定向两种覆盖语义的证书判据。 \noindent\small \texttt{CoverageCertificate}:覆盖证书:采样分辨率、位置与朝向目标数、缺口数与最坏命中距离。 \begin{lstlisting}[style=corecode] class CoverageCertificate: """覆盖证书:采样语义 + 缺口 + 最坏命中距离。""" kind: str # "full_disk" | "forward_half_disk" target_step_m: float headings: int r_eff_min_m: float targets: int uncovered: int worst_hit_distance_m: float nn_tour_m: float generator_sha256: str = "" verify_heading_margin_deg: float = 0.0 @property def valid(self) -> bool: return self.uncovered == 0 and self.targets > 0 def as_dict(self) -> dict: return {"kind": self.kind, "target_step_m": self.target_step_m, "headings": self.headings, "r_eff_min_m": self.r_eff_min_m, "targets": self.targets, "uncovered": self.uncovered, "worst_hit_distance_m": round(self.worst_hit_distance_m, 3), "nn_tour_m": round(self.nn_tour_m, 1), "nn_tour_virtual_s": round(self.nn_tour_m / SPEED_MPS, 1), "generator_sha256": self.generator_sha256, "verify_heading_margin_deg": self.verify_heading_margin_deg} \end{lstlisting} \noindent\small \texttt{CoveragePlan}:覆盖计划:点集、覆盖语义与证书。 \begin{lstlisting}[style=corecode] class CoveragePlan: """一份覆盖计划:点集 + 语义 + 证书(可为解析证明)。""" name: str problem: int points: tuple[Vec2, ...] kind: str # "analytic" | "certified" coverage: str # "full_disk" | "forward_half_disk" note: str = "" certificate: CoverageCertificate | None = None generator: dict = field(default_factory=dict) def as_dict(self) -> dict: return {"name": self.name, "problem": self.problem, "kind": self.kind, "coverage": self.coverage, "note": self.note, "generator": self.generator, "certificate": (self.certificate.as_dict() if self.certificate is not None else None), "points": [[p[0], p[1]] for p in self.points]} \end{lstlisting} \noindent\small \texttt{coverage\_points}:49 点覆盖网格,单元对角 $700\sqrt2\approx989.95\le1000$ m。 \begin{lstlisting}[style=corecode] def coverage_points(config: ProblemConfig = ProblemConfig()) -> list[Vec2]: """49 点覆盖网格生成(纯几何,可独立测试)。""" step = config.search.grid_step_m half = config.search.grid_half_cells return [(step * i, step * j) for j in range(half, -half - 1, -1) for i in range(-half, half + 1)] \end{lstlisting} \noindent\small \texttt{CoverageLedger}:逐频道检测记录,按(频道, 网格点)二维记账。 \begin{lstlisting}[style=corecode] class CoverageLedger: """逐频道覆盖账本:(频道, 网格点) -> 是否已测。""" config: ProblemConfig = field(default_factory=ProblemConfig) points: list[Vec2] = field(default_factory=coverage_points) measured: set = field(default_factory=set) # {(channel, point_index)} skipped: dict = field(default_factory=dict) # channel -> 原因 def mark_measured(self, channel: int, point_index: int) -> None: self.measured.add((channel, point_index)) def measured_at(self, channel: int, point_index: int) -> bool: return (channel, point_index) in self.measured def skip_channel(self, channel: int, reason: str) -> None: """记录整频道跳过原因(如 already_cleared / located_pending_clear)。""" self.skipped[channel] = reason def pending_points(self, channel: int) -> list[int]: """该频道尚未测量的网格点下标。扫描调度策略未实现(P4)。""" return [index for index in range(len(self.points)) if not self.measured_at(channel, index)] def coverage_complete(self, channel: int) -> bool: """该频道覆盖是否完成(含显式跳过)。""" return channel in self.skipped or not self.pending_points(channel) \end{lstlisting} \noindent\small \texttt{seven\_points}:原点与环上六个等分点的七站覆盖集。 \begin{lstlisting}[style=corecode] def seven_points(ring_radius_m: float = RING_RADIUS_M) -> list[tuple[float, float]]: """原点 + 环上 6 等分点的七站覆盖集。""" return [(0.0, 0.0)] + [(ring_radius_m * math.cos(k * math.pi / 3), ring_radius_m * math.sin(k * math.pi / 3)) for k in range(6)] \end{lstlisting} \noindent\small \texttt{seven\_plan}:第三问解析覆盖计划;环半径 $1159.7$ m 时最坏覆盖距离不超过 $984.539$ m。 \begin{lstlisting}[style=corecode] def seven_plan(ring_radius_m: float) -> CoveragePlan: """第三问解析计划(覆盖证明由 validate_seven_coverage 给出)。""" return CoveragePlan( name=f"seven-r{ring_radius_m:.1f}", problem=3, kind="analytic", coverage="full_disk", note=("解析证明 max(r/√3, sqrt(R²+r²−√3Rr)) < 1000;" "r∈[1122.9, 1732] 时成立,巡检半径越小巡回越短"), points=tuple(seven_points(ring_radius_m))) \end{lstlisting} \noindent\small \texttt{verify\_full\_disk}:第三问覆盖语义:圆域内每点都落在某个被访点的最小接收圆盘内。 \begin{lstlisting}[style=corecode] def verify_full_disk(points: list[Vec2], step_m: float = VERIFY_STEP_M, r_eff: float = R_EFF_MIN_M) -> CoverageCertificate: """第三问语义:Ω 内每点都在某个被访点的 r_eff 圆盘内。""" points = list(points) positions = omega_positions(step_m) table = _bucket(points, r_eff) missing, worst = 0, 0.0 for (px, py) in positions: ci, cj = int(px // r_eff), int(py // r_eff) best = None for di in (-1, 0, 1): for dj in (-1, 0, 1): for c_index in table.get((ci + di, cj + dj), ()): cx, cy = points[c_index] distance = math.hypot(cx - px, cy - py) if distance <= r_eff and (best is None or distance < best): best = distance if best is None: missing += 1 else: worst = max(worst, best) return CoverageCertificate("full_disk", step_m, 1, r_eff, len(positions), missing, worst, nn_tour_length(points)) \end{lstlisting} \noindent\small \texttt{verify\_forward\_half\_disk}:第四问覆盖语义:任意位置与朝向都存在前向半盘内的被访点。 \begin{lstlisting}[style=corecode] def verify_forward_half_disk(points: list[Vec2], step_m: float = VERIFY_STEP_M, headings: int = VERIFY_HEADINGS, r_eff: float = R_EFF_MIN_M, heading_margin_deg: float = 0.0) -> CoverageCertificate: """第四问语义:∀(p,h) 存在前向半盘内的被访点。""" points = list(points) targets = forward_targets(step_m, headings) table = _bucket(points, r_eff) missing, worst = 0, 0.0 sin_margin = math.sin(math.radians(max(heading_margin_deg, 0.0))) for (px, py, heading) in targets: ux, uy = math.cos(heading), math.sin(heading) ci, cj = int(px // r_eff), int(py // r_eff) best = None for di in (-1, 0, 1): for dj in (-1, 0, 1): for c_index in table.get((ci + di, cj + dj), ()): cx, cy = points[c_index] dx, dy = cx - px, cy - py distance_sq = dx * dx + dy * dy if distance_sq > r_eff * r_eff: continue if dx * ux + dy * uy < sin_margin * math.sqrt(distance_sq) - 1e-9: continue distance = math.sqrt(distance_sq) if best is None or distance < best: best = distance if best is None: missing += 1 else: worst = max(worst, best) return CoverageCertificate("forward_half_disk", step_m, headings, r_eff, len(targets), missing, worst, nn_tour_length(points)) \end{lstlisting} \noindent\small \texttt{plan\_instance}:按名称取覆盖计划;证书计划在运行期按同一分辨率复核。 \begin{lstlisting}[style=corecode] def plan_instance(name: str, verify: bool = True) -> CoveragePlan: """统一入口:按名称取覆盖计划(内置解析计划 / 内嵌证书计划)。""" if name == "square49": return square_49_plan() if name == "seven": return seven_plan(1200.0) if name not in EMBEDDED_PLANS: raise FileNotFoundError(f"未找到覆盖计划 {name!r};现有:{plan_names()}") plan = embedded_plan(name) if verify and plan.kind == "certified": certificate = verify_plan(plan) if not certificate.valid: raise ValueError( f"覆盖计划 {name!r} 运行期校验失败:" f"{certificate.uncovered}/{certificate.targets} 个 (p,h) 未被覆盖") plan = CoveragePlan( name=plan.name, problem=plan.problem, kind=plan.kind, coverage=plan.coverage, note=plan.note, generator=plan.generator, certificate=CoverageCertificate( kind=certificate.kind, target_step_m=certificate.target_step_m, headings=certificate.headings, r_eff_min_m=certificate.r_eff_min_m, targets=certificate.targets, uncovered=certificate.uncovered, worst_hit_distance_m=certificate.worst_hit_distance_m, nn_tour_m=certificate.nn_tour_m, generator_sha256=(plan.certificate.generator_sha256 if plan.certificate else "")), points=plan.points) return plan \end{lstlisting} \subsection*{问题三:网格候选集} 逐频道的网格幸存集,按格心裕量 $\ell/\sqrt2$ 内缩后再删格,据此支持负观测更新、跳测规则与缺席证明。 \noindent\small \texttt{ChannelParticles}:单个频道的网格候选集:格心裕量 $\ell/\sqrt2$,所有删除判定内缩该裕量,故真源所在格永不被删。 \begin{lstlisting}[style=corecode] class ChannelParticles: """单个频道的网格幸存集。""" def cell_center(self, index: int) -> Vec2: i, j = divmod(index, self.width) return (self.x0 + (i + 0.5) * self.spacing_m, self.x0 + (j + 0.5) * self.spacing_m) def discard(self, indices: list[int]) -> None: """公开删除入口:联合信念把"朝向不可能"的格一并删除时使用。""" self._discard(indices) def _discard(self, indices: list[int]) -> None: if not indices: return coarse_w = self._coarse_width() for index in indices: if index not in self.alive: continue self.alive.discard(index) key = (index // self.width // _COARSE_FACTOR) * coarse_w \ + (index % self.width) // _COARSE_FACTOR count = self._coarse.get(key, 0) - 1 if count <= 0: self._coarse.pop(key, None) else: self._coarse[key] = count self.version += 1 self._mec = None def _kill_disk(self, center: Vec2, radius_m: float) -> None: """删除格心距 center <= radius_m 的全部幸存格(整格判据见模块注释)。""" if not self.alive: return reach = radius_m + _DIST_EPS_M ilo = max(0, int(math.floor((center[0] - reach - self.x0) / self.spacing_m))) ihi = min(self.width - 1, int(math.ceil((center[0] + reach - self.x0) / self.spacing_m))) jlo = max(0, int(math.floor((center[1] - reach - self.x0) / self.spacing_m))) jhi = min(self.width - 1, int(math.ceil((center[1] + reach - self.x0) / self.spacing_m))) limit_sq = reach * reach doomed = [] for i in range(ilo, ihi + 1): dx = self.x0 + (i + 0.5) * self.spacing_m - center[0] base = i * self.width for j in range(jlo, jhi + 1): index = base + j if index not in self.alive: continue dy = self.x0 + (j + 0.5) * self.spacing_m - center[1] if dx * dx + dy * dy <= limit_sq: doomed.append(index) self._discard(doomed) def _kill_outside_disk(self, center: Vec2, radius_m: float) -> None: """删除格心距 center > radius_m 的全部幸存格(保留盘内)。""" if not self.alive: return reach = radius_m + _DIST_EPS_M doomed = [index for index in self.alive if math.dist(self.cell_center(index), center) > reach] self._discard(doomed) def _kill_outside_wedge(self, station: Vec2, reading_deg: float) -> None: """删除角向偏离读数超出 δ_eff + 角向格裕量的幸存格。 角向格裕量 = asin(m / max(dist−m, 1)):格内任一点相对格心的方位 偏差不超过它(点距测点还可再近 m,故分母再内缩 m)。 """ if not self.alive: return sx, sy = float(station[0]), float(station[1]) doomed = [] for index in self.alive: gx, gy = self.cell_center(index) dist = math.hypot(gx - sx, gy - sy) if dist <= self.margin_m + 1.0: continue # 格贴测点:格内方位可指向任意方向,不可删 angular_margin = math.degrees( math.asin(min(0.9999, self.margin_m / (dist - self.margin_m)))) gap = angular_gap(math.degrees(math.atan2(gy - sy, gx - sx)), reading_deg) if gap > self.half_angle_deg + angular_margin + _ANG_EPS_DEG: doomed.append(index) self._discard(doomed) def apply_measure(self, station: Vec2, result: str, svd_deg: float | None = None, *, directional: bool = False) -> None: """按读数更新幸存集。真源保留性由 Voronoi 裕量保证(模块注释)。 ``directional=True``(第四问):no_signal **不删空间候选**——定向源 的静默还可能来自朝向遮挡,单独删空间候选会误删真源;朝向维度由 :class:`~mcm26b.belief.joint.JointBelief` 维护。 """ if result == NO_SIGNAL: if directional: return # no_signal ⟹ dist > r_eff >= 1000:整格落入 B(s, 1000−m) 的删。 self._kill_disk(station, self.r_eff_min_m - self.margin_m) elif result == NEAR: self._kill_outside_disk(station, self.near_radius_m + self.margin_m) elif result == DIRECTION: if svd_deg is None: raise ValueError("direction 读数必须携带 svd_deg") # direction 隐含 dist <= r_max [题面] self._kill_outside_disk(station, self.r_eff_max_m + self.margin_m) self._kill_outside_wedge(station, svd_deg) else: raise ValueError(f"未知测向结果: {result!r}") def alive_points(self, limit: int | None = None) -> list[Vec2]: sorted_alive = sorted(self.alive) if limit is not None and len(sorted_alive) > limit: step = len(sorted_alive) / limit indices = {sorted_alive[int(k * step) % len(sorted_alive)] for k in range(limit)} indices = sorted(indices) else: indices = sorted_alive return [self.cell_center(index) for index in indices] def nearest_alive_distance(self, point: Vec2, limit_m: float) -> float | None: """最近幸存格心距离;> limit_m 时返回 None(粗格环扫 + 下界剪枝)。 返回值语义:恰为最近幸存格心距离(若 <= limit_m);None 表示所有 幸存格心都超出 limit_m。粗格聚合半径已计入扫描范围,不漏判。 """ if not self.alive: return None coarse_cell_m = self.spacing_m * _COARSE_FACTOR coarse_reach = coarse_cell_m * math.sqrt(2) / 2 cw = self._coarse_width() ci_base = int(math.floor((point[0] - self.x0) / coarse_cell_m)) cj_base = int(math.floor((point[1] - self.x0) / coarse_cell_m)) ring_max = int(math.ceil((limit_m + coarse_reach) / coarse_cell_m)) + 1 best: float | None = None for ring in range(ring_max + 1): lower_bound = max(0.0, ring - 1) * coarse_cell_m - coarse_reach if best is not None and lower_bound >= best: break # 更外环的所有格心都 >= 下界 >= 当前最优 for ci in range(max(0, ci_base - ring), min(cw - 1, ci_base + ring) + 1): for cj in range(max(0, cj_base - ring), min(cw - 1, cj_base + ring) + 1): if max(abs(ci - ci_base), abs(cj - cj_base)) != ring: continue # 只扫当前环(Chebyshev 环) if (ci * cw + cj) not in self._coarse: continue ccx = self.x0 + (ci + 0.5) * coarse_cell_m ccy = self.x0 + (cj + 0.5) * coarse_cell_m if math.hypot(ccx - point[0], ccy - point[1]) \ > limit_m + coarse_reach: continue i0, j0 = ci * _COARSE_FACTOR, cj * _COARSE_FACTOR for i in range(i0, min(i0 + _COARSE_FACTOR, self.width)): base = i * self.width for j in range(j0, min(j0 + _COARSE_FACTOR, self.width)): index = base + j if index not in self.alive: continue dist = math.dist(self.cell_center(index), point) if dist <= limit_m and (best is None or dist < best): best = dist return best def guaranteed_no_signal(self, station: Vec2) -> bool: """真源与该测点距离必 > r_max(>= r_eff)⟹ 必 no_signal,测了也无信息。""" return self.nearest_alive_distance( station, self.r_eff_max_m + self.margin_m) is None def mec(self) -> tuple[Vec2, float]: """幸存集最小包围圆(圆心, 半径);按 version 缓存。""" if self._mec is None: if not self.alive: raise ValueError("幸存集为空(该频道已可证明缺席)") points = [self.cell_center(index) for index in self.alive] self._mec = smallest_enclosing_circle(points) return self._mec def subsample(self, size: int) -> list[Vec2]: """按索引序均匀抽取的子采样(索引序≈空间序,代表点空间铺开)。""" return self.alive_points(limit=size) def expected_survival(self, point: Vec2, subsample, representatives) -> float: """E[log 幸存占比]:越小该测点的期望信息增益越大。 r_eff 不确定性解析积分:dist∈(1000,1500] 时按 r_eff~U(1000,1500) 取 p_ns=(dist−1000)/500 为 no_signal 分支概率。 - no_signal 分支:删 B(point, 1000−m),幸存 = 子采样中盘外比例; - direction 分支:以代表点方位为期望读数,角域 ∩ B(point,1500+m) 内幸存; - near 分支(dist <= 5+m):可行集收缩到 5m 盘,幸存占比趋近 0。 """ if not self.alive or len(subsample) == 0 or len(representatives) == 0: return 0.0 if _np is not None and isinstance(subsample, _np.ndarray) \ and isinstance(representatives, _np.ndarray): return self._expected_survival_vector(point, subsample, representatives) eps = 1.0 / len(subsample) keep_ns = max(1.0 - self.fraction_within(subsample, point, self.r_eff_min_m - self.margin_m), eps) log_keep_ns = math.log(keep_ns) total = 0.0 for rep in representatives: dist = math.hypot(rep[0] - point[0], rep[1] - point[1]) if dist <= self.near_radius_m + self.margin_m: total += math.log(eps) continue p_ns = min(1.0, max(0.0, (dist - self.r_eff_min_m) / (self.r_eff_max_m - self.r_eff_min_m))) reading = math.atan2(rep[1] - point[1], rep[0] - point[0]) survivors = 0 for p in subsample: if math.dist(p, point) > self.r_eff_max_m + self.margin_m: continue gap = angular_gap(math.degrees( math.atan2(p[1] - point[1], p[0] - point[0])), math.degrees(reading)) if gap <= self.half_angle_deg + _ANG_EPS_DEG: survivors += 1 keep_dir = max(survivors / len(subsample), eps) total += p_ns * log_keep_ns + (1.0 - p_ns) * math.log(keep_dir) return total / max(len(representatives), 1) def fraction_within(self, samples, point: Vec2, radius_m: float) -> float: if len(samples) == 0: return 0.0 reach = radius_m + _DIST_EPS_M if _np is not None and isinstance(samples, _np.ndarray): distance = _np.hypot(samples[:, 0] - point[0], samples[:, 1] - point[1]) return float(_np.count_nonzero(distance <= reach)) / len(samples) count = sum(1 for p in samples if math.dist(p, point) <= reach) return count / len(samples) def expected_posterior_radius(self, point: Vec2, subsample, representatives) -> float: """一次测量后期望后验半径(米):选点主指标,直指清除证书阈值。 对每个代表假设(真值候选)按 r_eff~U(1000,1500) 解析积分 no_signal/direction 分支,near 分支(dist <= 5+m)后验为 5m 盘; 各分支半径用幸存子采样的质心最大距离近似(偏差 << 格距,排序用)。 与几何版 worst-posterior 同族,但作用于含负观测的非凸幸存集, 且以期望代替最坏(代表点覆盖幸存集主轴)。 """ if not self.alive or len(subsample) == 0 or len(representatives) == 0: return 0.0 if _np is not None and isinstance(subsample, _np.ndarray) \ and isinstance(representatives, _np.ndarray): return self._expected_posterior_radius_vector( point, subsample, representatives) eps_radius = self.spacing_m / 2.0 total = 0.0 for rep in representatives: dist = math.hypot(rep[0] - point[0], rep[1] - point[1]) if dist <= self.near_radius_m + self.margin_m: total += self.near_radius_m + self.margin_m continue p_ns = min(1.0, max(0.0, (dist - self.r_eff_min_m) / (self.r_eff_max_m - self.r_eff_min_m))) members_ns = [p for p in subsample if math.dist(p, point) > self.r_eff_min_m - self.margin_m] reading = math.atan2(rep[1] - point[1], rep[0] - point[0]) members_dir = [] for p in subsample: if math.dist(p, point) > self.r_eff_max_m + self.margin_m: continue gap = angular_gap(math.degrees( math.atan2(p[1] - point[1], p[0] - point[0])), math.degrees(reading)) if gap <= self.half_angle_deg + _ANG_EPS_DEG: members_dir.append(p) r_ns = self._branch_radius(members_ns) if members_ns else eps_radius r_dir = self._branch_radius(members_dir) if members_dir else eps_radius total += p_ns * r_ns + (1.0 - p_ns) * r_dir return total / max(len(representatives), 1) \end{lstlisting} \subsection*{问题三:信息增益选点与滚动任务排序} 统一任务表由站点扫频、定位测量与清除三类任务组成,每执行一项即重排;定位点按期望信息增益加移动代价评分。 \noindent\small \texttt{BayesLoop}:问题三闭环:网格候选集的正负观测更新、信息增益选点与滚动任务排序。 \begin{lstlisting}[style=corecode] class BayesLoop(OmnidirectionalLoop): """问题3贝叶斯闭环:负观测感知 + 信息增益选点 + 滚动任务排序。""" def _build_tasks(self) -> list[Task]: tasks: list[Task] = [] for index, point in enumerate(self.ledger.points): pending = self._fused_channels_at(index) if not pending: continue switches = len(pending) - (1 if self.client.state.channel in pending else 0) tasks.append(Task("station", tuple(point), self.config.cost.measure_s * len(pending) + self.config.cost.switch_s * max(switches, 0), station_index=index, station_channels=tuple(pending))) for channel in self.belief.channels_by_status(ChannelStatus.DETECTED): cert = self._best_certificate(channel) if cert is not None and cert.certified \ and channel not in self._clear_failed: tasks.append(Task("clear", cert.center, self.config.cost.clear_success_s, channel=channel)) continue if self._defer_to_station(channel): continue # 下一条方位线可由"本就要去的站点"顺路提供 point = self._select_point(channel) if point is None: continue tasks.append(Task("locate", point, self.config.cost.measure_s + self.config.cost.switch_s, channel=channel)) return tasks def _execute_station(self, index: int) -> None: """单站批量扫频:只测待测频道(跳测规则已过滤),扫完即返回重排。 P0-1:本站同时测量"已发现未定证"频道,使第二/第三条方位线在巡回 途中顺路取得;证书一旦成立即就地清除,不额外绕行。 """ point = self.ledger.points[index] pending = self._fused_channels_at(index) unknown = [c for c, b in self.belief.channels.items() if b.status is ChannelStatus.UNKNOWN] self.skipped_measures += len(unknown) - len( [c for c in pending if self.belief.channels[c].status is ChannelStatus.UNKNOWN]) # 当前频道优先,减少换频;同站 near 直接清除不打断扫频 pending.sort(key=lambda c: (c != self.client.state.channel, c)) for channel in pending: if self.stop_reason is not None or self._count_limit_reached() \ or self._virtual_cap_reached(): return detected_before = (self.belief.channels[channel].status is ChannelStatus.DETECTED) outcome = self.client.measure(point, channel) self.ledger.mark_measured(channel, index) self._record_measure(channel, point, outcome) if outcome.result in (DIRECTION, NEAR): self._may_clear.add(channel) if outcome.result == NEAR: region = self.belief.candidate_set(channel) cert = clear_certificate(region, channel, self.config) if cert.certified: if self._attempt_clear(channel, point, cert): self._clear_failed.discard(channel) else: self._clear_failed.add(channel) self._may_clear.discard(channel) else: self._anomaly("near_without_certificate", channel, position=point) continue if not detected_before: continue if outcome.result == NO_SIGNAL: # 该频道已被发现:保证接收盘内不可能 no_signal,属守恒性异常 region = self.belief.candidate_set(channel) safe = (safe_disk_radius(region, self.config) if region is not None else -1.0) if safe >= 0 and math.dist(point, region.mec()[0]) <= safe: self._anomaly("no_signal_in_safe_disk", channel, position=point) continue cert = self._best_certificate(channel) if cert is not None and cert.certified: if self._attempt_clear(channel, cert.center, cert): self._clear_failed.discard(channel) else: self._clear_failed.add(channel) self._may_clear.discard(channel) def _record_measure(self, channel: int, position: Vec2, outcome) -> None: """belief(凸证书)与 PF(幸存集)同步更新;PF 空即证缺席。""" self.belief.update_measure(channel, position, outcome.result, outcome.svd_deg) # 同一 0.5m 测点 × 同一频道的重复检测:历史上一次官方演练有 98.84% # 的虚拟时间耗在空复扫上,本计数是该问题的回归指标。 key = _point_key(position) seen = self._measured_points.setdefault(channel, set()) if key in seen: self.redundant_rescan_measures += 1 seen.add(key) self._clear_failed.discard(channel) # 新证据落地:允许再次尝试清除 belief = self.belief.channels[channel] if belief.status in (ChannelStatus.CLEARED, ChannelStatus.ABSENT): return # 已证毕:清除/缺席后的 no_signal 与距离无关,不进 PF pf = self.particles[channel] pf.apply_measure(position, outcome.result, outcome.svd_deg) if not pf: if belief.status is ChannelStatus.UNKNOWN: self.belief.mark_absent(channel) self.absence_evidence[channel] = "pf_empty" if channel not in self.pf_absent_channels: self.pf_absent_channels.append(channel) else: self._anomaly("pf_empty_while_detected", channel, position=position) self._apply_count_bound() def _best_certificate(self, channel: int) -> ClearCertificate | None: """凸证书优先;负观测挖洞使 PF 幸存集更紧时用网格证书补位。""" region = self.belief.candidate_set(channel) cert = clear_certificate(region, channel, self.config) \ if region is not None else None if cert is not None and cert.certified: return self._near_clear_target(cert) if self.config.bayes.grid_cert_enabled: pf = self.particles[channel] if len(pf): center, radius = pf.mec() limit = (self.config.field.clear_radius_m - pf.margin_m - self.config.search.clear_margin_m) if radius <= limit: return self._near_clear_target( ClearCertificate(channel, center, radius, limit, True)) return cert def _select_point(self, channel: int) -> Vec2 | None: """定位测量点:期望 log 幸存占比 + λ·移动时间 最小的候选。""" context = self._probe_context(channel) if context is None: return None candidates, info_score, center, position = context bayes = self.config.bayes def score(q: Vec2) -> float: # 一次定位后仍需接近源才能清除;仅计到检测点的移动会偏好绕行。 move = bayes.lambda_move_s * (move_time(position, q, self.config.cost) + move_time(q, center, self.config.cost)) return info_score(q) + move return min(candidates, key=lambda q: (score(q), _point_key(q))) def _localize_and_clear(self, channel: int) -> bool: """阻塞式定位子循环(resolve 阶段安全网):PF 选点,失败退凸选点。""" search = self.config.search self._may_clear.add(channel) for _ in range(search.max_localize_steps): if self._virtual_cap_reached(): return False cert = self._best_certificate(channel) if cert is not None and cert.certified and channel in self._may_clear: if self._attempt_clear(channel, cert.center, cert): self._clear_failed.discard(channel) return True self._clear_failed.add(channel) self._may_clear.discard(channel) continue point = self._select_point(channel) if point is None: region = self.belief.candidate_set(channel) if region is None: return False point = select_measurement_point( region, self.client.state.position, self.config).position outcome = self.client.measure(point, channel) self._record_measure(channel, point, outcome) self._may_clear.add(channel) if outcome.result == NO_SIGNAL: region = self.belief.candidate_set(channel) if region is not None and safe_disk_radius(region, self.config) >= 0 \ and math.dist(point, region.mec()[0]) \ <= safe_disk_radius(region, self.config): self._anomaly("no_signal_in_safe_disk", channel, position=point) cert = self._best_certificate(channel) if cert is not None and cert.certified: if self._attempt_clear(channel, cert.center, cert): self._clear_failed.discard(channel) return True self._clear_failed.add(channel) self._may_clear.discard(channel) return self.belief.channels[channel].status is ChannelStatus.CLEARED \end{lstlisting} \subsection*{问题四:朝向候选、可见性选点与网格遍历清除} 每个幸存格附朝向档掩码构成联合候选集,可见性权重用于探点排序;定位停滞时以 $20$ m 方格遍历清除兜底。 \noindent\small \texttt{JointBelief}:单频道的位置与朝向联合候选集;朝向分 $24$ 档,负观测只在整格必落在最小接收半径内时才约束朝向。 \begin{lstlisting}[style=corecode] class JointBelief: """单频道 (位置 × 朝向) 联合可行集。""" def is_absent(self) -> bool: """联合集空 ⟹ 该频道不存在源(真源保留定理的二维推广)。""" return not self.position.alive def sample(self, size: int = 256) -> list[int]: """均匀抽样幸存格(评分用;证书始终用完整集合)。""" cached = self._sample_cache if cached is not None and cached[0] == self.position.version: return cached[1] alive = sorted(self.position.alive) if len(alive) > size: step = len(alive) / size alive = [alive[int(k * step) % len(alive)] for k in range(size)] self._sample_cache = (self.position.version, alive) return alive def expected_posterior_radius(self, reader: Vec2, size: int | None = None) -> float: """一次测量后的期望后验 MEC 半径(米);D1 值函数。 与 :meth:`ChannelParticles.expected_posterior_radius` 同族,差别只在 no_signal 分支概率:定向源不仅会因距离 > r_eff 静默,还会因朝向遮挡 静默,因此 p_vis(p) = P(r_eff ≥ |p−q|) × cover_ratio(p, q) 其中 ``cover_ratio`` 是该格朝向掩码中"可能覆盖 q 方位"的档位占比。 分支半径语义: - direction(可见,非 near):读数为 q 处的方位 ±δ,后验收缩到该方位 楔形与当前可行集之交,半径取幸存子采样的质心最大距离; - no_signal:定向源的静默**不删空间候选**,位置后验半径保持当前值; - near:后验为 5m 盘。 返回值只用于**选点排序**,不参与任何安全判定;证书仍由凸/网格口径给出。 """ pf = self.position if not pf.alive: return 0.0 if size is None: size = self.config.bayes.score_subsample indices = self.sample(max(2, size)) if not indices: return 0.0 stride = max(len(indices) // max(self.config.bayes.mc_representatives, 1), 1) representatives = indices[::stride] if _np is not None: return self._epr_vector(reader, indices, representatives) return self._epr_python(reader, indices, representatives) # pragma: no cover def prob_detect(self, reader: Vec2, size: int = 256) -> float: """在 ``reader`` 处能看见任一可行 (位置, 朝向) 的概率质量(启发式)。 r_eff ~ U[1000, 1500] 解析积分;朝向按可行档位占比计权。 只用于选点排序,不参与任何安全判定。 """ cells = self.sample(size) if not cells: return 0.0 total = 0.0 span = self.field.r_eff_max_m - self.field.r_eff_min_m for index in cells: gx, gy = self.position.cell_center(index) distance = math.hypot(reader[0] - gx, reader[1] - gy) if distance > self.field.r_eff_max_m: continue if distance <= self.field.r_eff_min_m: p_range = 1.0 else: p_range = (self.field.r_eff_max_m - distance) / span if p_range <= 0.0: continue bearing = math.degrees(math.atan2(reader[1] - gy, reader[0] - gx)) cover = _COVER_TABLE[_table_index(bearing)] bits = bin(self._mask(index) & cover).count("1") if bits: total += p_range * bits / HEADING_BINS return total / len(cells) def apply(self, station: Vec2, result: str, svd_deg: float | None = None) -> None: """按读数更新联合可行集(保守,真源 (位置, 朝向) 永不被删)。""" if result == DIRECTION: self.position.apply_measure(station, DIRECTION, svd_deg, directional=True) self._apply_heading(station, keep=True) elif result == NEAR: # near 同样要求可见(评测机先判朝向再判距离) self.position.apply_measure(station, NEAR, directional=True) self._apply_heading(station, keep=True) elif result == NO_SIGNAL: # 距离阈值必须取**保守侧**:只有"整格都必然落在 r_eff_min 圆盘内" # 时,no_signal 才一定由朝向遮挡解释。格心到格内任一点 ≤ margin, # 故阈值取 r_eff_min − margin;取 + margin 会把 1000~1010m 的模糊带 # 也当成"必须不覆盖",从而可能删掉真源的朝向档(破坏真源保留性)。 self._apply_heading(station, keep=False, within_m=max( 0.0, self.field.r_eff_min_m - self.position.margin_m)) else: raise ValueError(f"未知测向结果: {result!r}") def summary(self) -> dict: """联合集规模快照(写入策略 summary,供审计与论文复算)。""" cells = self.sample(256) bins = [bin(self._mask(index)).count("1") for index in cells] return {"cells": len(self.position), "sampled_mean_heading_bins": (round(sum(bins) / len(bins), 2) if bins else 0.0), "heading_bins_total": HEADING_BINS} \end{lstlisting} \noindent\small \texttt{DirectionalExtension}:问题四闭环:定向覆盖计划、遮挡解释与网格遍历清除兜底。 \begin{lstlisting}[style=corecode] class DirectionalExtension(OmnidirectionalLoop): """问题4混合源闭环(P5):全向基线 + 定向观测处理 + 后备清除。""" def _localize_and_clear(self, channel: int) -> bool: """定位子循环:选点测量收敛候选集,拿到清除证书后清除。 与 P4 的差异:保证接收盘内的 no_signal 说明探点被朝向遮挡 [观测推理],走"对跖复核 -> 环探"升级梯子(不计入定位步数, 由 max_blocked_probes 限流),之后仍未收敛再转后备清除。 """ search = self.config.search self._may_clear.add(channel) steps = 0 while steps < search.max_localize_steps: if self._virtual_cap_reached(): return False region = self.belief.candidate_set(channel) if region is None: return False cert = clear_certificate(region, channel, self.config) if cert.certified and channel in self._may_clear: if self._attempt_clear(channel, cert.center, cert): return True self._may_clear.discard(channel) # 证书失败:补测后才可重试 continue if len(self.blocked_probes.get(channel, [])) >= search.max_blocked_probes: break # 梯子用尽仍被遮挡:定位停滞,转后备清除 point = self._next_probe(region, channel) within_range = (math.dist(point, region.mec()[0]) <= safe_disk_radius(region, self.config)) outcome = self.client.measure(point, channel) self._observe(channel, point, outcome.result, outcome.svd_deg) self._may_clear.add(channel) if outcome.result == NO_SIGNAL: if within_range and channel not in self.directional_observed: self.directional_observed.append(channel) self.blocked_probes.setdefault(channel, []).append(point) continue # 盘外无信号也可能是距离不足,不据此断言定向 self.blocked_probes.pop(channel, None) steps += 1 if self.belief.channels[channel].status is ChannelStatus.CLEARED: return True region = self.belief.candidate_set(channel) if region is None: return False cert = clear_certificate(region, channel, self.config) if cert.certified and channel in self._may_clear: if self._attempt_clear(channel, cert.center, cert): return True self._may_clear.discard(channel) return self._fallback_clear(channel, region) def _observe(self, channel: int, point: Vec2, result: str, svd_deg: float | None) -> None: """同步更新凸候选集与 (位置 × 朝向) 联合可行集。 联合集只在频道首次被看见(direction/near)时建立:从未被看见的 频道由覆盖证明负责缺席,不需要朝向维度。联合集一旦为空,直接给出 该频道的缺席证明(严格,与覆盖证明同一条件),可提前停止对其检测。 """ self.belief.update_measure(channel, point, result, svd_deg) if not self.config.search.directional_joint_belief: return joint = self.joint.get(channel) if joint is None: if result not in (DIRECTION, NEAR): return joint = JointBelief(self.config) self.joint[channel] = joint joint.apply(point, result, svd_deg) if joint.is_absent() and self.belief.channels[channel].status \ is not ChannelStatus.CLEARED: self.belief.mark_absent(channel) self.absence_evidence[channel] = "joint_empty" if channel not in self.joint_absent_channels: self.joint_absent_channels.append(channel) def _next_probe(self, region: CandidateRegion, channel: int) -> Vec2: """定向遮挡下的探点升级梯子 [工程]。 - 未被遮挡过:worst-posterior 选点器(保证接收盘内); - 第 1 次被遮挡:对 MEC 圆心取对跖方向,偏移裁进保证接收盘; - 之后:绕 MEC 圆心、半径 = ρ 的环探,方位自首个被遮挡点起 每次 +45°。几何边界必须如实陈述:环上可见弧没有下界—— 源可位于环上(|g-c| = ρ)且朝向沿外切线,可见集退化为单个 切点,固定步进环探可以全部漏过。梯子只提高提前命中概率, 不构成接收或命中保证;完备性完全由后备清除兜底。 """ visibility = self._visibility_probe(region, channel) if visibility is not None: return visibility blocked = self.blocked_probes.get(channel, []) center, radius = region.mec() if not blocked: return select_measurement_point(region, self.client.state.position, self.config).position safe = safe_disk_radius(region, self.config) first = blocked[0] if len(blocked) == 1 and safe >= 0: dx, dy = center[0] - first[0], center[1] - first[1] norm = math.hypot(dx, dy) if norm <= 1e-9: # 首个被遮挡探点恰在圆心:方向信息为零,沿 +x 走满安全盘。 return (center[0] + safe, center[1]) scale = min(1.0, safe / norm) # 对跖方向,偏移模长 = min(norm, safe):圆心对称点裁进保证接收盘。 return (center[0] + dx * scale, center[1] + dy * scale) base = math.atan2(first[1] - center[1], first[0] - center[0]) azimuth = base + math.radians(45.0 * (len(blocked) - 1)) return (center[0] + radius * math.cos(azimuth), center[1] + radius * math.sin(azimuth)) def _fallback_clear(self, channel: int, region: CandidateRegion) -> bool: """定位停滞的后备:候选集有限圆盘覆盖逐格清除(格数即请求上界)。""" search, session = self.config.search, self.config.session width = max(p[0] for p in region.polygon) - min(p[0] for p in region.polygon) height = max(p[1] for p in region.polygon) - min(p[1] for p in region.polygon) cells = fallback_clear_cells(region.polygon, self.config) entry = {"channel": channel, "cells": len(cells), "bound": fallback_cell_bound(width, height, self.config), "posterior_radius_m": round(region.mec()[1], 3)} self.fallback_log.append(entry) if len(cells) > search.fallback_max_cells: # 宽候选集盲扫违背预算纪律:留待定位收敛,不得硬扫 [计划§4]。 entry["outcome"] = "skipped_cell_cap" self._anomaly("fallback_cell_cap_exceeded", channel, cells=len(cells), cap=search.fallback_max_cells) return False # 现实预算核对:N 次请求 × 单请求高分位估计 + 退出余量,不足不启动。 remaining = self.client.real_remaining_s if remaining is None or len(cells) * session.real_rtt_p95_s \ + session.reserve_exit_s > remaining: entry["outcome"] = "skipped_budget" return False attempts = 0 order = self._fallback_order(cells) for cell in order: if self._virtual_cap_reached(): entry["outcome"] = "skipped_virtual_budget" entry["attempts"] = attempts return False attempts += 1 outcome = self.client.clear(cell, channel) if outcome.result == CLEAR_SUCCESS: self.belief.mark_cleared(channel) self.cleared.append(channel) entry["outcome"] = "cleared" entry["attempts"] = attempts return True # 候选集保守包含真源,格心距格内任一点 <= 10*sqrt(2) < 20m:全部 # 失败说明守恒性被打破,按异常留痕,不得据此删除候选。 entry["outcome"] = "exhausted" entry["attempts"] = attempts self._anomaly("fallback_exhausted_without_success", channel, cells=len(cells)) return False \end{lstlisting} \endgroup