diff --git a/1.md b/1.md
new file mode 100644
index 0000000..ace4cb6
--- /dev/null
+++ b/1.md
@@ -0,0 +1,152 @@
+下面把现有《B题建模分析与实施计划》拆成可执行任务,并逐项检查设计思路。总体结论:**主线设计是对的,四问串联、保守几何、覆盖证明、协议幂等这些方向都合理;但有几个关键点需要补强,尤其是问题2候选区域非空性、定向源朝向不确定性、现实20分钟窗口下的请求数控制,以及正式测试时间安排。**
+
+---
+
+## 一、总体判断
+
+现有方案的最大优点是:
+
+1. **四问没有割裂**:问题1的定位区域和包围圆服务于问题2选点,问题3的覆盖发现与清除框架延伸到问题4。
+2. **坚持保守性**:用有界误差、半平面交、最小包围圆、49点覆盖证明,避免“看起来能清”但实际漏源。
+3. **协议与状态意识强**:强调 `accepted=false` 不更新状态、重试复用 `request_id`、`/clear` 不切换频道、虚拟时间公式。
+4. **停止条件严格**:没有把“发现10个源”或“连续无信号”当成功。
+
+主要风险:
+
+- **问题2的候选区域 \( \mathcal Q \) 可能为空**,需要补非空判断和退化策略。
+- **定向源第4问中 `no_signal` 不能直接删除空间候选点**,现有基线保守但效率低,需明确后备清除的规模和请求预算。
+- **现实20分钟窗口**下,49点×20频道最多980次检测,可能请求数过大;虚拟时间不是瓶颈,网络延迟和程序运行时间才是。
+- **正式测试机会少**,9月13日截止,必须尽早演练并保留正式机会。
+
+---
+
+## 二、任务拆分:可执行清单
+
+建议按下面 P0–P7 拆分,每阶段都有明确交付和验收。
+
+| 阶段 | 任务 | 交付物 | 验收标准 | 优先级 |
+|---|---|---|---|---|
+| P0 规则固定 | 核对题目、附件1/2、协议、计时、日志要求 | 规则表、状态机、字段表 | 计时示例能逐步得到附件给定值;`/clear` 不切换频道;`accepted=false` 不更新 | 最高 |
+| P1 协议基线 | 实现 `/enter`、`/detect`、`/clear`、`/exit`;串行、幂等重试、超时 | 可运行客户端、自建回放测试 | 失败重试不污染状态;虚拟时间与公式一致;能主动退出 | 最高 |
+| P2 几何内核 | 角域半平面表示、半平面交、凸多边形、直径、最小包围圆 | `geometry` 模块、测试用例 | 处理0°/360°跨界、空集/单点/线段/无界;等边三角形反例通过 | 最高 |
+| P3 问题2选点 | 构造 \(K_1\),计算候选区域 \( \mathcal Q\),采样评估后验包围圆 | 第二检测点策略、候选区域算法 | 保证收到信号;\( \mathcal Q\) 为空时有退化方案;near分支处理 | 高 |
+| P4 问题3全向闭环 | 49点覆盖、逐频道覆盖记录、发现—定位—清除—停止 | 全向演练版本、统计脚本 | 10源和16源都能正确退出;边界朝外不漏;停止条件正确 | 最高 |
+| P5 问题4定向扩展 | 定向源覆盖证明、失去信号处理、20m圆盘后备清除 | 定向演练版本 | 边界朝外定向源能发现;失去信号后能清除;不误删真源 | 高 |
+| P6 加速与消融 | 共享检测、自适应覆盖、目标排序、提前清除 | 消融实验记录 | 全清不退化;虚拟时间/请求数有改善证据 | 中 |
+| P7 正式与论文 | 演练、正式各3次、日志原名、表1、论文 | 6次正式记录、原始日志 | 案例编码、程序版本、统计一致;日志上传成功 | 最高 |
+
+时间紧时,**P6 可删减,P0–P5 必须保留**。
+
+---
+
+## 三、设计思路逐项检查
+
+| 设计点 | 判断 | 需要补强/注意 |
+|---|---|---|
+| 四问统一框架 | 合理 | 问题1和2的几何结果要直接复用到3、4,避免重复建模 |
+| 有界误差±1°、示向度两位小数 | 正确 | 实现可加±1.005°保守裕量,但不能写成题面规定 |
+| 同地点误差固定 | 正确 | 不能原地重复测量取平均 |
+| 角域半平面交 | 正确 | 必须处理0°/360°跨界;区分空集、单点、线段、有限多边形、无界 |
+| 直径枚举 | 正确 | 小规模枚举顶点对即可;无界区域不能截断后声称直径 |
+| 以直径为直径的圆覆盖区域 | **不能保证** | 等边三角形反例正确;保证清除要用最小包围圆 \( \rho(K)\le20 \) |
+| 问题2候选区域 \( \mathcal Q \) | 方向正确 | \(K_1\) 应加 \( \Omega \) 和 \(B(s_1,1500)\) 约束;\( \mathcal Q\) 可能为空,需退化策略 |
+| 外包圆候选 \(B(c,1000-r)\) | 正确 | 仅当 \(r<1000\) 时非空;若 \(r\) 接近1000,区域很小 |
+| “候选点距第一次检测点不超过1000米” | 错误表述 | 计划已指出,正确是“到 \(K_1\) 中任意可能位置距离不超过1000米” |
+| 49点覆盖 | 证明成立 | 需实现网格边/顶点规则;全向和定向都适用;全 no_signal 可证明该频道不存在 |
+| 49点扫描规模 | 估算合理 | 虚拟时间约3.665小时,但现实20分钟受请求延迟约束 |
+| 定向源 `no_signal` | 不能直接删除空间候选点 | 基线可只记录覆盖,不删点;加速版再维护朝向可行集合 |
+| 定向源后备清除 | 思路可靠 | 候选集合有界后,用边长20m小方格中心,中心到格内点≤10√2<20,逐点 `/clear` |
+| 停止条件 | 严格正确 | 清除16个不同频道,或每频道清除成功/完整覆盖证明不存在 |
+| `/clear` 不切换频道 | 正确 | 清除后当前测向频道不变,影响下一次切换耗时 |
+| 协议幂等 | 正确 | 重试同 `request_id` 和完全相同内容;`accepted=false` 不更新状态 |
+| 现实预算 | 关键风险 | 用 `remaining_real_duration_s` 管理;预留退出和故障恢复;日志≤2MB |
+| 正式测试 | 时间紧 | 问题3和4各3次;建议9月13日15:30前完成,17:30后不能启动新测试 |
+
+---
+
+## 四、重点补强建议
+
+### 1. 问题2:候选区域必须明确非空处理
+
+建议把第一次测向后的候选集合写清楚:
+
+\[
+K_1=\Omega \cap W(s_1,\theta_1\pm\delta)\cap B(s_1,1500)
+\]
+
+其中 \(W\) 是角域,\(\delta\) 取 \(1.005^\circ\) 保守裕量。
+然后:
+
+\[
+\mathcal Q=\{q:\max_{g\in K_1}\|q-g\|\le1000\}
+\]
+
+若 \( \mathcal Q\neq\varnothing \),在其中采样选点,评估最坏后验包围圆半径和移动成本。
+若 \( \mathcal Q=\varnothing \),则退化为:
+
+\[
+q^*=\arg\min_q \max_{g\in K_1}\|q-g\|
+\]
+
+即选一个使最坏接收距离最小的点,同时结合交会角筛选。
+若 \(K_1\) 的外包圆为 \(B(c,r)\) 且 \(r<1000\),则 \(B(c,1000-r)\) 是更简单的充分候选区域。
+
+### 2. 问题4:定向源不要过早依赖 `no_signal` 删点
+
+第4问中,`no_signal` 只说明“当前位置、接收半径、朝向”联合假设下不可见,不能单独排除位置。
+基线策略建议:
+
+- 用49点覆盖保证发现;
+- 发现后主动测向缩小候选;
+- 若失去信号,不再强行靠 `direction` 追踪,而是对当前有界候选集合做20m圆盘覆盖,逐点 `/clear`;
+- 若时间允许,再维护朝向可行集合,减少无效检测。
+
+### 3. 现实20分钟:请求数比虚拟时间更关键
+
+49点×20频道最多980次检测,若每次网络延迟0.5–1秒,现实时间可能超过8–16分钟。
+建议:
+
+- 已清除频道不再检测;
+- 已发现待定位频道可暂停覆盖扫描;
+- 共享检测位置:到一个点后顺便测多个未知频道;
+- 自适应覆盖:根据 no_signal 覆盖记录跳点,但必须保持证明完整;
+- 正式测试前实测接口延迟、日志大小、总请求数。
+
+### 4. 问题1:直径圆覆盖判断可给更明确算法
+
+对凸多边形 \(P\):
+
+- 先求直径 \(D=\max_{i,j}\|v_i-v_j\|\);
+- 枚举所有顶点对作为候选直径,检查其余顶点是否都在以该对为直径的圆内;
+- 若存在一个直径对使所有顶点在圆内,则覆盖;否则不覆盖;
+- 但保证清除时不要用直径圆,要用最小包围圆 \( \rho(K)\le20 \)。
+
+---
+
+## 五、建议的最小可行路线
+
+如果时间不足,按这个顺序做:
+
+1. **P0+P1**:协议、计时、状态更新、幂等重试。
+2. **P2**:角域交、直径、最小包围圆。
+3. **P3**:问题2候选区域和选点。
+4. **P4**:问题3全向闭环,49点覆盖,发现—定位—清除—停止。
+5. **P5**:问题4定向扩展,后备清除。
+6. **P7**:演练后尽早正式测试。
+
+可删减:P6中的复杂调度、朝向联合维护、学习型策略。
+不可删减:覆盖证明、停止条件、协议幂等、日志、正式测试。
+
+---
+
+## 六、结论
+
+现有设计思路**整体可行且偏保守**,适合竞赛中“先保证全清,再优化时间”的目标。
+最需要立即补强的是:
+
+1. 问题2中 \(K_1\) 和 \( \mathcal Q\) 的严格定义及空集处理;
+2. 问题4中定向源朝向不确定性和 `no_signal` 的正确使用;
+3. 现实20分钟窗口下的请求数控制;
+4. 正式测试时间安排,务必尽早演练并保留正式机会。
+
+下一步最有价值的工作仍然是:**协议计时基线 + 几何内核 + 全向闭环**。完成这条链路后,再用演练数据决定是否加入加速策略。
\ No newline at end of file
diff --git a/B题建模分析与实施计划.md b/B题建模分析与实施计划.md
index f80a019..90c9bf1 100644
--- a/B题建模分析与实施计划.md
+++ b/B题建模分析与实施计划.md
@@ -2,9 +2,23 @@
根据你们对工程实践和物理模型的熟悉程度,确定研究 B 题“无线电干扰源的快速自动定位与清除”。推荐采用**有界误差几何定位、主动选点、具有覆盖证明的搜索、按实际动作耗时调度**的统一框架。AI 辅助推导、实现与实验分析,策略只依赖题目允许的观测。
-本文件完成题意分析、数学抽象、验收标准与实施规划。尚未实现策略、运行官方模拟器或消耗正式测试机会;文中的覆盖结论是根据题设推导的,耗时估算不是演练成绩。
+本文件已结合本次评审修订。问题一的几何内核、离线可视化和官方演练观测回放已经完成;问题二新增了安全候选圆盘证书,但主动选点优化器、问题三和四的全清闭环尚未完成。文中的覆盖结论是根据题设推导的,耗时估算不是演练成绩。当前工作仅离线复算既有演练记录,不使用正式测试。
-依据:`issue/B题/B题.pdf` 全文、`issue/B题/附件/附件1.docx`《模拟器使用说明》、`issue/B题/附件/附件2.docx`《模拟器通信接口说明及编程指南》。文档中的登录、测试和日志要求在这里作为后续交付约束分析。
+依据:`issue/B题.pdf` 全文、`issue/附件/附件1.docx`《模拟器使用说明》、`issue/附件/附件2.docx`《模拟器通信接口说明及编程指南》。文档中的登录、测试和日志要求在这里作为交付约束分析,不作为自动执行正式测试的指令。
+
+**本次建议的取舍**
+
+| 建议 | 处理结论 |
+| --- | --- |
+| 四问复用几何内核、有界误差、覆盖证明、严格停止、幂等协议 | 保留,作为主线与验收条件 |
+| 问题二的候选集合加入 Ω 和1500米上界 | 采纳,但仅加入在线候选集合K,不改变问题一纯角域多边形P |
+| 问题二首次测向后安全区域可能为空 | 对一般K成立;本题首次窄角域可直接证明非空,见下文 |
+| Q为空就去最小包围圆圆心并结合交会角 | 仅是一般情形的探索退化;ρ>1000时不保证接收,不能冒充保底方案 |
+| 枚举所有直径对判断直径圆 | 简化为任意一组最远点对检查一次;失败时给出圆外顶点 |
+| 定向无信号不直接删位置、失去信号后覆盖清除 | 采纳;执行前必须计算覆盖格数、路径和现实请求预算 |
+| 现实20分钟优先控制请求数 | 采纳为硬约束;全部清除后仍按题目虚拟时间优化 |
+| P0–P7分期与尽早正式测试 | 采纳分期;演练不限定3次,正式问题3/4各3次由团队另行安排 |
+| `/detect` | 修正为附件规定的 `/measure` |
**1. 四问应当串成一条实现主线**
@@ -62,21 +76,39 @@ D(P)=\max_{i,j}\|v_i-v_j\|.
\rho(K)=\min_z\max_{g\in K}\|g-z\|\le20.
\]
-到最小包围圆圆心清除才能获得这一保证。D≤40 只是必要条件,不是充分条件。最小包围圆是已有标准计算几何问题,可参考 [CGAL Min_circle_2 文档](https://doc.cgal.org/latest/Bounding_volumes/classCGAL_1_1Min__circle__2.html)。实现时给 20 米阈值保留数值余量。
+到最小包围圆圆心清除是充分选择,并非唯一选择。所有可保证清除的位置组成 C₂₀(K)=∩_{g∈K}B(g,20);只要该集合非空,就可进一步选择距离机器狗最近的可行点来节省移动。D≤40只是必要条件,不是充分条件。最小包围圆是已有标准计算几何问题,可参考 [CGAL Min_circle_2 文档](https://doc.cgal.org/latest/Bounding_volumes/classCGAL_1_1Min__circle__2.html)。实现时给20米阈值保留数值余量;数值阈值附近返回谨慎判断,不把浮点容差当作精确算术证明。
+
+直径圆覆盖只需任意一组最远顶点A、B:包含距离D的两点且半径为D/2的圆,其圆心只能是(A+B)/2。因此检查一次全部顶点即可;若失败,换另一组直径对也不可能产生覆盖圆。问题一实现现输出最坏超出距离、圆外顶点索引和一个具体反例顶点。
在线定位还可用 Ω 和“收到信号意味着距离≤1500”约束 K。圆盘交角域可能包含曲线边界;若用多边形近似,必须构造外包近似,才能维持“不丢失真源”的保证。用圆周采样点连成的内接多边形不能直接作为保守外包。
第 2 问建议先明确“较好的定位效果”:以新观测后的最坏包围圆半径为主要几何指标,再考虑移动和检测成本。交会角大、面积小可作为候选点筛选指标,但不是对所有未知位置都最优的证明。
-第一次测向得到 K₁ 后,对全向源,一个保守且可以说明的第二检测点候选区域为:
+第一次收到direction后,明确取K₁=Ω∩W(s₁,θ₁±δ)∩B(s₁,1500)。保留整个距离圆盘是保守处理,忽略direction还隐含的近距离排除不会丢失真源。对全向源,第二检测点的安全候选区域为:
\[
\mathcal Q=\{q:\max_{g\in K_1}\|q-g\|\le1000\}.
\]
-在这里测量,不论实际目标位于 K₁ 何处,接收距离都满足题目最小接收半径;可能返回 direction,也可能返回 near。若已知外包圆 K₁⊆B(c,r),且 r<1000,则 B(c,1000−r) 是更容易构造的充分候选区域。不要误写成“候选点距第一次检测点不超过1000米”。
+在这里测量,不论实际目标位于K₁何处,接收距离都满足题目最小接收半径;可能返回direction,也可能返回near。若已知外包圆K₁⊆B(c,r),且r≤1000,则B(c,1000−r)是充分候选区域,等号时是单点。不要误写成“距第一次检测点不超过1000米”。宽松外包圆的r>1000不能证明真实Q为空。
-在候选区域内,围绕当前测向方向的两侧取有限候选点,分别评估可能观测下的区域收缩和动作耗时。最坏情形目标可写为 sup ρ(K₁∩W(q,θ′)),其中 θ′遍历与 K₁和误差界一致的观测,near 分支单独处理。稀疏采样评估是启发式,需要通过加密采样和误差边界案例验证,不能标成严格最坏情形最优解。
+本题还有比“检查Q是否为空”更强的解析结论。令R=1500,u为第一次示向方向,δ≤45°,定义
+
+\[
+a=\frac{R}{2\cos\delta},\qquad c=s_1+a u.
+\]
+
+整个截断角域W∩B(s₁,R)包含在B(c,a)内。证明:任意候选g=s₁+t uφ满足0≤t≤R、|φ−θ₁|≤δ,因此
+
+\[
+\|g-c\|^2=t^2+a^2-2at\cos(\varphi-\theta_1).
+\]
+
+该式关于t是凸函数,最大值在0或R取得;两端都不超过a²。于是K₁⊆B(c,a),从而B(c,1000−a)⊆Q。δ=1.005°时,a≈750.115391米,安全圆盘半径约249.884609米,严格为正;题设δ=1°同样成立。即使检测点或c在Ω外也合法,因为机器狗不受源位置圆域限制。实现见 `geometry/planning.py::first_bearing_candidate_disk`。
+
+因此,在本题一致的首次测向条件下无需依赖“Q空集退化”。对后续更一般的非空紧集K,Q非空当且仅当ρ(K)≤1000;若确实ρ>1000,去MEC圆心只是最小化最坏接收距离的探索动作,必须允许no_signal。若K为空,应先核查频道、数据、误差界与几何计算,不给空集编造中心。上述证书只保证距离;不能直接用于定向源的朝向可见性保证。
+
+在上述安全圆盘内,围绕测向轴两侧取候选点,计算后验包围半径与动作代价。最坏情形目标可写为sup ρ(K₁∩W(q,θ′)),θ′遍历与K₁和误差界一致的读数;实现先用目标位置与误差区间端点采样,并加密检查。若返回near,则候选源位于当前点5米以内,可直接在当前位置尝试清除该频道。交会角仅作筛选或同分择优,不为改善交会角越出已证明安全的候选区域。采样是启发式,不能称为严格最坏情形最优解;圆弧先验如需离散,使用外包切线多边形并记录逼近误差。
**4. 搜索完整性和停止条件要有可验证的依据**
@@ -96,7 +128,9 @@ S=\{(700i,700j):i,j=-3,-2,\ldots,3\},\quad |S|=49.
作为规模检查,49点全部扫描20频道最多需要980次检测。若从原点去网格一角,再按逐行折返路线扫完,纯扫描路程约36569.85米;每点检测成本上界120秒,纯扫描虚拟总耗时上界约13193.97秒,即3.665小时。**该估算不含定位、清除及其绕行,也不保证满足现实20分钟窗口。**它说明覆盖基线在100小时虚拟上限内有设计空间,现实可行性仍要测接口延迟、总请求数和日志大小。
-发现源后,先通过主动测向缩小候选集合。若定向源再次失去信号,可对已知有界候选集合构造半径小于20米的有限圆盘覆盖,逐点调用 /clear,作为不依赖朝向的后备清除方法。例如边长20米的小方格中心距格内任何点不超过10√2米;保留与候选集合相交的格子便可得到保守覆盖。该后备方法保证几何上的有限可清除性,耗时和请求预算须在演练中验证,不宜作为默认追踪策略。
+发现源后,先通过主动测向缩小候选集合。若定向源再次失去信号,可对已知有界候选集合构造半径小于20米的有限圆盘覆盖,逐点调用/clear,作为不依赖朝向的后备清除方法。边长20米方格中心距格内任何点不超过10√2米;保留与候选集合相交的格子即可保守覆盖。格子按统一半开区间索引,源恰在网格线上时至少归入一个格子;若相交判定数值不确定则保留格子。
+
+有限不等于现实预算可行。设候选外包框宽w、高h,使用全局20米网格枚举时,上界可取N≤(⌈w/20⌉+2)(⌈h/20⌉+2),再以实际相交格子列表得到精确请求上限。生成路径长度L后,单源全失败清除耗时为L/5+3N;若最后一次成功则为L/5+3(N−1)+5。现实代价用N乘实测单请求高分位耗时加本地计算时间估计。不满足剩余预算时应继续有希望的定位、选择其它任务或按预算退出,不能声称后备清除保证20分钟内成功。即使已知源落在半径1500米圆盘,按外包框枚举仍有约2.3万个格子的量级,故宽候选集合不能直接盲扫。
停止成功的依据为以下任一项:
@@ -139,7 +173,9 @@ AI 的适合用法是把这些模块逐一变成可测试程序、生成边界
协议必须同时检查 HTTP 状态和 accepted。accepted=false 时不得更新状态,返回的 virtual_time_s=0 不能重置本地虚拟时钟。网络超时重试同一动作必须复用原 request_id 和完全相同的内容;处理重试响应也不能重复累加本地成功数与费用。新动作使用新ID,不并发调用不同动作。
-用 /enter 返回的 remaining_real_duration_s 管理本局现实预算,预留退出与故障恢复余量。根据附件,正式日志上传要求文件不超过2MB;接口延迟、日志增长和异常恢复都要在演练中实测。
+用/enter返回的remaining_real_duration_s建立本地单调时钟截止时间;后续响应若提供剩余时间则校准,不能把首次剩余时间当成永远不变。记录每次请求的现实往返耗时,预留退出与故障恢复余量。例如先预留60秒,再由演练高分位延迟调节;60秒是初始工程参数,不是官方规定。估计剩余请求数时同时计入测量、失败重试、清除和退出,禁止并发动作。
+
+980次检测在单次0.5–1秒的假设下占490–980秒(约8.17–16.33分钟),还没有定位清除余量。这个延迟范围不是实测结论;本地接口和模拟器计算的实际耗时应测量。当前14个动作的单次短演练不足以估计高分位延迟或全程稳定性。已清除频道跳过,已发现频道暂停覆盖并进入定位队列,移动到同一点后共享频道测量;自适应跳点必须保留逐频道覆盖证据。根据附件,正式上传日志不超过2MB,需监测官方日志体积;自建JSON大小不能冒充加密日志的大小。
基准实验至少覆盖:全向与混合源;数量10和16;接收半径1000和1500米;目标贴近圆域边界且朝外;测向接近0°/360°;误差取界值;检测点距离5米及清除距离20米的边界;重复请求、拒绝动作及短暂断连。样例数量按剩余时间和运行成本安排,不把有限样例通过等同于数学保证。
@@ -147,19 +183,20 @@ AI 的适合用法是把这些模块逐一变成可测试程序、生成边界
**7. 建议实施次序与阶段交付**
-以下工期是规划估计,不是已经完成的工作。依据附件中的北京时间截止安排,先完成可演练版本,再优化。
+下面按P0–P7列出可执行交付与当前状态。排期以验收门槛推进,避免把“几何代码完成”误认为“全清策略完成”。
-| 阶段 | 建议投入 | 具体工作 | 进入下一阶段的条件 |
+| 阶段 | 交付与执行任务 | 验收门槛 | 当前状态 |
| --- | --- | --- | --- |
-| P0 规则固定 | 1–2小时 | 整理规则表、模块接口、日志字段;核实模拟器可运行 | 成本口径和状态更新规则一致 |
-| P1 工程基线 | 2–4小时 | 实现4类请求、幂等重试、计时回放和自建测试环境 | 199秒示例一致,失败与重试不污染状态 |
-| P2 几何内核 | 4–6小时 | 完成问题1,实现角域交、包围圆、问题2候选点 | 退化几何和误差边界检查通过 |
-| P3 全向闭环 | 4–6小时 | 覆盖发现、局部定位、清除、停止、统计 | 问题3演练可完整退出,输出可追溯 |
-| P4 定向扩展 | 4–6小时 | 验证49点覆盖;实现失去信号处理与后备清除 | 问题4边界朝外案例和演练通过 |
-| P5 加速与定稿 | 4–8小时 | 做共享测量、目标排序等消融,固定参数和版本 | 性能改善有证据,完整性标准不退化 |
-| P6 正式与论文 | 单独留出半天以上 | 分别完成3次正式测试、核查上传、导出原始日志、完成论文表格 | 6次记录、案例编码和交付材料一致 |
+| P0 规则固定 | 规则表、状态机、四类API字段与成本 | 示例逐步105/111/194/199秒;clear不切换频道 | 已核对规则,完整状态机待工程化 |
+| P1 协议基线 | enter/measure/clear/exit客户端;串行、幂等、截止时间 | accepted=false和超时重试不污染状态;主动退出;故障注入回放 | 已有少量测向采集器及真实计时回放;完整客户端待实现 |
+| P2 几何内核 | 问题一解答、区域分类、直径圆证据、MEC、清除余量 | 跨界、退化、无界、解析反例、独立算法对照 | 已实现并优化,20项自动化测试通过 |
+| P3 问题二选点 | K₁先验、安全候选盘、后验半径采样与near分支 | 距离证书有效;采样加密;不把探索当接收保证 | 解析非空证明及证书已实现;选点器与实验待实现 |
+| P4 全向闭环 | 49点覆盖记录、定位队列、清除、停止、日志统计 | 10/16源、边界源和错误响应案例;官方演练全清并正常退出 | 待实现 |
+| P5 定向扩展 | no_signal联合约束、覆盖发现、有限清除后备与预算 | 朝外边界源不漏;真源不误删;格数与退出预算检查 | 已有证明和设计;闭环待实现 |
+| P6 加速消融 | 共享测点、自适应覆盖、目标排序、提前清除 | 配对案例全清不退化,时间/请求数改善可复算 | 可删减;复杂学习与朝向搜索后置 |
+| P7 正式与论文 | 固定版本、论文表1、日志原名、上传核查 | 问题3/4各3份正式记录与程序版本对应 | 团队后续安排;本任务不执行正式测试 |
-按当前日期2026年9月10日,建议9月10日完成P0/P1并启动P2,9月11日完成主要几何与问题3闭环,9月12日完成问题4与主要演练;稳定后即可安排正式测试,不把全部正式机会留到最后。附件建议9月13日15:30前完成正式测试,17:30后不能启动新的演练或正式测试;已进入上传队列的日志仍会继续上传。这里引用的是所提供附件的规则,运行前还应核对模拟器界面公告。
+按2026年9月10日起的剩余时间,建议10日补齐P1并衔接P3,11日完成P4自建边界案例与全向演练,12日完成P5及混合源演练;未达验收就删减P6并优先修正闭环。演练不限3次,正式机会为问题3和4各3次。团队在版本稳定后自行安排正式记录,留出失败恢复和论文整理时间。附件建议9月13日15:30前完成正式测试,17:30后不能启动新的演练或正式测试;已进入上传队列的日志仍会继续上传。此处引用所给附件安排,运行前核对模拟器界面公告;这份排期不改变当前禁止使用正式测试的限制。
若时间不足,优先删减加速策略,保留可解释定位、覆盖证明、可靠协议和完整日志。开发组织可按几何算法、工程接口、实验论文分工,具体人员由团队安排。
@@ -167,6 +204,10 @@ AI 的适合用法是把这些模块逐一变成可测试程序、生成边界
**8. 当前已落实的结论与下一步**
-已完整阅读 B 题四页和两份附件,确认四问衔接、协议动作、计时、日志及正式测试要求;推导了49点发现覆盖方案,独立复算了纯扫描规模与虚拟时间上界。另对497160个位置与朝向组合进行了辅助数值校核,含圆域边界、网格顶点和随机内部位置,未发现覆盖遗漏;有限采样只辅助检查上述证明,不代替证明。尚未验证官方服务延迟、误差场的具体实现或任何策略成绩。
+已完成题面与附件核对、49点覆盖推导及扫描成本复算;此前497160个位置与朝向组合的辅助数值校核未发现覆盖遗漏,但采样不代替证明。问题一已通过官方演练三点观测离线验证;该次只有enter、measure、exit,没有清除动作,不能当作全清策略成绩。
-下一步最有价值的实现是 P1 的协议与计时基线,以及 P2 的角域交与包围圆内核。完成它们后,先交付一条“发现—定位—清除—证明完成—退出”的可运行链路,再用演练数据决定加速方向。
+本次将MEC从全支撑点枚举改为带固定随机种子的Welzl增量算法,保留原枚举法作独立对照;对外输出增加直径圆反例顶点、超出距离和20米清除余量。CLI可显式追加0.005°舍入裕量。真实演练默认δ=1°时D=23.996723米、ρ=12.068583米;δ=1.005°时D=24.119088米、ρ=12.130377米,两种口径下直径圆都不覆盖,但MEC半径都小于20米。原始演练记录未改写。
+
+可复现报告见 `output/q1/optimization_report.json`,问题一证明、输入输出及数值限制见 `docs/问题一解答.md`。局部MEC基准只反映本地几何计算,不能据此宣称请求延迟、完整策略或官方成绩得到同样提升。
+
+下一步优先补齐P1完整协议与故障回放,再实现P3选点器并接入P4“发现—定位—清除—证明完成—退出”闭环。P2已经具备可复用输入输出;不要重复开发几何内核或提前堆叠学习算法。以全向演练的实际请求、日志和耗时决定P5后备规模与P6加速投入。
diff --git a/README.md b/README.md
index e69de29..3f87e42 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1,29 @@
+# B题问题一 交会定位
+
+给定同一干扰源的检测点坐标及带±1°误差的示向度,计算前向角域交集、定位区域直径、直径圆覆盖判断与最小包围圆。
+
+- [完整中文解答与算法证明](docs/问题一解答.md)
+- [修订后的四问思路与P0–P7计划](B题建模分析与实施计划.md)
+- [核心代码](geometry/localization.py)
+- [离线交互模拟](output/q1/question1.html)
+- [官方演练观测与原始记录](output/q1/practice/)
+- [优化与舍入敏感性报告](output/q1/optimization_report.json)
+
+Python3.10及以上,核心算法和演示生成无需安装第三方库。
+
+```powershell
+python -m geometry examples/q1/triangle.json
+python main.py examples/q1/normal.json -o output/q1/result.json
+python -m geometry examples/q1/normal.json --rounding-margin-deg 0.005
+python -m unittest discover -s tests -v
+python scripts/build_q1_demo.py
+python scripts/benchmark_q1.py
+```
+
+用浏览器打开 `output/q1/question1.html`。可选择合成反例或官方演练,逐个加入测点,切换全景与定位区域放大。
+
+输出新增直径圆覆盖反例顶点、最大超出距离和20米清除余量;默认仍为题设±1°,舍入裕量须显式启用。最小包围圆使用可复现的随机增量算法,原枚举法保留作独立对照。`geometry.first_bearing_candidate_disk` 提供问题二首次测向安全区域的解析证书;问题二选点优化器及问题三、四的全清闭环仍待实现。
+
+可视化事件检查使用Node.js:`node scripts/verify_visualization.cjs`。可选PNG重绘使用Pillow及Windows微软雅黑字体:`python scripts/render_q1_figures.py`;这些只用于图形校核,不是核心求解依赖。
+
+本项目只使用官方**演练**采集了问题一所需观测,未使用正式测试。采集器不会启动测试,必须先在界面确认“问题3演练”、接口已就绪,再提供 `--practice-confirmed` 和本局案例编码。参见完整解答中的采集说明。
diff --git a/docs/问题一解答.md b/docs/问题一解答.md
new file mode 100644
index 0000000..67d91ad
--- /dev/null
+++ b/docs/问题一解答.md
@@ -0,0 +1,249 @@
+# B题问题一 交会定位区域的直径与圆覆盖
+
+问题一可以转化为二维半平面交问题:每次带误差的示向度给出一个前向角域,多次测量的交集就是定位区域。对于非空有界定位多边形,直径等于最远顶点对的距离。**以这个直径为直径的圆不一定覆盖定位区域。**判断是否能覆盖,应检验最远顶点对中点到所有顶点的距离,或直接计算最小包围圆。
+
+代码、合成算例与官方演练观测均已实现和保存。本解答只解决问题一;在官方问题3演练中采集同一频道的三个检测点,用于检验交会计算,没有执行全域搜索清除任务,未使用正式测试。
+
+**一 题目假设和模型边界**
+
+检测点为 \(S_i=(s_{ix},s_{iy})\),测得示向度为 \(\theta_i\),误差半角为 \(\delta=1^\circ\)。角度从正东开始逆时针计算。同一频道对应同一干扰源;位置不变时重复检测不会消除误差。
+
+本问求附录2交会法所定义的多边形,因此主模型只取前向角域之交,不额外加入1800米圆域或1500米接收上界。这些先验可用于后续搜索定位,但加入圆盘后边界可能包含圆弧,不能仍按原问题的纯多边形处理。本程序也不以图像视窗作为真实边界。
+
+模拟器示向度保留两位小数,正文规定误差在±1°内。本次主计算使用±1°;如需把独立舍入误差也纳入保守预算,可用 `--rounding-margin-deg 0.005` 显式追加半角,输出会记录实际误差半角。这是额外的工程裕量,不是题面规定。
+
+**二 从测向角域得到线性约束**
+
+设二维叉积 \([a,b]=a_xb_y-a_yb_x\),令
+
+\[
+u_i^-=(\cos(\theta_i-\delta),\sin(\theta_i-\delta)),\qquad
+u_i^+=(\cos(\theta_i+\delta),\sin(\theta_i+\delta)).
+\]
+
+未知源位置 \(X=(x,y)\) 必须满足
+
+\[
+[u_i^-,X-S_i]\ge0,\qquad [u_i^+,X-S_i]\le0.
+\]
+
+这两条不等式选择两条边界射线之间的前向角域,而不是无限延伸的两条直线。写为标准形式 \(a_jx+b_jy\le c_j\):
+
+\[
+\begin{aligned}
+\sin(\theta_i-\delta)x-\cos(\theta_i-\delta)y
+&\le \sin(\theta_i-\delta)s_{ix}-\cos(\theta_i-\delta)s_{iy},\\
+-\sin(\theta_i+\delta)x+\cos(\theta_i+\delta)y
+&\le-\sin(\theta_i+\delta)s_{ix}+\cos(\theta_i+\delta)s_{iy}.
+\end{aligned}
+\]
+
+程序另外保留 \(u_i\cdot(X-S_i)\ge0\) 的前向约束。当 \(0<\delta<90^\circ\) 时它是冗余的,但可让 \(\delta=0\) 的扩展情形正确表示射线,而不是整条直线。实现支持0≤δ<90°,题目算例始终使用1°。
+
+定位区域为
+
+\[
+P=\bigcap_{i=1}^{n}W_i=\{X:AX\le b\}.
+\]
+
+通过向量而不是简单的角度大小比较处理约束,可自然处理359°与0°跨界问题。
+
+**三 可复现的算法**
+
+1. 验证输入为有限数值,将检测点平移至其均值附近并按尺度归一化,减小大坐标带来的抵消误差。
+2. 构造所有半平面;枚举每对不平行边界直线的交点。
+3. 只保留满足全部不等式的交点;去重后用单调链算法求凸包,并保留点、线段等退化结果。
+4. 判断有无非零方向 \(d\) 满足 \(Ad\le0\)。若存在,任一可行点沿该方向无限前进仍可行,区域无界,直径为无穷大。
+5. 对有界非空区域,枚举凸包顶点对,得到直径及一对最远顶点。
+6. 用任意一组最远顶点对中点与半径D/2检验圆覆盖;失败时返回圆外顶点及最大超出距离。用Welzl随机增量法求最小包围圆,输出20米清除半径余量,以区分“直径圆覆盖”和“保证清除”。
+
+非空前向角域的交集不会包含一条双向无限直线,即它是尖的多面集。这保证非空区域存在极点,所以在本模型中可以通过是否存在可行边界交点来判空。无观测时需单独返回整个平面无界,不能依据“没有顶点”误判为空集。
+
+二维非平凡衰退锥若存在,至少有一条边界方向与某个约束边界平行。因此检查每条边界的两个切向方向是否满足 \(Ad\le0\),即可判定无界性。无界情形中输出的有限顶点仅是区域的极点,不构成完整有限多边形。
+
+伪代码如下:
+
+```text
+输入同一频道的测量列表 M
+若 M 为空:返回无界
+H ← 将每次测量转换为前向角域的半平面约束
+V ← 所有边界对交点中满足 H 的点
+若 V 为空:返回空集
+若存在非零 d 满足 H 的齐次约束 A d ≤ 0:返回无界
+V ← 去重并排序为凸包
+(D, A, B) ← V 中的最远顶点对
+O ← (A+B)/2
+覆盖成立 ← max_{v∈V} ||v−O|| ≤ D/2
+返回 V、D、A、B、O、覆盖判断和最小包围圆
+```
+
+设半平面数为k(本实现k=3n),最终顶点数为m。枚举交点并检验全部约束需要O(k³),直径计算O(m²),无界性检查O(k²)。可行交点排序的代价上界为O(k² log k),临时交点存储上界O(k²)。新增直径圆证据只需O(m)遍历。
+
+最小包围圆改为Welzl随机增量算法:先打乱点顺序;已有圆包含新点则保留,否则强制新点成为边界点并解更小子问题;二维最多需要3个支撑点。3个强制边界点必须用外接圆,不能误用这3点的无约束最小圆。实现以显式栈代替递归,防止大量点超过Python递归深度。固定随机种子使输出可复现,理论期望线性指随机排列下的期望,不是该固定种子的最坏复杂度保证。对外函数内部也做坐标归一化,最后重新计算所有点到输出圆心的最大距离,保持数值包含性。旧O(m⁴)支撑点枚举保留为独立测试基准。
+
+半平面交与直径仍采用容易审查的小规模算法,未为少量测点引入更复杂的旋转卡壳。大规模在线数据可后续替换半平面交,不能把MEC局部提速等同于整个求解器线性复杂度。
+
+**四 为什么只枚举顶点对就能得到直径**
+
+有界凸多边形内任意两点可表示为顶点的凸组合:
+
+\[
+x=\sum_i\lambda_i v_i,\quad y=\sum_j\mu_jv_j,\quad
+\lambda_i,\mu_j\ge0,\quad\sum_i\lambda_i=\sum_j\mu_j=1.
+\]
+
+由三角不等式,
+
+\[
+\|x-y\|=\left\|\sum_{i,j}\lambda_i\mu_j(v_i-v_j)\right\|
+\le\sum_{i,j}\lambda_i\mu_j\|v_i-v_j\|
+\le\max_{i,j}\|v_i-v_j\|.
+\]
+
+最远顶点对本身又属于区域,因此上界可以达到:
+
+\[
+\boxed{D(P)=\max_{i,j}\|v_i-v_j\|.}
+\]
+
+点的直径为0,线段的直径为端点距离。对空集,本程序约定直径未定义,输出null;对无界区域,输出 `diameter_infinite=true`,不把Infinity写入不兼容标准的JSON数值。
+
+**五 直径圆能否覆盖的充分必要条件**
+
+取任意最远顶点对A、B,距离为D,令O=(A+B)/2。如果半径D/2的某个圆能够同时包含A和B,则
+
+\[
+D=\|A-B\|\le\|A-O'\|+\|O'-B\|\le D.
+\]
+
+两个不等式只能同时取等号,因此其圆心只能是O。又因为圆盘是凸集,包含全部顶点等价于包含整个多边形。所以
+
+\[
+\boxed{\text{存在直径为D的覆盖圆}\iff
+\max_{v\in V}\|v-(A+B)/2\|\le D/2.}
+\]
+
+不必尝试移动直径圆,也不必尝试其它最远点对:若这个唯一可能的圆心不满足覆盖,就不存在直径为D的覆盖圆。最小包围圆半径ρ满足ρ≥D/2,当且仅当ρ=D/2时存在这样的覆盖圆。
+
+代码的 `diameter_circle.max_excess_m` 为maxᵥ‖v−O‖−D/2;超出数值容差时返回 `witness_vertex` 和从0开始的 `outside_vertex_indices`。因此输出既有判断,也有可独立复核的反例。判定中使用 `coverage_tolerance_m`,边界附近的布尔结果属于浮点容差下结论。
+
+保证清除使用MEC半径加数值余量εₘ,再比较20米。`clearance_certificate.remaining_margin_m=20−(ρ+εₘ)`;非负时支持当前圆心清除,负数时不作此保证。这里的证书以观测误差界正确、真源属于P和浮点几何计算可靠为前提,不是实际清除成功记录。数学上所有可行清除中心构成∩ᵥB(v,20),MEC圆心是充分选择;后续可在此集合中另选距离机器狗最近的点。
+
+**六 满足题设±1°的具体反例**
+
+取三个检测点及示向度:
+
+| 检测点 | x(米) | y(米) | 示向度 |
+| --- | ---: | ---: | ---: |
+| S1 | −1000 | 0 | 1° |
+| S2 | 540 | −500√3 | 121° |
+| S3 | 520 | 520√3 | 241° |
+
+三个角域的交集恰为等边三角形,顶点是(0,0)、(40,0)、(20,20√3)。每个角域的一条边界对应三角形的一条边,另一条边界在三角形外侧,不切去三角形内部。可将真实源设在三角形中心(20,20√3/3),其真实方位与上述读数的偏差均小于1°,且三个检测距离都小于1500米。因此这不是脱离测向模型的任意图形反例。
+
+它的直径D=40米;直径圆半径20米。以底边两端为最远点对,圆心为(20,0),到第三个顶点的距离是20√3≈34.6410米,因此不能覆盖。其最小包围圆为
+
+\[
+O_*=(20,20\sqrt3/3),\qquad \rho_*=40/\sqrt3\approx23.0940\text{米}>20\text{米}.
+\]
+
+
+
+图中蓝色为定位区域,红色虚线为直径圆,绿色实线为最小包围圆。黑色加号只在合成场景中表示已知真源。
+
+**七 官方演练数据验证**
+
+2026年9月10日,界面明确为“问题3 演练 测试”后,通过公开HTTP接口采集。案例编码为 `U5ZW-P9KN-TCAG-R7ST`,选取频道9。
+
+| 检测点 | x(米) | y(米) | 示向度 |
+| --- | ---: | ---: | ---: |
+| S1 | 0 | 0 | 131.52° |
+| S2 | −613.761885 | 391.530775 | 34.92° |
+| S3 | −314.272134 | 656.683353 | 227.19° |
+
+重复在S1测量一次仍返回131.52°。这次重复观测只用于核查原地误差固定,没有作为新的独立测点加入模型。
+
+使用±1°得到四边形,其顶点按逆时针顺序为:
+
+| 顶点 | x(米) | y(米) |
+| --- | ---: | ---: |
+| V1 | −460.698306 | 502.411776 |
+| V2 | −456.424236 | 497.750714 |
+| V3 | −440.569893 | 515.476604 |
+| V4 | −441.370356 | 516.413163 |
+
+计算结果:D=23.996723米,最远点对为V1和V3;直径圆中心为(−450.634100,508.944190),半径11.998362米,但最远顶点到这个圆心的距离为12.602364米,所以该圆不能覆盖区域。最小包围圆中心为(−449.976955,507.952835),半径12.068583米。
+
+
+
+共发送14个被接受的请求:1次进入、12次检测、1次退出,最终虚拟时间293.602198秒。日志末尾为 `/exit`、`accepted=true`、`exit_reason=user_exit`;界面显示测试正常结束。没有调用 `/clear`,所以这里不能将演练结束解读为“清除了所有源”。官方没有公开该源的坐标真值,本例验证的是观测交会计算、接口适配和几何自洽性,不声称验证了真实位置误差。
+
+原始请求响应保存在 `output/q1/practice/*_actions.json`,保留了复算所需的请求ID、位置、频道与时间;三点观测及其计算结果保存在 `*_observations.json`。没有读取或解密模拟器隐藏场景数据。
+
+**八 运行方式和代码入口**
+
+核心算法仅用Python标准库,支持Python3.10及以上;本次在Python3.12.14中验证。在项目根目录运行:
+
+```powershell
+python -m geometry examples/q1/triangle.json -o output/q1/triangle_recomputed.json
+python main.py examples/q1/normal.json
+python -m geometry examples/q1/normal.json --rounding-margin-deg 0.005
+python -m unittest discover -s tests -v
+python scripts/build_q1_demo.py
+python scripts/benchmark_q1.py
+```
+
+输入JSON的最小格式:
+
+```json
+{
+ "measurements": [
+ {"x": 0, "y": 0, "bearing_deg": 131.52, "half_angle_deg": 1},
+ {"x": -613.7618853216438, "y": 391.53077545248124, "bearing_deg": 34.92},
+ {"x": -314.2721344680094, "y": 656.6833525352392, "bearing_deg": 227.19}
+ ]
+}
+```
+
+省略 `half_angle_deg` 时默认为1。输出包含区域状态、顶点、直径、最远点对、直径圆覆盖判断、反例证据、最小包围圆和清除余量。CLI的 `error_policy` 记录追加裕量与每条观测最终半角;默认追加0,不改变输入口径。`tolerance_m` 是归一化容差换算回米的尺度;核心算法不接收真源坐标,样例中的 `target` 只用于合成数据的可视化与测试。空集和无界区域不输出有限圆及清除证书。
+
+| 文件 | 职责 |
+| --- | --- |
+| `geometry/localization.py` | 测向角域、半平面、区域分类、直径和包围圆 |
+| `geometry/planning.py` | 问题二首次测向的距离保证候选圆盘;独立于问题一模型 |
+| `geometry/__main__.py`、`main.py` | JSON命令行输入输出 |
+| `tests/test_geometry.py` | 解析反例、随机几何、独立裁剪对照与退化测试 |
+| `tests/test_practice_replay.py` | 离线复算演练计时与观测结果,不连接模拟器 |
+| `tests/test_geometry_certificates.py` | 增量圆与穷举法对照、覆盖证据、清除阈值、舍入选项及首次选点证书 |
+| `scripts/benchmark_q1.py` | 局部性能对比与官方演练误差界敏感性复算 |
+| `scripts/capture_practice.py` | 在已经启动且已核验的演练中采集少量测向 |
+| `scripts/build_q1_demo.py` | 用Python算法生成案例、结果和交互图数据 |
+| `visualization/q1.template.html` | 可视化源模板;不另写一套求解算法 |
+| `output/q1/question1.html` | 可直接打开的离线交互式模拟,所有数据已内嵌 |
+
+如需再次采集,先在模拟器明确启动“问题3演练测试”,等待接口就绪,读取案例编码,然后运行:
+
+```powershell
+python scripts/capture_practice.py --robot-id 你的参赛队号 --practice-confirmed --case-code 界面中的演练案例编码
+```
+
+接口协议没有返回演练/正式模式的字段,`--practice-confirmed` 是操作者确认已核验界面的显式参数,不能替代界面核查。采集脚本没有启动测试的功能,没有正式模式选项,只会调用 `/enter`、`/measure`、`/exit`。禁止在正式测试会话中运行。
+
+**九 验证范围和数值限制**
+
+20项自动化测试通过,包括100组随机有界案例、60组与独立矩形裁剪实现的对照、250组增量圆与穷举圆对照、1500个共圆点及重复点/共线点、平移旋转缩放不变性,以及已保存演练日志的逐动作计时复算。新增测试还覆盖清除距离20米临界值、显式舍入选项、首次安全候选圆盘边界和零半径情形。已测试一组有限但直径超过10亿米的近乎平行测向,确认不会因为显示范围而被截断成小区域。
+
+本次离线优化报告保存在 `output/q1/optimization_report.json`。同一演练观测的敏感性结果如下,原始记录不改写:
+
+| 误差半角 | 直径 / 米 | MEC半径 / 米 | 直径圆最大超出 / 米 | 含数值裕量的清除余量 / 米 |
+| --- | ---: | ---: | ---: | ---: |
+| 1°(默认题设) | 23.996723 | 12.068583 | 0.604002 | 7.931415 |
+| 1.005°(额外舍入) | 24.119088 | 12.130377 | 0.608486 | 7.869621 |
+
+在本机60个共圆点的局部基准中,旧MEC枚举中位约306.5毫秒,新算法约0.096毫秒;4点时旧法约0.022毫秒、新法约0.029毫秒,小规模没有提速优势。这些是特定输入的局部实测,不是普遍倍率保证,也不代表网络、全清闭环或官方成绩。三点演练的完整问题一求解中位约0.099毫秒。运行基准脚本可以重新生成当前机器的报告。
+
+浮点计算不是精确算术。实现使用归一化位置容差10⁻⁹、平行行列式阈值10⁻¹²和较严格的衰退方向角度容差。极端近乎平行、差异接近机器精度或接近退化的输入可能需要高精度几何谓词;结果接近临界阈值时应做容差敏感性检查,而不把最后若干小数位当作严格证明。正常题设坐标和测向精度下,本次案例远离这些数值极限。
+
+交互图数据全部由同一Python求解器生成。已在最小DOM测试环境中检查32个“案例/测点数量/视图”组合,未发现缺失控件或NaN等非法图形坐标;并对360与736像素、明暗两种配色的SVG作静态图形检查。浏览器工具对本地文件URL的访问被策略拦截,因此没有声称完成真实浏览器的HTML/CSS布局验证。离线页面和对话内可视化均已生成;静态PNG可作为直接查看的补充。
+
+几何算法参考:[CGAL最小包围圆定义与Welzl增量方法](https://doc.cgal.org/latest/Bounding_volumes/classCGAL_1_1Min__circle__2.html);[McGill旋转卡壳说明](https://www-cgrl.cs.mcgill.ca/~godfried/research/calipers.html)。本次代码仅用Python标准库,不依赖CGAL。
diff --git a/examples/q1/empty.json b/examples/q1/empty.json
new file mode 100644
index 0000000..93eb0ad
--- /dev/null
+++ b/examples/q1/empty.json
@@ -0,0 +1,20 @@
+{
+ "id": "empty",
+ "name": "不相容观测",
+ "source": "synthetic",
+ "target": null,
+ "measurements": [
+ {
+ "x": -100,
+ "y": 0,
+ "bearing_deg": 180,
+ "half_angle_deg": 1.0
+ },
+ {
+ "x": 100,
+ "y": 0,
+ "bearing_deg": 0,
+ "half_angle_deg": 1.0
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/q1/normal.json b/examples/q1/normal.json
new file mode 100644
index 0000000..91eab3d
--- /dev/null
+++ b/examples/q1/normal.json
@@ -0,0 +1,29 @@
+{
+ "id": "normal",
+ "name": "三点交会",
+ "source": "synthetic",
+ "target": [
+ 80,
+ 90
+ ],
+ "measurements": [
+ {
+ "x": -600,
+ "y": -250,
+ "bearing_deg": 27.21505117707799,
+ "half_angle_deg": 1.0
+ },
+ {
+ "x": 650,
+ "y": -100,
+ "bearing_deg": 161.015051177078,
+ "half_angle_deg": 1.0
+ },
+ {
+ "x": 100,
+ "y": 700,
+ "bearing_deg": 268.4721225527146,
+ "half_angle_deg": 1.0
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/q1/poor.json b/examples/q1/poor.json
new file mode 100644
index 0000000..4d5e3b2
--- /dev/null
+++ b/examples/q1/poor.json
@@ -0,0 +1,29 @@
+{
+ "id": "poor",
+ "name": "近共线检测点",
+ "source": "synthetic",
+ "target": [
+ 900,
+ 50
+ ],
+ "measurements": [
+ {
+ "x": 0,
+ "y": 0,
+ "bearing_deg": 3.1798301198642345,
+ "half_angle_deg": 1.0
+ },
+ {
+ "x": 100,
+ "y": 0,
+ "bearing_deg": 3.576334374997351,
+ "half_angle_deg": 1.0
+ },
+ {
+ "x": 200,
+ "y": 0,
+ "bearing_deg": 4.085616779974877,
+ "half_angle_deg": 1.0
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/q1/segment.json b/examples/q1/segment.json
new file mode 100644
index 0000000..4d630a2
--- /dev/null
+++ b/examples/q1/segment.json
@@ -0,0 +1,23 @@
+{
+ "id": "segment",
+ "name": "零误差退化线段",
+ "source": "synthetic",
+ "target": [
+ 40,
+ 0
+ ],
+ "measurements": [
+ {
+ "x": 0,
+ "y": 0,
+ "bearing_deg": 0,
+ "half_angle_deg": 0
+ },
+ {
+ "x": 100,
+ "y": 0,
+ "bearing_deg": 180,
+ "half_angle_deg": 0
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/q1/triangle.json b/examples/q1/triangle.json
new file mode 100644
index 0000000..39439a0
--- /dev/null
+++ b/examples/q1/triangle.json
@@ -0,0 +1,29 @@
+{
+ "id": "triangle",
+ "name": "直径圆反例",
+ "source": "synthetic",
+ "target": [
+ 20,
+ 11.547005383792515
+ ],
+ "measurements": [
+ {
+ "x": -1000,
+ "y": 0,
+ "bearing_deg": 1,
+ "half_angle_deg": 1.0
+ },
+ {
+ "x": 540,
+ "y": -866.0254037844386,
+ "bearing_deg": 121,
+ "half_angle_deg": 1.0
+ },
+ {
+ "x": 520,
+ "y": 900.6664199358162,
+ "bearing_deg": 241,
+ "half_angle_deg": 1.0
+ }
+ ]
+}
\ No newline at end of file
diff --git a/geometry/__init__.py b/geometry/__init__.py
new file mode 100644
index 0000000..0ece746
--- /dev/null
+++ b/geometry/__init__.py
@@ -0,0 +1,6 @@
+"""B题问题一:有界误差方位交会定位。仅使用 Python 标准库。"""
+
+from .localization import Measurement, solve, smallest_enclosing_circle
+from .planning import first_bearing_candidate_disk
+
+__all__ = ["Measurement", "solve", "smallest_enclosing_circle", "first_bearing_candidate_disk"]
diff --git a/geometry/__main__.py b/geometry/__main__.py
new file mode 100644
index 0000000..627a3ad
--- /dev/null
+++ b/geometry/__main__.py
@@ -0,0 +1,38 @@
+"""python -m geometry observations.json [-o result.json]"""
+
+import argparse
+import json
+from dataclasses import replace
+from math import isfinite
+from pathlib import Path
+from .localization import Measurement, solve
+
+
+def main():
+ parser = argparse.ArgumentParser(description="B题问题一:角域交、直径与包围圆")
+ parser.add_argument("input", type=Path, help="包含measurements数组的JSON文件")
+ parser.add_argument("-o", "--output", type=Path)
+ parser.add_argument("--rounding-margin-deg", type=float, default=0,
+ help="额外误差半角,默认0;可显式设0.005进行舍入敏感性分析")
+ args = parser.parse_args()
+ try:
+ source = json.loads(args.input.read_text(encoding="utf-8-sig"))
+ measurements = [Measurement(**row) for row in source["measurements"]]
+ if not isfinite(args.rounding_margin_deg) or args.rounding_margin_deg < 0:
+ raise ValueError("额外误差半角必须为有限非负数")
+ measurements = [replace(m, half_angle_deg=m.half_angle_deg+args.rounding_margin_deg)
+ for m in measurements]
+ result = solve(measurements)
+ result["error_policy"] = {"additional_half_angle_deg": args.rounding_margin_deg,
+ "effective_half_angles_deg": [m.half_angle_deg for m in measurements]}
+ except (ValueError, TypeError, KeyError, OSError) as exc:
+ parser.error(str(exc))
+ text = json.dumps(result, ensure_ascii=False, indent=2, allow_nan=False)
+ if args.output:
+ args.output.parent.mkdir(parents=True, exist_ok=True)
+ args.output.write_text(text + "\n", encoding="utf-8")
+ print(text)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/geometry/localization.py b/geometry/localization.py
new file mode 100644
index 0000000..80e32a3
--- /dev/null
+++ b/geometry/localization.py
@@ -0,0 +1,269 @@
+"""Intersect forward bearing wedges without an artificial bounding box.
+
+All calculations are floating point with normalized station coordinates.
+The reference algorithm deliberately favors auditability over asymptotic speed.
+"""
+
+from dataclasses import dataclass
+from itertools import combinations
+from math import cos, sin, radians, hypot, isfinite
+from random import Random
+
+Point = tuple[float, float]
+EPS = 1e-9
+PARALLEL_EPS = 1e-12
+
+
+@dataclass(frozen=True)
+class Measurement:
+ x: float
+ y: float
+ bearing_deg: float
+ half_angle_deg: float = 1.0
+
+ def __post_init__(self):
+ values = (self.x, self.y, self.bearing_deg, self.half_angle_deg)
+ if any(isinstance(v, bool) or not isinstance(v, (int, float))
+ or not isfinite(v) for v in values):
+ raise ValueError("坐标、示向度和误差半角必须是有限数值")
+ if not 0 <= self.half_angle_deg < 90:
+ raise ValueError("误差半角必须在 [0, 90) 度;题设为1度")
+
+
+def cross(a: Point, b: Point) -> float:
+ return a[0] * b[1] - a[1] * b[0]
+
+
+def distance(a: Point, b: Point) -> float:
+ return hypot(a[0] - b[0], a[1] - b[1])
+
+
+def halfplanes(m: Measurement) -> list[tuple[float, float, float]]:
+ """Return a*x+b*y<=c with unit normals, including the forward constraint."""
+ 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]
+
+
+def convex_hull(points: list[Point], eps: float = EPS) -> list[Point]:
+ """Monotone chain, CCW, no repeated final vertex; retain segment endpoints."""
+ 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]
+
+
+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)
+
+
+def _circumcircle(a: Point, b: Point, c: Point):
+ u = (b[0] - a[0], b[1] - a[1])
+ v = (c[0] - a[0], c[1] - a[1])
+ det = 2 * cross(u, v)
+ if abs(det) <= PARALLEL_EPS * max(hypot(*u) * hypot(*v), 1e-300):
+ return None
+ uu, vv = u[0] ** 2 + u[1] ** 2, v[0] ** 2 + v[1] ** 2
+ center = (a[0] + (uu * v[1] - vv * u[1]) / det,
+ a[1] + (u[0] * vv - v[0] * uu) / det)
+ return center, distance(center, a)
+
+
+def _enclosing_circle_reference(points: list[Point], eps: float = EPS):
+ """Enumerate all 1/2/3-point supports; O(m^4), appropriate for small m.
+
+ Radius is recomputed against every vertex to preserve numerical containment.
+ Coordinates should be normalized when there are very large offsets.
+ """
+ if not points:
+ raise ValueError("空集不计算最小包围圆")
+ xs, ys = [p[0] for p in points], [p[1] for p in points]
+ best_c = ((min(xs) + max(xs)) / 2, (min(ys) + max(ys)) / 2)
+ best_r = max(distance(best_c, p) for p in points)
+
+ def consider(center, radius):
+ nonlocal best_c, best_r
+ if radius > best_r + eps:
+ return
+ actual = max(distance(center, p) for p in points)
+ if actual <= radius + eps and actual < best_r:
+ best_c, best_r = center, actual
+
+ for p in points:
+ consider(p, 0.0)
+ for a, b in combinations(points, 2):
+ consider(((a[0] + b[0]) / 2, (a[1] + b[1]) / 2), distance(a, b) / 2)
+ for a, b, c in combinations(points, 3):
+ circle = _circumcircle(a, b, c)
+ if circle is not None:
+ consider(*circle)
+ return best_c, best_r
+
+
+def smallest_enclosing_circle(points: list[Point], eps: float = EPS):
+ """Welzl's randomized incremental algorithm, using an explicit call stack.
+
+ The fixed shuffle makes results reproducible. Expected linear work refers
+ to randomized input order, not a worst-case guarantee for this fixed seed.
+ Normalize internally so this public helper also handles translated inputs.
+ The exhaustive reference is retained for independent regression comparisons.
+ """
+ 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 circle is None or distance(circle[0], local[n-1]) > circle[1] + tolerance:
+ stack.append((n-1, boundary + (local[n-1],), False))
+ 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)
+
+
+def solve(measurements: list[Measurement], eps: float = EPS) -> dict:
+ """Solve Q1's pure angular intersection (no disk/range prior added).
+
+ Nonempty intersections of <180-degree forward wedges are pointed polyhedra,
+ so they possess a vertex, even if unbounded. Feasibility follows from vertex
+ enumeration; unboundedness follows from a nonzero recession direction.
+ Empty diameter is null, unbounded diameter is null with diameter_infinite=true.
+ """
+ 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):
+ return all(a * p[0] + b * p[1] <= c + eps 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 abs(det) <= PARALLEL_EPS:
+ continue
+ p = ((c * e - b * f) / det, (a * f - c * d) / det)
+ if 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
diff --git a/geometry/planning.py b/geometry/planning.py
new file mode 100644
index 0000000..119894e
--- /dev/null
+++ b/geometry/planning.py
@@ -0,0 +1,32 @@
+"""Distance certificates for Q2; does not change Q1's angular polygon model."""
+
+from math import cos, sin, radians, isfinite
+from .localization import Measurement
+
+
+def first_bearing_candidate_disk(measurement: Measurement,
+ max_range_m: float = 1500,
+ guaranteed_range_m: float = 1000) -> dict:
+ """Enclose the entire truncated first-bearing sector, then erode its disk.
+
+ For half-angle <=45 degrees the sector MEC has center distance and radius
+ a=R/(2*cos(delta)). Intersecting with Omega only shrinks the source set.
+ A missing sufficient disk is NOT proof that Q is empty. The certificate
+ guarantees distance only: directional transmitters may still be invisible.
+ """
+ if any(isinstance(v, bool) or not isinstance(v, (int, float)) or not isfinite(v)
+ or v <= 0 for v in (max_range_m, guaranteed_range_m)):
+ raise ValueError("接收半径必须为有限正数")
+ if measurement.half_angle_deg > 45:
+ raise ValueError("此窄角域证书仅支持半角不超过45度;题设为1度")
+ delta, theta = radians(measurement.half_angle_deg), radians(measurement.bearing_deg % 360)
+ a = max_range_m/(2*cos(delta))
+ center = [measurement.x+a*cos(theta), measurement.y+a*sin(theta)]
+ available = guaranteed_range_m-a
+ return {"source_model": "first_bearing_sector_with_range_upper_bound",
+ "sector_enclosing_circle": {"center": center, "radius_m": a},
+ "sufficient_candidate_disk": {"center": center, "radius_m": available}
+ if available >= 0 else None,
+ "candidate_nonempty_certified": available >= 0,
+ "distance_only": True,
+ "max_range_m": max_range_m, "guaranteed_range_m": guaranteed_range_m}
diff --git a/issue/A题.pdf b/issue/A题.pdf
deleted file mode 100644
index c492bcd..0000000
Binary files a/issue/A题.pdf and /dev/null differ
diff --git a/issue/A题/A题.pdf b/issue/A题/A题.pdf
deleted file mode 100644
index c492bcd..0000000
Binary files a/issue/A题/A题.pdf and /dev/null differ
diff --git a/issue/A题/附件/附件1.xlsx b/issue/A题/附件/附件1.xlsx
deleted file mode 100644
index 7e6d9ff..0000000
Binary files a/issue/A题/附件/附件1.xlsx and /dev/null differ
diff --git a/issue/A题/附件/附件2.xlsx b/issue/A题/附件/附件2.xlsx
deleted file mode 100644
index 0066c18..0000000
Binary files a/issue/A题/附件/附件2.xlsx and /dev/null differ
diff --git a/issue/A题/附件/附件3/result1.xlsx b/issue/A题/附件/附件3/result1.xlsx
deleted file mode 100644
index 03cefc9..0000000
Binary files a/issue/A题/附件/附件3/result1.xlsx and /dev/null differ
diff --git a/issue/A题/附件/附件3/result2.xlsx b/issue/A题/附件/附件3/result2.xlsx
deleted file mode 100644
index 03cefc9..0000000
Binary files a/issue/A题/附件/附件3/result2.xlsx and /dev/null differ
diff --git a/issue/A题/附件/附件3/result3.xlsx b/issue/A题/附件/附件3/result3.xlsx
deleted file mode 100644
index 4fb701e..0000000
Binary files a/issue/A题/附件/附件3/result3.xlsx and /dev/null differ
diff --git a/issue/A题/附件/附件3/result4.xlsx b/issue/A题/附件/附件3/result4.xlsx
deleted file mode 100644
index c0536cb..0000000
Binary files a/issue/A题/附件/附件3/result4.xlsx and /dev/null differ
diff --git a/issue/C题.pdf b/issue/C题.pdf
deleted file mode 100644
index 6bf33e4..0000000
Binary files a/issue/C题.pdf and /dev/null differ
diff --git a/issue/C题/C题.pdf b/issue/C题/C题.pdf
deleted file mode 100644
index 6bf33e4..0000000
Binary files a/issue/C题/C题.pdf and /dev/null differ
diff --git a/issue/C题/附件/附件1.xlsx b/issue/C题/附件/附件1.xlsx
deleted file mode 100644
index e8bfd81..0000000
Binary files a/issue/C题/附件/附件1.xlsx and /dev/null differ
diff --git a/issue/C题/附件/附件2.xlsx b/issue/C题/附件/附件2.xlsx
deleted file mode 100644
index b0f2bd0..0000000
Binary files a/issue/C题/附件/附件2.xlsx and /dev/null differ
diff --git a/issue/C题/附件/附件3.xlsx b/issue/C题/附件/附件3.xlsx
deleted file mode 100644
index 6155884..0000000
Binary files a/issue/C题/附件/附件3.xlsx and /dev/null differ
diff --git a/issue/C题/附件/附件4.xlsx b/issue/C题/附件/附件4.xlsx
deleted file mode 100644
index 2d6a8ea..0000000
Binary files a/issue/C题/附件/附件4.xlsx and /dev/null differ
diff --git a/issue/C题/附件/附件5/result1.xlsx b/issue/C题/附件/附件5/result1.xlsx
deleted file mode 100644
index 2ff8b04..0000000
Binary files a/issue/C题/附件/附件5/result1.xlsx and /dev/null differ
diff --git a/issue/C题/附件/附件5/result2.xlsx b/issue/C题/附件/附件5/result2.xlsx
deleted file mode 100644
index 087cbc7..0000000
Binary files a/issue/C题/附件/附件5/result2.xlsx and /dev/null differ
diff --git a/issue/C题/附件/附件5/result3.xlsx b/issue/C题/附件/附件5/result3.xlsx
deleted file mode 100644
index 0ca36a4..0000000
Binary files a/issue/C题/附件/附件5/result3.xlsx and /dev/null differ
diff --git a/issue/C题/附件/附件5/result4-2.xlsx b/issue/C题/附件/附件5/result4-2.xlsx
deleted file mode 100644
index 087cbc7..0000000
Binary files a/issue/C题/附件/附件5/result4-2.xlsx and /dev/null differ
diff --git a/issue/C题/附件/附件5/result4-3.xlsx b/issue/C题/附件/附件5/result4-3.xlsx
deleted file mode 100644
index 0ca36a4..0000000
Binary files a/issue/C题/附件/附件5/result4-3.xlsx and /dev/null differ
diff --git a/issue/附件/~$附件2.docx b/issue/附件/~$附件2.docx
new file mode 100644
index 0000000..f9a31eb
Binary files /dev/null and b/issue/附件/~$附件2.docx differ
diff --git a/issue/B题/附件/附件1.docx b/issue/附件/附件1.docx
similarity index 100%
rename from issue/B题/附件/附件1.docx
rename to issue/附件/附件1.docx
diff --git a/issue/B题/附件/附件2.docx b/issue/附件/附件2.docx
similarity index 100%
rename from issue/B题/附件/附件2.docx
rename to issue/附件/附件2.docx
diff --git a/main.py b/main.py
index 36b6e61..1e333a1 100644
--- a/main.py
+++ b/main.py
@@ -1,5 +1,4 @@
-def main():
- print("Hello from math!")
+from geometry.__main__ import main
if __name__ == "__main__":
diff --git a/output/q1/demo_data.json b/output/q1/demo_data.json
new file mode 100644
index 0000000..5e8d999
--- /dev/null
+++ b/output/q1/demo_data.json
@@ -0,0 +1,815 @@
+[
+ {
+ "id": "normal",
+ "name": "三点交会",
+ "source": "synthetic",
+ "target": [
+ 80,
+ 90
+ ],
+ "measurements": [
+ {
+ "x": -600,
+ "y": -250,
+ "bearing_deg": 27.21505117707799,
+ "half_angle_deg": 1.0
+ },
+ {
+ "x": 650,
+ "y": -100,
+ "bearing_deg": 161.015051177078,
+ "half_angle_deg": 1.0
+ },
+ {
+ "x": 100,
+ "y": 700,
+ "bearing_deg": 268.4721225527146,
+ "half_angle_deg": 1.0
+ }
+ ],
+ "frames": [
+ {
+ "status": "unbounded",
+ "vertices": [
+ [
+ -600.0,
+ -250.0
+ ]
+ ],
+ "tolerance_m": 1e-09,
+ "diameter_infinite": true,
+ "diameter_m": null,
+ "recession_direction": [
+ 0.8971423583981722,
+ 0.44174154068614097
+ ],
+ "message": "存在非零衰退方向,区域无界,直径为无穷大"
+ },
+ {
+ "status": "polygon",
+ "vertices": [
+ [
+ 45.3908396669742,
+ 96.27384560337686
+ ],
+ [
+ 80.26352724282751,
+ 84.95314961314784
+ ],
+ [
+ 106.24825816469405,
+ 97.74770218809789
+ ],
+ [
+ 71.61376404345356,
+ 110.34332460542697
+ ]
+ ],
+ "tolerance_m": 6.294839156007086e-07,
+ "diameter_infinite": false,
+ "diameter_m": 60.87526295170263,
+ "diameter_pair": [
+ [
+ 45.3908396669742,
+ 96.27384560337686
+ ],
+ [
+ 106.24825816469405,
+ 97.74770218809789
+ ]
+ ],
+ "area_m2": 778.9645078415749,
+ "diameter_circle": {
+ "center": [
+ 75.81954891583413,
+ 97.01077389573737
+ ],
+ "radius_m": 30.437631475851315,
+ "max_vertex_distance_m": 30.43763147585132,
+ "covers": true,
+ "coverage_tolerance_m": 2.5179356624028345e-06,
+ "max_excess_m": 4.367922104569409e-15,
+ "outside_vertex_indices": [],
+ "witness_vertex": null
+ },
+ "minimum_enclosing_circle": {
+ "center": [
+ 75.81954891583412,
+ 97.01077389573737
+ ],
+ "radius_m": 30.437631475851326
+ },
+ "guaranteed_clearable_at_mec_center": false,
+ "clearance_certificate": {
+ "model": "angular_intersection_only",
+ "clear_radius_m": 20.0,
+ "numerical_margin_m": 2.5179356624028345e-06,
+ "radius_with_margin_m": 30.437633993786985,
+ "remaining_margin_m": -10.437633993786985,
+ "center": [
+ 75.81954891583412,
+ 97.01077389573737
+ ]
+ }
+ },
+ {
+ "status": "polygon",
+ "vertices": [
+ [
+ 72.95144059951443,
+ 87.32686714676511
+ ],
+ [
+ 80.26352724282755,
+ 84.95314961314779
+ ],
+ [
+ 94.39740997503758,
+ 91.91249455495989
+ ],
+ [
+ 94.49056873706193,
+ 102.02365367196415
+ ],
+ [
+ 73.93038642999645,
+ 109.5008321248877
+ ]
+ ],
+ "tolerance_m": 7.462871059079371e-07,
+ "diameter_infinite": false,
+ "diameter_m": 26.986082918182134,
+ "diameter_pair": [
+ [
+ 94.39740997503758,
+ 91.91249455495989
+ ],
+ [
+ 73.93038642999645,
+ 109.5008321248877
+ ]
+ ],
+ "area_m2": 382.03705547378974,
+ "diameter_circle": {
+ "center": [
+ 84.163898202517,
+ 100.7066633399238
+ ],
+ "radius_m": 13.493041459091067,
+ "max_vertex_distance_m": 17.45675089097608,
+ "covers": false,
+ "coverage_tolerance_m": 2.9851484236317485e-06,
+ "max_excess_m": 3.96370943188501,
+ "outside_vertex_indices": [
+ 0,
+ 1
+ ],
+ "witness_vertex": [
+ 72.95144059951443,
+ 87.32686714676511
+ ]
+ },
+ "minimum_enclosing_circle": {
+ "center": [
+ 81.87363979249537,
+ 98.04155797588491
+ ],
+ "radius_m": 13.943107186092131
+ },
+ "guaranteed_clearable_at_mec_center": true,
+ "clearance_certificate": {
+ "model": "angular_intersection_only",
+ "clear_radius_m": 20.0,
+ "numerical_margin_m": 2.9851484236317485e-06,
+ "radius_with_margin_m": 13.943110171240557,
+ "remaining_margin_m": 6.056889828759443,
+ "center": [
+ 81.87363979249537,
+ 98.04155797588491
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "triangle",
+ "name": "直径圆反例",
+ "source": "synthetic",
+ "target": [
+ 20,
+ 11.547005383792515
+ ],
+ "measurements": [
+ {
+ "x": -1000,
+ "y": 0,
+ "bearing_deg": 1,
+ "half_angle_deg": 1.0
+ },
+ {
+ "x": 540,
+ "y": -866.0254037844386,
+ "bearing_deg": 121,
+ "half_angle_deg": 1.0
+ },
+ {
+ "x": 520,
+ "y": 900.6664199358162,
+ "bearing_deg": 241,
+ "half_angle_deg": 1.0
+ }
+ ],
+ "frames": [
+ {
+ "status": "unbounded",
+ "vertices": [
+ [
+ -1000.0,
+ 0.0
+ ]
+ ],
+ "tolerance_m": 1e-09,
+ "diameter_infinite": true,
+ "diameter_m": null,
+ "recession_direction": [
+ 1.0,
+ 0.0
+ ],
+ "message": "存在非零衰退方向,区域无界,直径为无穷大"
+ },
+ {
+ "status": "polygon",
+ "vertices": [
+ [
+ -22.483050583603557,
+ 34.1356440648463
+ ],
+ [
+ -1.1527327997955013,
+ 0.0
+ ],
+ [
+ 40.000000000000284,
+ 0.0
+ ],
+ [
+ 19.44641513032613,
+ 35.59985327195466
+ ]
+ ],
+ "tolerance_m": 8.83402513014311e-07,
+ "diameter_infinite": false,
+ "diameter_m": 71.19953515266143,
+ "diameter_pair": [
+ [
+ -22.483050583603557,
+ 34.1356440648463
+ ],
+ [
+ 40.000000000000284,
+ 0.0
+ ]
+ ],
+ "area_m2": 1463.776307271084,
+ "diameter_circle": {
+ "center": [
+ 8.75847470819835,
+ 17.06782203242318
+ ],
+ "radius_m": 35.59976757633071,
+ "max_vertex_distance_m": 35.59976757633074,
+ "covers": true,
+ "coverage_tolerance_m": 3.533610052057244e-06,
+ "max_excess_m": 2.4519345249005315e-14,
+ "outside_vertex_indices": [],
+ "witness_vertex": null
+ },
+ "minimum_enclosing_circle": {
+ "center": [
+ 8.75847470819835,
+ 17.06782203242318
+ ],
+ "radius_m": 35.59976757633074
+ },
+ "guaranteed_clearable_at_mec_center": false,
+ "clearance_certificate": {
+ "model": "angular_intersection_only",
+ "clear_radius_m": 20.0,
+ "numerical_margin_m": 3.533610052057244e-06,
+ "radius_with_margin_m": 35.59977110994079,
+ "remaining_margin_m": -15.59977110994079,
+ "center": [
+ 8.75847470819835,
+ 17.06782203242318
+ ]
+ }
+ },
+ {
+ "status": "polygon",
+ "vertices": [
+ [
+ -5.755396159656812e-13,
+ 0.0
+ ],
+ [
+ 40.000000000000234,
+ 0.0
+ ],
+ [
+ 19.999999999999847,
+ 34.64101615137823
+ ]
+ ],
+ "tolerance_m": 1.0200653573832087e-06,
+ "diameter_infinite": false,
+ "diameter_m": 40.00000000000081,
+ "diameter_pair": [
+ [
+ -5.755396159656812e-13,
+ 0.0
+ ],
+ [
+ 40.000000000000234,
+ 0.0
+ ]
+ ],
+ "area_m2": 692.8203230275786,
+ "diameter_circle": {
+ "center": [
+ 19.99999999999983,
+ 0.0
+ ],
+ "radius_m": 20.000000000000405,
+ "max_vertex_distance_m": 34.641016151378224,
+ "covers": false,
+ "coverage_tolerance_m": 4.080261429532835e-06,
+ "max_excess_m": 14.641016151377823,
+ "outside_vertex_indices": [
+ 2
+ ],
+ "witness_vertex": [
+ 19.999999999999847,
+ 34.64101615137823
+ ]
+ },
+ "minimum_enclosing_circle": {
+ "center": [
+ 19.99999999999983,
+ 11.547005383792735
+ ],
+ "radius_m": 23.094010767585495
+ },
+ "guaranteed_clearable_at_mec_center": false,
+ "clearance_certificate": {
+ "model": "angular_intersection_only",
+ "clear_radius_m": 20.0,
+ "numerical_margin_m": 4.080261429532835e-06,
+ "radius_with_margin_m": 23.094014847846925,
+ "remaining_margin_m": -3.0940148478469247,
+ "center": [
+ 19.99999999999983,
+ 11.547005383792735
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "poor",
+ "name": "近共线检测点",
+ "source": "synthetic",
+ "target": [
+ 900,
+ 50
+ ],
+ "measurements": [
+ {
+ "x": 0,
+ "y": 0,
+ "bearing_deg": 3.1798301198642345,
+ "half_angle_deg": 1.0
+ },
+ {
+ "x": 100,
+ "y": 0,
+ "bearing_deg": 3.576334374997351,
+ "half_angle_deg": 1.0
+ },
+ {
+ "x": 200,
+ "y": 0,
+ "bearing_deg": 4.085616779974877,
+ "half_angle_deg": 1.0
+ }
+ ],
+ "frames": [
+ {
+ "status": "unbounded",
+ "vertices": [
+ [
+ 0.0,
+ 0.0
+ ]
+ ],
+ "tolerance_m": 1e-09,
+ "diameter_infinite": true,
+ "diameter_m": null,
+ "recession_direction": [
+ 0.9992763681850043,
+ 0.03803603537420482
+ ],
+ "message": "存在非零衰退方向,区域无界,直径为无穷大"
+ },
+ {
+ "status": "unbounded",
+ "vertices": [
+ [
+ 190.67332308426802,
+ 7.257709171010532
+ ],
+ [
+ 649.0783141257873,
+ 24.706243940862326
+ ],
+ [
+ 1149.8860918066787,
+ 84.03538833511422
+ ]
+ ],
+ "tolerance_m": 5.0000000000000004e-08,
+ "diameter_infinite": true,
+ "diameter_m": null,
+ "recession_direction": [
+ 0.9973401975299315,
+ 0.07288710716551386
+ ],
+ "message": "存在非零衰退方向,区域无界,直径为无穷大"
+ },
+ {
+ "status": "unbounded",
+ "vertices": [
+ [
+ 349.47112173238565,
+ 13.302121787008087
+ ],
+ [
+ 649.0783141257873,
+ 24.706243940862326
+ ],
+ [
+ 704.9784261478786,
+ 27.221516841665565
+ ],
+ [
+ 1149.886091806679,
+ 84.03538833511422
+ ],
+ [
+ 1094.1078143807658,
+ 79.57076189541714
+ ]
+ ],
+ "tolerance_m": 1.0000000000000001e-07,
+ "diameter_infinite": true,
+ "diameter_m": null,
+ "recession_direction": [
+ 0.9973401975299315,
+ 0.07288710716551386
+ ],
+ "message": "存在非零衰退方向,区域无界,直径为无穷大"
+ }
+ ]
+ },
+ {
+ "id": "empty",
+ "name": "不相容观测",
+ "source": "synthetic",
+ "target": null,
+ "measurements": [
+ {
+ "x": -100,
+ "y": 0,
+ "bearing_deg": 180,
+ "half_angle_deg": 1.0
+ },
+ {
+ "x": 100,
+ "y": 0,
+ "bearing_deg": 0,
+ "half_angle_deg": 1.0
+ }
+ ],
+ "frames": [
+ {
+ "status": "unbounded",
+ "vertices": [
+ [
+ -100.0,
+ 0.0
+ ]
+ ],
+ "tolerance_m": 1e-09,
+ "diameter_infinite": true,
+ "diameter_m": null,
+ "recession_direction": [
+ -0.9998476951563913,
+ 0.01745240643728344
+ ],
+ "message": "存在非零衰退方向,区域无界,直径为无穷大"
+ },
+ {
+ "status": "empty",
+ "vertices": [],
+ "diameter_m": null,
+ "diameter_infinite": false,
+ "tolerance_m": 1.0000000000000001e-07,
+ "message": "测向角域没有公共点;检查频道、数据及误差界"
+ }
+ ]
+ },
+ {
+ "id": "segment",
+ "name": "零误差退化线段",
+ "source": "synthetic",
+ "target": [
+ 40,
+ 0
+ ],
+ "measurements": [
+ {
+ "x": 0,
+ "y": 0,
+ "bearing_deg": 0,
+ "half_angle_deg": 0
+ },
+ {
+ "x": 100,
+ "y": 0,
+ "bearing_deg": 180,
+ "half_angle_deg": 0
+ }
+ ],
+ "frames": [
+ {
+ "status": "unbounded",
+ "vertices": [
+ [
+ 0.0,
+ 0.0
+ ]
+ ],
+ "tolerance_m": 1e-09,
+ "diameter_infinite": true,
+ "diameter_m": null,
+ "recession_direction": [
+ 1.0,
+ 0.0
+ ],
+ "message": "存在非零衰退方向,区域无界,直径为无穷大"
+ },
+ {
+ "status": "segment",
+ "vertices": [
+ [
+ 0.0,
+ 0.0
+ ],
+ [
+ 100.0,
+ 0.0
+ ]
+ ],
+ "tolerance_m": 5.0000000000000004e-08,
+ "diameter_infinite": false,
+ "diameter_m": 100.0,
+ "diameter_pair": [
+ [
+ 0.0,
+ 0.0
+ ],
+ [
+ 100.0,
+ 0.0
+ ]
+ ],
+ "area_m2": 0.0,
+ "diameter_circle": {
+ "center": [
+ 50.0,
+ 0.0
+ ],
+ "radius_m": 50.0,
+ "max_vertex_distance_m": 50.0,
+ "covers": true,
+ "coverage_tolerance_m": 2.0000000000000002e-07,
+ "max_excess_m": 0.0,
+ "outside_vertex_indices": [],
+ "witness_vertex": null
+ },
+ "minimum_enclosing_circle": {
+ "center": [
+ 50.0,
+ 0.0
+ ],
+ "radius_m": 50.0
+ },
+ "guaranteed_clearable_at_mec_center": false,
+ "clearance_certificate": {
+ "model": "angular_intersection_only",
+ "clear_radius_m": 20.0,
+ "numerical_margin_m": 2.0000000000000002e-07,
+ "radius_with_margin_m": 50.0000002,
+ "remaining_margin_m": -30.000000200000002,
+ "center": [
+ 50.0,
+ 0.0
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "id": "practice-U5ZW-P9KN-TCAG-R7ST",
+ "name": "官方演练 U5ZW-P9KN-TCAG-R7ST",
+ "source": "official_practice",
+ "target": null,
+ "measurements": [
+ {
+ "x": 0,
+ "y": 0,
+ "bearing_deg": 131.52,
+ "half_angle_deg": 1.0
+ },
+ {
+ "x": -613.7618853216438,
+ "y": 391.53077545248124,
+ "bearing_deg": 34.92,
+ "half_angle_deg": 1.0
+ },
+ {
+ "x": -314.2721344680094,
+ "y": 656.6833525352392,
+ "bearing_deg": 227.19,
+ "half_angle_deg": 1.0
+ }
+ ],
+ "frames": [
+ {
+ "status": "unbounded",
+ "vertices": [
+ [
+ 0.0,
+ 0.0
+ ]
+ ],
+ "tolerance_m": 1e-09,
+ "diameter_infinite": true,
+ "diameter_m": null,
+ "recession_direction": [
+ -0.6497134405131868,
+ 0.7601792191427741
+ ],
+ "message": "存在非零衰退方向,区域无界,直径为无穷大"
+ },
+ {
+ "status": "polygon",
+ "vertices": [
+ [
+ -460.69830582516533,
+ 502.4117755118043
+ ],
+ [
+ -456.18958970820563,
+ 497.4948221803977
+ ],
+ [
+ -436.511353451526,
+ 510.7280211898303
+ ],
+ [
+ -441.37035594940073,
+ 516.4131625064848
+ ]
+ ],
+ "tolerance_m": 3.6400549446402593e-07,
+ "diameter_infinite": false,
+ "diameter_m": 25.57672002626576,
+ "diameter_pair": [
+ [
+ -460.69830582516533,
+ 502.4117755118043
+ ],
+ [
+ -436.511353451526,
+ 510.7280211898303
+ ]
+ ],
+ "area_m2": 167.16830387852585,
+ "diameter_circle": {
+ "center": [
+ -448.6048296383457,
+ 506.5698983508173
+ ],
+ "radius_m": 12.78836001313288,
+ "max_vertex_distance_m": 12.788360013132888,
+ "covers": true,
+ "coverage_tolerance_m": 1.4560219778561037e-06,
+ "max_excess_m": 7.577386519575496e-15,
+ "outside_vertex_indices": [],
+ "witness_vertex": null
+ },
+ "minimum_enclosing_circle": {
+ "center": [
+ -448.6048296383457,
+ 506.5698983508173
+ ],
+ "radius_m": 12.788360013132888
+ },
+ "guaranteed_clearable_at_mec_center": true,
+ "clearance_certificate": {
+ "model": "angular_intersection_only",
+ "clear_radius_m": 20.0,
+ "numerical_margin_m": 1.4560219778561037e-06,
+ "radius_with_margin_m": 12.788361469154864,
+ "remaining_margin_m": 7.211638530845136,
+ "center": [
+ -448.6048296383457,
+ 506.5698983508173
+ ]
+ }
+ },
+ {
+ "status": "polygon",
+ "vertices": [
+ [
+ -460.69830582516533,
+ 502.41177551180425
+ ],
+ [
+ -456.4242356873799,
+ 497.75071394627366
+ ],
+ [
+ -440.56989343525083,
+ 515.4766035082919
+ ],
+ [
+ -441.3703559494006,
+ 516.4131625064849
+ ]
+ ],
+ "tolerance_m": 4.666666666666667e-07,
+ "diameter_infinite": false,
+ "diameter_m": 23.996723024535473,
+ "diameter_pair": [
+ [
+ -460.69830582516533,
+ 502.41177551180425
+ ],
+ [
+ -440.56989343525083,
+ 515.4766035082919
+ ]
+ ],
+ "area_m2": 89.48455568449387,
+ "diameter_circle": {
+ "center": [
+ -450.6340996302081,
+ 508.9441895100481
+ ],
+ "radius_m": 11.998361512267737,
+ "max_vertex_distance_m": 12.60236369723464,
+ "covers": false,
+ "coverage_tolerance_m": 1.8666666666666669e-06,
+ "max_excess_m": 0.604002184966904,
+ "outside_vertex_indices": [
+ 1
+ ],
+ "witness_vertex": [
+ -456.4242356873799,
+ 497.75071394627366
+ ]
+ },
+ "minimum_enclosing_circle": {
+ "center": [
+ -449.97695530423994,
+ 507.9528348831342
+ ],
+ "radius_m": 12.068583013309823
+ },
+ "guaranteed_clearable_at_mec_center": true,
+ "clearance_certificate": {
+ "model": "angular_intersection_only",
+ "clear_radius_m": 20.0,
+ "numerical_margin_m": 1.8666666666666669e-06,
+ "radius_with_margin_m": 12.068584879976491,
+ "remaining_margin_m": 7.931415120023509,
+ "center": [
+ -449.97695530423994,
+ 507.9528348831342
+ ]
+ }
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/output/q1/empty_result.json b/output/q1/empty_result.json
new file mode 100644
index 0000000..cf6778c
--- /dev/null
+++ b/output/q1/empty_result.json
@@ -0,0 +1,8 @@
+{
+ "status": "empty",
+ "vertices": [],
+ "diameter_m": null,
+ "diameter_infinite": false,
+ "tolerance_m": 1.0000000000000001e-07,
+ "message": "测向角域没有公共点;检查频道、数据及误差界"
+}
\ No newline at end of file
diff --git a/output/q1/figures/counterexample-360-dark.png b/output/q1/figures/counterexample-360-dark.png
new file mode 100644
index 0000000..0b8f4e9
Binary files /dev/null and b/output/q1/figures/counterexample-360-dark.png differ
diff --git a/output/q1/figures/counterexample-360-dark.svg b/output/q1/figures/counterexample-360-dark.svg
new file mode 100644
index 0000000..138be0e
--- /dev/null
+++ b/output/q1/figures/counterexample-360-dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/output/q1/figures/counterexample-360-light.png b/output/q1/figures/counterexample-360-light.png
new file mode 100644
index 0000000..db8ef79
Binary files /dev/null and b/output/q1/figures/counterexample-360-light.png differ
diff --git a/output/q1/figures/counterexample-360-light.svg b/output/q1/figures/counterexample-360-light.svg
new file mode 100644
index 0000000..8c8cbb8
--- /dev/null
+++ b/output/q1/figures/counterexample-360-light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/output/q1/figures/counterexample-736-dark.png b/output/q1/figures/counterexample-736-dark.png
new file mode 100644
index 0000000..0c71a26
Binary files /dev/null and b/output/q1/figures/counterexample-736-dark.png differ
diff --git a/output/q1/figures/counterexample-736-dark.svg b/output/q1/figures/counterexample-736-dark.svg
new file mode 100644
index 0000000..691cbf2
--- /dev/null
+++ b/output/q1/figures/counterexample-736-dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/output/q1/figures/counterexample-736-light.png b/output/q1/figures/counterexample-736-light.png
new file mode 100644
index 0000000..1104b8a
Binary files /dev/null and b/output/q1/figures/counterexample-736-light.png differ
diff --git a/output/q1/figures/counterexample-736-light.svg b/output/q1/figures/counterexample-736-light.svg
new file mode 100644
index 0000000..6a2e95e
--- /dev/null
+++ b/output/q1/figures/counterexample-736-light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/output/q1/figures/practice-360-dark.png b/output/q1/figures/practice-360-dark.png
new file mode 100644
index 0000000..2610e77
Binary files /dev/null and b/output/q1/figures/practice-360-dark.png differ
diff --git a/output/q1/figures/practice-360-dark.svg b/output/q1/figures/practice-360-dark.svg
new file mode 100644
index 0000000..c2a4eea
--- /dev/null
+++ b/output/q1/figures/practice-360-dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/output/q1/figures/practice-360-light.png b/output/q1/figures/practice-360-light.png
new file mode 100644
index 0000000..7498ee7
Binary files /dev/null and b/output/q1/figures/practice-360-light.png differ
diff --git a/output/q1/figures/practice-360-light.svg b/output/q1/figures/practice-360-light.svg
new file mode 100644
index 0000000..d152726
--- /dev/null
+++ b/output/q1/figures/practice-360-light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/output/q1/figures/practice-736-dark.png b/output/q1/figures/practice-736-dark.png
new file mode 100644
index 0000000..86937f8
Binary files /dev/null and b/output/q1/figures/practice-736-dark.png differ
diff --git a/output/q1/figures/practice-736-dark.svg b/output/q1/figures/practice-736-dark.svg
new file mode 100644
index 0000000..d469c6f
--- /dev/null
+++ b/output/q1/figures/practice-736-dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/output/q1/figures/practice-736-light.png b/output/q1/figures/practice-736-light.png
new file mode 100644
index 0000000..05ea768
Binary files /dev/null and b/output/q1/figures/practice-736-light.png differ
diff --git a/output/q1/figures/practice-736-light.svg b/output/q1/figures/practice-736-light.svg
new file mode 100644
index 0000000..9aca944
--- /dev/null
+++ b/output/q1/figures/practice-736-light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/output/q1/normal_result.json b/output/q1/normal_result.json
new file mode 100644
index 0000000..e8da7ce
--- /dev/null
+++ b/output/q1/normal_result.json
@@ -0,0 +1,77 @@
+{
+ "status": "polygon",
+ "vertices": [
+ [
+ 72.95144059951443,
+ 87.32686714676511
+ ],
+ [
+ 80.26352724282755,
+ 84.95314961314779
+ ],
+ [
+ 94.39740997503758,
+ 91.91249455495989
+ ],
+ [
+ 94.49056873706193,
+ 102.02365367196415
+ ],
+ [
+ 73.93038642999645,
+ 109.5008321248877
+ ]
+ ],
+ "tolerance_m": 7.462871059079371e-07,
+ "diameter_infinite": false,
+ "diameter_m": 26.986082918182134,
+ "diameter_pair": [
+ [
+ 94.39740997503758,
+ 91.91249455495989
+ ],
+ [
+ 73.93038642999645,
+ 109.5008321248877
+ ]
+ ],
+ "area_m2": 382.03705547378974,
+ "diameter_circle": {
+ "center": [
+ 84.163898202517,
+ 100.7066633399238
+ ],
+ "radius_m": 13.493041459091067,
+ "max_vertex_distance_m": 17.45675089097608,
+ "covers": false,
+ "coverage_tolerance_m": 2.9851484236317485e-06,
+ "max_excess_m": 3.96370943188501,
+ "outside_vertex_indices": [
+ 0,
+ 1
+ ],
+ "witness_vertex": [
+ 72.95144059951443,
+ 87.32686714676511
+ ]
+ },
+ "minimum_enclosing_circle": {
+ "center": [
+ 81.87363979249537,
+ 98.04155797588491
+ ],
+ "radius_m": 13.943107186092131
+ },
+ "guaranteed_clearable_at_mec_center": true,
+ "clearance_certificate": {
+ "model": "angular_intersection_only",
+ "clear_radius_m": 20.0,
+ "numerical_margin_m": 2.9851484236317485e-06,
+ "radius_with_margin_m": 13.943110171240557,
+ "remaining_margin_m": 6.056889828759443,
+ "center": [
+ 81.87363979249537,
+ 98.04155797588491
+ ]
+ }
+}
\ No newline at end of file
diff --git a/output/q1/optimization_report.json b/output/q1/optimization_report.json
new file mode 100644
index 0000000..2673c8b
--- /dev/null
+++ b/output/q1/optimization_report.json
@@ -0,0 +1,67 @@
+{
+ "python": "3.12.14",
+ "timing_method": "median perf_counter; warm local process",
+ "timing_scope": "MEC only; not whole solver speedup or simulator/network performance",
+ "circle_benchmarks": [
+ {
+ "vertices": 4,
+ "reference_ms": 0.021899999410379678,
+ "incremental_ms": 0.028999995265621692,
+ "speedup": 0.7551725167466228,
+ "radius_difference_m": 0.0
+ },
+ {
+ "vertices": 20,
+ "reference_ms": 4.815899999812245,
+ "incremental_ms": 0.055900003644637764,
+ "speedup": 86.15205162467306,
+ "radius_difference_m": 3.552713678800501e-15
+ },
+ {
+ "vertices": 60,
+ "reference_ms": 306.5234000023338,
+ "incremental_ms": 0.09600000339560211,
+ "speedup": 3192.9519704202016,
+ "radius_difference_m": 0.0
+ }
+ ],
+ "practice_sensitivity": [
+ {
+ "additional_half_angle_deg": 0,
+ "solve_ms": 0.09890000364976004,
+ "diameter_m": 23.996723024535473,
+ "mec_radius_m": 12.068583013309823,
+ "diameter_circle_excess_m": 0.604002184966904,
+ "clearance_margin_m": 7.931415120023509
+ },
+ {
+ "additional_half_angle_deg": 0.005,
+ "solve_ms": 0.0962999984039925,
+ "diameter_m": 24.119088149560866,
+ "mec_radius_m": 12.130376957888465,
+ "diameter_circle_excess_m": 0.608486475900504,
+ "clearance_margin_m": 7.869621175444868
+ }
+ ],
+ "q2_first_bearing_certificate": {
+ "source_model": "first_bearing_sector_with_range_upper_bound",
+ "sector_enclosing_circle": {
+ "center": [
+ 750.1153914962542,
+ 0.0
+ ],
+ "radius_m": 750.1153914962542
+ },
+ "sufficient_candidate_disk": {
+ "center": [
+ 750.1153914962542,
+ 0.0
+ ],
+ "radius_m": 249.88460850374577
+ },
+ "candidate_nonempty_certified": true,
+ "distance_only": true,
+ "max_range_m": 1500,
+ "guaranteed_range_m": 1000
+ }
+}
diff --git a/output/q1/poor_result.json b/output/q1/poor_result.json
new file mode 100644
index 0000000..a5aa4ba
--- /dev/null
+++ b/output/q1/poor_result.json
@@ -0,0 +1,33 @@
+{
+ "status": "unbounded",
+ "vertices": [
+ [
+ 349.47112173238565,
+ 13.302121787008087
+ ],
+ [
+ 649.0783141257873,
+ 24.706243940862326
+ ],
+ [
+ 704.9784261478786,
+ 27.221516841665565
+ ],
+ [
+ 1149.886091806679,
+ 84.03538833511422
+ ],
+ [
+ 1094.1078143807658,
+ 79.57076189541714
+ ]
+ ],
+ "tolerance_m": 1.0000000000000001e-07,
+ "diameter_infinite": true,
+ "diameter_m": null,
+ "recession_direction": [
+ 0.9973401975299315,
+ 0.07288710716551386
+ ],
+ "message": "存在非零衰退方向,区域无界,直径为无穷大"
+}
\ No newline at end of file
diff --git a/output/q1/practice-U5ZW-P9KN-TCAG-R7ST_result.json b/output/q1/practice-U5ZW-P9KN-TCAG-R7ST_result.json
new file mode 100644
index 0000000..4a5fd98
--- /dev/null
+++ b/output/q1/practice-U5ZW-P9KN-TCAG-R7ST_result.json
@@ -0,0 +1,72 @@
+{
+ "status": "polygon",
+ "vertices": [
+ [
+ -460.69830582516533,
+ 502.41177551180425
+ ],
+ [
+ -456.4242356873799,
+ 497.75071394627366
+ ],
+ [
+ -440.56989343525083,
+ 515.4766035082919
+ ],
+ [
+ -441.3703559494006,
+ 516.4131625064849
+ ]
+ ],
+ "tolerance_m": 4.666666666666667e-07,
+ "diameter_infinite": false,
+ "diameter_m": 23.996723024535473,
+ "diameter_pair": [
+ [
+ -460.69830582516533,
+ 502.41177551180425
+ ],
+ [
+ -440.56989343525083,
+ 515.4766035082919
+ ]
+ ],
+ "area_m2": 89.48455568449387,
+ "diameter_circle": {
+ "center": [
+ -450.6340996302081,
+ 508.9441895100481
+ ],
+ "radius_m": 11.998361512267737,
+ "max_vertex_distance_m": 12.60236369723464,
+ "covers": false,
+ "coverage_tolerance_m": 1.8666666666666669e-06,
+ "max_excess_m": 0.604002184966904,
+ "outside_vertex_indices": [
+ 1
+ ],
+ "witness_vertex": [
+ -456.4242356873799,
+ 497.75071394627366
+ ]
+ },
+ "minimum_enclosing_circle": {
+ "center": [
+ -449.97695530423994,
+ 507.9528348831342
+ ],
+ "radius_m": 12.068583013309823
+ },
+ "guaranteed_clearable_at_mec_center": true,
+ "clearance_certificate": {
+ "model": "angular_intersection_only",
+ "clear_radius_m": 20.0,
+ "numerical_margin_m": 1.8666666666666669e-06,
+ "radius_with_margin_m": 12.068584879976491,
+ "remaining_margin_m": 7.931415120023509,
+ "center": [
+ -449.97695530423994,
+ 507.9528348831342
+ ]
+ }
+}
\ No newline at end of file
diff --git a/output/q1/practice/U5ZW-P9KN-TCAG-R7ST_actions.json b/output/q1/practice/U5ZW-P9KN-TCAG-R7ST_actions.json
new file mode 100644
index 0000000..16f0517
--- /dev/null
+++ b/output/q1/practice/U5ZW-P9KN-TCAG-R7ST_actions.json
@@ -0,0 +1,278 @@
+[
+ {
+ "path": "/enter",
+ "request": {
+ "arena_id": "default",
+ "robot_id": "202608023016",
+ "request_id": "q1-536f4d679c4f-1"
+ },
+ "response": {
+ "accepted": true,
+ "real_timestamp_ms": 1789041227659,
+ "virtual_time_s": 0,
+ "max_virtual_duration_s": 360000,
+ "max_real_duration_s": 1200,
+ "remaining_real_duration_s": 1200
+ },
+ "retries": 0
+ },
+ {
+ "path": "/measure",
+ "request": {
+ "arena_id": "default",
+ "robot_id": "202608023016",
+ "request_id": "q1-536f4d679c4f-2",
+ "position": {
+ "x": 0,
+ "y": 0
+ },
+ "channel": 1
+ },
+ "response": {
+ "accepted": true,
+ "real_timestamp_ms": 1789041227678,
+ "virtual_time_s": 5,
+ "measure_result": "no_signal"
+ },
+ "retries": 0
+ },
+ {
+ "path": "/measure",
+ "request": {
+ "arena_id": "default",
+ "robot_id": "202608023016",
+ "request_id": "q1-536f4d679c4f-3",
+ "position": {
+ "x": 0,
+ "y": 0
+ },
+ "channel": 2
+ },
+ "response": {
+ "accepted": true,
+ "real_timestamp_ms": 1789041227694,
+ "virtual_time_s": 11,
+ "measure_result": "no_signal"
+ },
+ "retries": 0
+ },
+ {
+ "path": "/measure",
+ "request": {
+ "arena_id": "default",
+ "robot_id": "202608023016",
+ "request_id": "q1-536f4d679c4f-4",
+ "position": {
+ "x": 0,
+ "y": 0
+ },
+ "channel": 3
+ },
+ "response": {
+ "accepted": true,
+ "real_timestamp_ms": 1789041227696,
+ "virtual_time_s": 17,
+ "measure_result": "no_signal"
+ },
+ "retries": 0
+ },
+ {
+ "path": "/measure",
+ "request": {
+ "arena_id": "default",
+ "robot_id": "202608023016",
+ "request_id": "q1-536f4d679c4f-5",
+ "position": {
+ "x": 0,
+ "y": 0
+ },
+ "channel": 4
+ },
+ "response": {
+ "accepted": true,
+ "real_timestamp_ms": 1789041227697,
+ "virtual_time_s": 23,
+ "measure_result": "no_signal"
+ },
+ "retries": 0
+ },
+ {
+ "path": "/measure",
+ "request": {
+ "arena_id": "default",
+ "robot_id": "202608023016",
+ "request_id": "q1-536f4d679c4f-6",
+ "position": {
+ "x": 0,
+ "y": 0
+ },
+ "channel": 5
+ },
+ "response": {
+ "accepted": true,
+ "real_timestamp_ms": 1789041227709,
+ "virtual_time_s": 29,
+ "measure_result": "no_signal"
+ },
+ "retries": 0
+ },
+ {
+ "path": "/measure",
+ "request": {
+ "arena_id": "default",
+ "robot_id": "202608023016",
+ "request_id": "q1-536f4d679c4f-7",
+ "position": {
+ "x": 0,
+ "y": 0
+ },
+ "channel": 6
+ },
+ "response": {
+ "accepted": true,
+ "real_timestamp_ms": 1789041227725,
+ "virtual_time_s": 35,
+ "measure_result": "no_signal"
+ },
+ "retries": 0
+ },
+ {
+ "path": "/measure",
+ "request": {
+ "arena_id": "default",
+ "robot_id": "202608023016",
+ "request_id": "q1-536f4d679c4f-8",
+ "position": {
+ "x": 0,
+ "y": 0
+ },
+ "channel": 7
+ },
+ "response": {
+ "accepted": true,
+ "real_timestamp_ms": 1789041227727,
+ "virtual_time_s": 41,
+ "measure_result": "no_signal"
+ },
+ "retries": 0
+ },
+ {
+ "path": "/measure",
+ "request": {
+ "arena_id": "default",
+ "robot_id": "202608023016",
+ "request_id": "q1-536f4d679c4f-9",
+ "position": {
+ "x": 0,
+ "y": 0
+ },
+ "channel": 8
+ },
+ "response": {
+ "accepted": true,
+ "real_timestamp_ms": 1789041227729,
+ "virtual_time_s": 47,
+ "measure_result": "no_signal"
+ },
+ "retries": 0
+ },
+ {
+ "path": "/measure",
+ "request": {
+ "arena_id": "default",
+ "robot_id": "202608023016",
+ "request_id": "q1-536f4d679c4f-10",
+ "position": {
+ "x": 0,
+ "y": 0
+ },
+ "channel": 9
+ },
+ "response": {
+ "accepted": true,
+ "real_timestamp_ms": 1789041227740,
+ "virtual_time_s": 53,
+ "measure_result": "direction",
+ "svd_deg": 131.52
+ },
+ "retries": 0
+ },
+ {
+ "path": "/measure",
+ "request": {
+ "arena_id": "default",
+ "robot_id": "202608023016",
+ "request_id": "q1-536f4d679c4f-11",
+ "position": {
+ "x": 0,
+ "y": 0
+ },
+ "channel": 9
+ },
+ "response": {
+ "accepted": true,
+ "real_timestamp_ms": 1789041227755,
+ "virtual_time_s": 58,
+ "measure_result": "direction",
+ "svd_deg": 131.52
+ },
+ "retries": 0
+ },
+ {
+ "path": "/measure",
+ "request": {
+ "arena_id": "default",
+ "robot_id": "202608023016",
+ "request_id": "q1-536f4d679c4f-12",
+ "position": {
+ "x": -613.7618853216438,
+ "y": 391.53077545248124
+ },
+ "channel": 9
+ },
+ "response": {
+ "accepted": true,
+ "real_timestamp_ms": 1789041227770,
+ "virtual_time_s": 208.602198,
+ "measure_result": "direction",
+ "svd_deg": 34.92
+ },
+ "retries": 0
+ },
+ {
+ "path": "/measure",
+ "request": {
+ "arena_id": "default",
+ "robot_id": "202608023016",
+ "request_id": "q1-536f4d679c4f-13",
+ "position": {
+ "x": -314.2721344680094,
+ "y": 656.6833525352392
+ },
+ "channel": 9
+ },
+ "response": {
+ "accepted": true,
+ "real_timestamp_ms": 1789041227772,
+ "virtual_time_s": 293.602198,
+ "measure_result": "direction",
+ "svd_deg": 227.19
+ },
+ "retries": 0
+ },
+ {
+ "path": "/exit",
+ "request": {
+ "arena_id": "default",
+ "robot_id": "202608023016",
+ "request_id": "q1-536f4d679c4f-14"
+ },
+ "response": {
+ "accepted": true,
+ "real_timestamp_ms": 1789041227775,
+ "virtual_time_s": 293.602198,
+ "exit_reason": "user_exit"
+ },
+ "retries": 0
+ }
+]
\ No newline at end of file
diff --git a/output/q1/practice/U5ZW-P9KN-TCAG-R7ST_observations.json b/output/q1/practice/U5ZW-P9KN-TCAG-R7ST_observations.json
new file mode 100644
index 0000000..78f0198
--- /dev/null
+++ b/output/q1/practice/U5ZW-P9KN-TCAG-R7ST_observations.json
@@ -0,0 +1,87 @@
+{
+ "name": "官方问题3演练观测",
+ "source": "official_practice",
+ "case_code": "U5ZW-P9KN-TCAG-R7ST",
+ "channel": 9,
+ "ground_truth": null,
+ "measurements": [
+ {
+ "x": 0,
+ "y": 0,
+ "bearing_deg": 131.52,
+ "half_angle_deg": 1.0
+ },
+ {
+ "x": -613.7618853216438,
+ "y": 391.53077545248124,
+ "bearing_deg": 34.92,
+ "half_angle_deg": 1.0
+ },
+ {
+ "x": -314.2721344680094,
+ "y": 656.6833525352392,
+ "bearing_deg": 227.19,
+ "half_angle_deg": 1.0
+ }
+ ],
+ "repeated_station_response": {
+ "accepted": true,
+ "real_timestamp_ms": 1789041227755,
+ "virtual_time_s": 58,
+ "measure_result": "direction",
+ "svd_deg": 131.52
+ },
+ "result": {
+ "status": "polygon",
+ "vertices": [
+ [
+ -460.69830582516533,
+ 502.41177551180425
+ ],
+ [
+ -456.4242356873799,
+ 497.75071394627366
+ ],
+ [
+ -440.56989343525083,
+ 515.4766035082919
+ ],
+ [
+ -441.3703559494006,
+ 516.4131625064849
+ ]
+ ],
+ "tolerance_m": 4.666666666666667e-07,
+ "diameter_infinite": false,
+ "diameter_m": 23.996723024535473,
+ "diameter_pair": [
+ [
+ -460.69830582516533,
+ 502.41177551180425
+ ],
+ [
+ -440.56989343525083,
+ 515.4766035082919
+ ]
+ ],
+ "area_m2": 89.48455568449387,
+ "diameter_circle": {
+ "center": [
+ -450.6340996302081,
+ 508.9441895100481
+ ],
+ "radius_m": 11.998361512267737,
+ "max_vertex_distance_m": 12.60236369723464,
+ "covers": false
+ },
+ "minimum_enclosing_circle": {
+ "center": [
+ -449.97695530423994,
+ 507.9528348831342
+ ],
+ "radius_m": 12.068583013309823
+ },
+ "guaranteed_clearable_at_mec_center": true
+ },
+ "scope": "仅采集问题一观测,不执行问题三全清除任务;没有调用clear"
+}
\ No newline at end of file
diff --git a/output/q1/question1.html b/output/q1/question1.html
new file mode 100644
index 0000000..2afcece
--- /dev/null
+++ b/output/q1/question1.html
@@ -0,0 +1,211 @@
+
B题问题一交会定位
+
+
带误差的交会定位
+
+
+
+
+
+
+
+
+ 定位区域
+ 直径线段及直径圆
+ 最小包围圆
+ □ 检测点 + 合成真值 ◎ 覆盖反例顶点
+
+
+
+ 检测点与示向度
+
+
+ | 测点 | x / m | y / m | 示向度 / ° | 误差半角 / ° |
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/output/q1/segment_result.json b/output/q1/segment_result.json
new file mode 100644
index 0000000..e3a2f39
--- /dev/null
+++ b/output/q1/segment_result.json
@@ -0,0 +1,59 @@
+{
+ "status": "segment",
+ "vertices": [
+ [
+ 0.0,
+ 0.0
+ ],
+ [
+ 100.0,
+ 0.0
+ ]
+ ],
+ "tolerance_m": 5.0000000000000004e-08,
+ "diameter_infinite": false,
+ "diameter_m": 100.0,
+ "diameter_pair": [
+ [
+ 0.0,
+ 0.0
+ ],
+ [
+ 100.0,
+ 0.0
+ ]
+ ],
+ "area_m2": 0.0,
+ "diameter_circle": {
+ "center": [
+ 50.0,
+ 0.0
+ ],
+ "radius_m": 50.0,
+ "max_vertex_distance_m": 50.0,
+ "covers": true,
+ "coverage_tolerance_m": 2.0000000000000002e-07,
+ "max_excess_m": 0.0,
+ "outside_vertex_indices": [],
+ "witness_vertex": null
+ },
+ "minimum_enclosing_circle": {
+ "center": [
+ 50.0,
+ 0.0
+ ],
+ "radius_m": 50.0
+ },
+ "guaranteed_clearable_at_mec_center": false,
+ "clearance_certificate": {
+ "model": "angular_intersection_only",
+ "clear_radius_m": 20.0,
+ "numerical_margin_m": 2.0000000000000002e-07,
+ "radius_with_margin_m": 50.0000002,
+ "remaining_margin_m": -30.000000200000002,
+ "center": [
+ 50.0,
+ 0.0
+ ]
+ }
+}
\ No newline at end of file
diff --git a/output/q1/triangle_recomputed.json b/output/q1/triangle_recomputed.json
new file mode 100644
index 0000000..b2eb163
--- /dev/null
+++ b/output/q1/triangle_recomputed.json
@@ -0,0 +1,48 @@
+{
+ "status": "polygon",
+ "vertices": [
+ [
+ -5.755396159656812e-13,
+ 0.0
+ ],
+ [
+ 40.000000000000234,
+ 0.0
+ ],
+ [
+ 19.999999999999847,
+ 34.64101615137823
+ ]
+ ],
+ "tolerance_m": 1.0200653573832087e-06,
+ "diameter_infinite": false,
+ "diameter_m": 40.00000000000081,
+ "diameter_pair": [
+ [
+ -5.755396159656812e-13,
+ 0.0
+ ],
+ [
+ 40.000000000000234,
+ 0.0
+ ]
+ ],
+ "area_m2": 692.8203230275786,
+ "diameter_circle": {
+ "center": [
+ 19.99999999999983,
+ 0.0
+ ],
+ "radius_m": 20.000000000000405,
+ "max_vertex_distance_m": 34.641016151378224,
+ "covers": false
+ },
+ "minimum_enclosing_circle": {
+ "center": [
+ 19.99999999999983,
+ 11.547005383792735
+ ],
+ "radius_m": 23.094010767585495
+ },
+ "guaranteed_clearable_at_mec_center": false
+}
diff --git a/output/q1/triangle_result.json b/output/q1/triangle_result.json
new file mode 100644
index 0000000..2845bd1
--- /dev/null
+++ b/output/q1/triangle_result.json
@@ -0,0 +1,68 @@
+{
+ "status": "polygon",
+ "vertices": [
+ [
+ -5.755396159656812e-13,
+ 0.0
+ ],
+ [
+ 40.000000000000234,
+ 0.0
+ ],
+ [
+ 19.999999999999847,
+ 34.64101615137823
+ ]
+ ],
+ "tolerance_m": 1.0200653573832087e-06,
+ "diameter_infinite": false,
+ "diameter_m": 40.00000000000081,
+ "diameter_pair": [
+ [
+ -5.755396159656812e-13,
+ 0.0
+ ],
+ [
+ 40.000000000000234,
+ 0.0
+ ]
+ ],
+ "area_m2": 692.8203230275786,
+ "diameter_circle": {
+ "center": [
+ 19.99999999999983,
+ 0.0
+ ],
+ "radius_m": 20.000000000000405,
+ "max_vertex_distance_m": 34.641016151378224,
+ "covers": false,
+ "coverage_tolerance_m": 4.080261429532835e-06,
+ "max_excess_m": 14.641016151377823,
+ "outside_vertex_indices": [
+ 2
+ ],
+ "witness_vertex": [
+ 19.999999999999847,
+ 34.64101615137823
+ ]
+ },
+ "minimum_enclosing_circle": {
+ "center": [
+ 19.99999999999983,
+ 11.547005383792735
+ ],
+ "radius_m": 23.094010767585495
+ },
+ "guaranteed_clearable_at_mec_center": false,
+ "clearance_certificate": {
+ "model": "angular_intersection_only",
+ "clear_radius_m": 20.0,
+ "numerical_margin_m": 4.080261429532835e-06,
+ "radius_with_margin_m": 23.094014847846925,
+ "remaining_margin_m": -3.0940148478469247,
+ "center": [
+ 19.99999999999983,
+ 11.547005383792735
+ ]
+ }
+}
\ No newline at end of file
diff --git a/output/q1/visualization_checks.json b/output/q1/visualization_checks.json
new file mode 100644
index 0000000..6aedea4
--- /dev/null
+++ b/output/q1/visualization_checks.json
@@ -0,0 +1,7 @@
+{
+ "interaction_states_checked": 32,
+ "missing_selectors": 0,
+ "nonfinite_svg_values": 0,
+ "browser_layout_verified": false,
+ "note": "Browser local-file navigation blocked by browser URL policy. Event handlers checked in minimal DOM; SVG statically inspected separately."
+}
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 8fadf5a..8cb01ac 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,7 @@
[project]
name = "math"
version = "0.1.0"
-description = "Add your description here"
+description = "B题问题一:带误差方位交会定位与可视化"
readme = "README.md"
-requires-python = ">=3.13"
+requires-python = ">=3.10"
dependencies = []
diff --git a/scripts/benchmark_q1.py b/scripts/benchmark_q1.py
new file mode 100644
index 0000000..a65f56a
--- /dev/null
+++ b/scripts/benchmark_q1.py
@@ -0,0 +1,56 @@
+"""Offline numerical comparison; timings are local measurements, not API latency."""
+
+from dataclasses import replace
+import json
+import math
+from pathlib import Path
+from statistics import median
+import sys
+from time import perf_counter
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT))
+from geometry import Measurement, solve, smallest_enclosing_circle, first_bearing_candidate_disk
+from geometry.localization import _enclosing_circle_reference
+
+
+def timed(function, points, repeats):
+ durations = []
+ for _ in range(repeats):
+ start = perf_counter()
+ result = function(points)
+ durations.append(perf_counter()-start)
+ return result, median(durations)*1000
+
+
+def main():
+ timings = []
+ for size in [4, 20, 60]:
+ points = [(20*math.cos(i*math.tau/size), 20*math.sin(i*math.tau/size)) for i in range(size)]
+ old, old_ms = timed(_enclosing_circle_reference, points, 3)
+ new, new_ms = timed(smallest_enclosing_circle, points, 11)
+ timings.append(dict(vertices=size, reference_ms=old_ms, incremental_ms=new_ms,
+ speedup=old_ms/new_ms, radius_difference_m=abs(old[1]-new[1])))
+ source = next((ROOT/'output/q1/practice').glob('*_observations.json'))
+ rows = json.loads(source.read_text(encoding='utf-8'))['measurements']
+ observations = [Measurement(**m) for m in rows]
+ sensitivity = []
+ for margin in [0, 0.005]:
+ ms = [replace(m, half_angle_deg=m.half_angle_deg+margin) for m in observations]
+ result, duration = timed(solve, ms, 31)
+ sensitivity.append(dict(additional_half_angle_deg=margin, solve_ms=duration,
+ diameter_m=result['diameter_m'],
+ mec_radius_m=result['minimum_enclosing_circle']['radius_m'],
+ diameter_circle_excess_m=result['diameter_circle']['max_excess_m'],
+ clearance_margin_m=result['clearance_certificate']['remaining_margin_m']))
+ report = dict(python=sys.version.split()[0], timing_method='median perf_counter; warm local process',
+ timing_scope='MEC only; not whole solver speedup or simulator/network performance',
+ circle_benchmarks=timings, practice_sensitivity=sensitivity,
+ q2_first_bearing_certificate=first_bearing_candidate_disk(Measurement(0, 0, 0, 1.005)))
+ output = ROOT/'output/q1/optimization_report.json'
+ output.write_text(json.dumps(report, ensure_ascii=False, indent=2, allow_nan=False)+'\n', encoding='utf-8')
+ print(json.dumps(report, ensure_ascii=False, indent=2))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/build_q1_demo.py b/scripts/build_q1_demo.py
new file mode 100644
index 0000000..dcb6a8d
--- /dev/null
+++ b/scripts/build_q1_demo.py
@@ -0,0 +1,71 @@
+"""Build reproducible examples and the interactive visualization from Python results."""
+
+from dataclasses import asdict
+import json
+import math
+from pathlib import Path
+import sys
+
+ROOT=Path(__file__).resolve().parents[1]
+sys.path.insert(0,str(ROOT))
+from geometry import Measurement, solve
+
+
+def measured(stations, target, errors):
+ return [Measurement(x,y,(math.degrees(math.atan2(target[1]-y,target[0]-x))+e)%360)
+ for (x,y),e in zip(stations,errors)]
+
+
+def scenarios():
+ target=(80,90)
+ normal=measured([(-600,-250),(650,-100),(100,700)],target,[.65,-.55,.35])
+ h=20*math.sqrt(3)
+ triangle=[Measurement(-1000,0,1),Measurement(540,-500*math.sqrt(3),121),
+ Measurement(520,h+500*math.sqrt(3),241)]
+ return [
+ dict(id='normal',name='三点交会',source='synthetic',target=target,measurements=normal),
+ dict(id='triangle',name='直径圆反例',source='synthetic',target=(20,h/3),measurements=triangle),
+ dict(id='poor',name='近共线检测点',source='synthetic',target=(900,50),
+ measurements=measured([(0,0),(100,0),(200,0)],(900,50),[0,0,0])),
+ dict(id='empty',name='不相容观测',source='synthetic',target=None,
+ measurements=[Measurement(-100,0,180),Measurement(100,0,0)]),
+ dict(id='segment',name='零误差退化线段',source='synthetic',target=(40,0),
+ measurements=[Measurement(0,0,0,0),Measurement(100,0,180,0)]),
+ ]
+
+
+def main():
+ cases=scenarios()
+ practice=ROOT/'output/q1/practice'
+ for path in sorted(practice.glob('*_observations.json')):
+ doc=json.loads(path.read_text(encoding='utf-8'))
+ cases.append(dict(id='practice-'+doc['case_code'],name='官方演练 '+doc['case_code'],
+ source='official_practice',target=None,
+ measurements=[Measurement(**m) for m in doc['measurements']]))
+ examples=ROOT/'examples/q1'
+ results=ROOT/'output/q1'
+ examples.mkdir(parents=True,exist_ok=True)
+ results.mkdir(parents=True,exist_ok=True)
+ dataset=[]
+ for case in cases:
+ ms=case['measurements']
+ frames=[solve(ms[:n]) for n in range(1,len(ms)+1)]
+ data={**case,'measurements':[asdict(m) for m in ms],'frames':frames}
+ dataset.append(data)
+ if case['source']=='synthetic':
+ (examples/(case['id']+'.json')).write_text(json.dumps({k:v for k,v in data.items() if k!='frames'},ensure_ascii=False,indent=2),encoding='utf-8')
+ (results/(case['id']+'_result.json')).write_text(json.dumps(frames[-1],ensure_ascii=False,indent=2,allow_nan=False),encoding='utf-8')
+ (results/'demo_data.json').write_text(json.dumps(dataset,ensure_ascii=False,indent=2,allow_nan=False),encoding='utf-8')
+ template=(ROOT/'visualization/q1.template.html').read_text(encoding='utf-8')
+ fragment=template.replace('/*__Q1_DATA__*/',json.dumps(dataset,ensure_ascii=False,allow_nan=False))
+ (ROOT/'visualization/question1.html').write_text(fragment,encoding='utf-8')
+ # A standalone companion uses the same fragment and computation results.
+ theme=(ROOT/'visualization/standalone.css').read_text(encoding='utf-8')
+ page='B题问题一交会定位'+fragment+''
+ (results/'question1.html').write_text(page,encoding='utf-8')
+ print(json.dumps({'cases':len(dataset),'fragment_bytes':len(fragment.encode('utf-8')),
+ 'standalone':str(results/'question1.html')},ensure_ascii=False))
+
+
+if __name__=='__main__':
+ main()
diff --git a/scripts/capture_practice.py b/scripts/capture_practice.py
new file mode 100644
index 0000000..a9fbc88
--- /dev/null
+++ b/scripts/capture_practice.py
@@ -0,0 +1,123 @@
+"""Collect a few bearings for Q1 from an ALREADY OPEN practice session.
+
+There is deliberately no test-start API, formal mode, or /clear call here.
+The documented robot API does not expose mode: verify the UI before running.
+"""
+
+import argparse
+import json
+import math
+from pathlib import Path
+import sys
+import time
+import uuid
+from urllib.error import URLError
+from urllib.request import Request, urlopen
+
+sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
+from geometry import Measurement, solve
+
+
+class PracticeClient:
+ def __init__(self, robot_id, output, base_url='http://127.0.0.1:2026'):
+ self.robot_id = robot_id
+ self.output = output
+ self.base_url = base_url.rstrip('/')
+ self.prefix = uuid.uuid4().hex[:12]
+ self.count = 0
+ self.events = []
+ self.deadline = None
+
+ def action(self, path, **extra):
+ if path not in ('/enter', '/measure', '/exit'):
+ raise ValueError('采集器只允许进入、检测、退出')
+ if self.deadline is not None and path != '/exit' and time.monotonic() > self.deadline-15:
+ raise TimeoutError('剩余时间不足,预留退出时间')
+ self.count += 1
+ payload = dict(arena_id='default', robot_id=self.robot_id,
+ request_id=f'q1-{self.prefix}-{self.count}', **extra)
+ encoded = json.dumps(payload, allow_nan=False).encode('utf-8')
+ for attempt in range(3):
+ request = Request(self.base_url+path, data=encoded,
+ headers={'Content-Type':'application/json'}, method='POST')
+ try:
+ with urlopen(request, timeout=5) as response:
+ status = response.status
+ body = json.load(response)
+ # A retried action is recorded/consumed only once.
+ self.events.append(dict(path=path, request=payload, response=body, retries=attempt))
+ self.output.write_text(json.dumps(self.events,ensure_ascii=False,indent=2),encoding='utf-8')
+ if status != 200 or body.get('accepted') is not True:
+ raise RuntimeError(f'动作未被接受: {status}, {body}')
+ if path == '/enter':
+ self.deadline = time.monotonic()+body['remaining_real_duration_s']
+ return body
+ except URLError:
+ if attempt == 2:
+ raise
+ time.sleep(.25)
+
+ def measure(self, p, channel):
+ return self.action('/measure',position={'x':p[0],'y':p[1]},channel=channel)
+
+
+def main():
+ parser=argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--robot-id',required=True)
+ parser.add_argument('--practice-confirmed',action='store_true',required=True,
+ help='仅在界面确认为问题3演练且接口已就绪时提供')
+ parser.add_argument('--case-code',required=True,help='界面显示的演练案例编码')
+ parser.add_argument('--output',type=Path,default=Path('output/q1/practice'))
+ args=parser.parse_args()
+ args.output.mkdir(parents=True,exist_ok=True)
+ log_path=args.output/f'{args.case_code}_actions.json'
+ if log_path.exists():
+ parser.error('同名采集日志已存在,请使用新的输出目录,避免覆盖证据')
+ client=PracticeClient(args.robot_id,log_path)
+ observations=[]
+ channel=None
+ first=None
+ entered=False
+ repeat=None
+ try:
+ client.action('/enter')
+ entered=True
+ for p in [(0,0),(800,0),(-800,0),(0,800),(0,-800)]:
+ for c in range(1,21):
+ response=client.measure(p,c)
+ if response['measure_result']=='direction':
+ first=(p,response['svd_deg'])
+ observations.append(Measurement(*p,response['svd_deg']))
+ channel=c
+ break
+ if channel is not None:
+ break
+ if channel is None:
+ raise RuntimeError('预设采样点未取得direction;本次不扩展为全域搜索')
+ p,theta=first
+ repeat=client.measure(p,channel)
+ a=math.radians(theta)
+ # For an omnidirectional source seen initially at <=1500m, these
+ # 700m-forward, +/-200m-lateral points remain within 1000m of its wedge.
+ for lateral in [200,-200,300,-300]:
+ q=(p[0]+700*math.cos(a)-lateral*math.sin(a),
+ p[1]+700*math.sin(a)+lateral*math.cos(a))
+ response=client.measure(q,channel)
+ if response['measure_result']=='direction':
+ observations.append(Measurement(*q,response['svd_deg']))
+ if len(observations)>=3:
+ break
+ data={'name':'官方问题3演练观测','source':'official_practice','case_code':args.case_code,
+ 'channel':channel,'ground_truth':None,'measurements':[m.__dict__ for m in observations],
+ 'repeated_station_response':repeat,'result':solve(observations),
+ 'scope':'仅采集问题一观测,不执行问题三全清除任务;没有调用clear'}
+ path=args.output/f'{args.case_code}_observations.json'
+ path.write_text(json.dumps(data,ensure_ascii=False,indent=2,allow_nan=False),encoding='utf-8')
+ print(json.dumps({'observations':str(path),'count':len(observations),'result':data['result']},ensure_ascii=False,indent=2))
+ finally:
+ if entered:
+ client.action('/exit')
+
+
+if __name__=='__main__':
+ main()
diff --git a/scripts/render_q1_figures.py b/scripts/render_q1_figures.py
new file mode 100644
index 0000000..1b3b298
--- /dev/null
+++ b/scripts/render_q1_figures.py
@@ -0,0 +1,94 @@
+"""Rasterize our restricted SVG primitives with Pillow, without a browser.
+
+This inspects geometry/labels at both widths/themes. It does not emulate HTML/CSS.
+"""
+import math
+from pathlib import Path
+import re
+import xml.etree.ElementTree as ET
+from PIL import Image,ImageDraw,ImageFont
+
+ROOT=Path(__file__).resolve().parents[1]
+S=2
+FONT=ImageFont.truetype('C:/Windows/Fonts/msyh.ttc',12*S)
+
+
+def render(path):
+ root=ET.fromstring(path.read_text(encoding='utf-8'))
+ width,height=map(float,(root.get('width'),root.get('height')))
+ bg='#191c21' if 'dark' in path.stem else '#ffffff'
+ image=Image.new('RGBA',(round(width*S),round(height*S)),bg)
+ clip=next(el for el in root.iter() if el.tag.endswith('clipPath'))[0]
+ clipbox=tuple(float(clip.get(k))*S for k in ('x','y','width','height'))
+
+ def color(value,opacity=1):
+ if value in (None,'none'): return None
+ value=value.lstrip('#')
+ return tuple(int(value[i:i+2],16) for i in (0,2,4))+(round(opacity*255),)
+
+ def draw(el,clipped=False):
+ nonlocal image
+ tag=el.tag.rsplit('}',1)[-1]
+ if tag in ('defs','title','desc'):return
+ if tag in ('svg','g'):
+ for child in el:draw(child,clipped or bool(el.get('clip-path')))
+ return
+ layer=Image.new('RGBA',image.size,(0,0,0,0));d=ImageDraw.Draw(layer)
+ num=lambda key,default=0:float(el.get(key,default))*S
+ fill=color(el.get('fill'),float(el.get('fill-opacity',1)))
+ stroke=color(el.get('stroke'),float(el.get('stroke-opacity',1)))
+ sw=max(1,round(num('stroke-width',1)))
+ if tag=='line':
+ xy=[(num('x1'),num('y1')),(num('x2'),num('y2'))]
+ dash=el.get('stroke-dasharray','none')
+ if dash=='none': d.line(xy,fill=stroke,width=sw)
+ else:
+ a,b=xy;dist=math.dist(a,b);unit=((b[0]-a[0])/max(dist,1e-15),(b[1]-a[1])/max(dist,1e-15))
+ on,off=[float(v)*S for v in dash.split()]
+ # Bound offscreen ray work to the finite exported viewport vicinity.
+ segments=min(100000,int(dist/(on+off))+1)
+ for i in range(segments):
+ lo=i*(on+off);hi=min(lo+on,dist)
+ d.line([(a[0]+lo*unit[0],a[1]+lo*unit[1]),(a[0]+hi*unit[0],a[1]+hi*unit[1])],fill=stroke,width=sw)
+ elif tag=='rect':
+ box=(num('x'),num('y'),num('x')+num('width'),num('y')+num('height'))
+ d.rectangle(box,fill=fill,outline=stroke,width=sw)
+ elif tag=='circle':
+ x,y,r=num('cx'),num('cy'),num('r');box=(x-r,y-r,x+r,y+r)
+ if el.get('stroke-dasharray') not in (None,'none'):
+ for a in range(0,360,18):d.arc(box,a,a+10,fill=stroke,width=sw)
+ else:d.ellipse(box,fill=fill,outline=stroke,width=sw)
+ elif tag=='polygon':
+ points=[tuple(float(v)*S for v in pair.split(',')) for pair in el.get('points').split()]
+ d.polygon(points,fill=fill)
+ if stroke:d.line(points+[points[0]],fill=stroke,width=sw,joint='curve')
+ elif tag=='path':
+ for x,y,axis,length in re.findall(r'M([\d.e+-]+),([\d.e+-]+)([hv])([\d.e+-]+)',el.get('d')):
+ x,y,length=float(x)*S,float(y)*S,float(length)*S
+ d.line([(x,y),(x+length,y) if axis=='h' else (x,y+length)],fill=stroke,width=sw)
+ elif tag=='text':
+ value=el.text or '';x,y=num('x'),num('y');anchor=el.get('text-anchor','start')
+ length=d.textlength(value,font=FONT)
+ if el.get('transform'):
+ tile=Image.new('RGBA',(math.ceil(length+8*S),20*S));td=ImageDraw.Draw(tile)
+ td.text((4*S,0),value,font=FONT,fill=fill)
+ tile=tile.rotate(90,expand=True)
+ layer.alpha_composite(tile,(round(x-tile.width/2),round(y-tile.height/2)))
+ else:
+ x-=length/2 if anchor=='middle' else length if anchor=='end' else 0
+ d.text((x,y),value,font=FONT,fill=fill,anchor='ls')
+ else:raise ValueError('unsupported SVG primitive '+tag)
+ if clipped:
+ x,y,w,h=clipbox
+ mask=Image.new('L',image.size);ImageDraw.Draw(mask).rectangle((x,y,x+w,y+h),fill=255)
+ transparent=Image.new('RGBA',image.size)
+ layer=Image.composite(layer,transparent,mask)
+ image=Image.alpha_composite(image,layer)
+
+ draw(root)
+ output=path.with_suffix('.png');image.convert('RGB').save(output)
+ return str(output.relative_to(ROOT))
+
+
+if __name__=='__main__':
+ for path in sorted((ROOT/'output/q1/figures').glob('*.svg')):print(render(path))
diff --git a/scripts/verify_visualization.cjs b/scripts/verify_visualization.cjs
new file mode 100644
index 0000000..8689f40
--- /dev/null
+++ b/scripts/verify_visualization.cjs
@@ -0,0 +1,61 @@
+/* Run only our generated local script in a minimal DOM, with no browser or network.
+ * This checks event handlers and serializes the produced SVG for static inspection.
+ * It is not a replacement for a native browser layout test.
+ */
+const fs=require('node:fs'),path=require('node:path'),vm=require('node:vm'),assert=require('node:assert');
+const rootDir=path.resolve(__dirname,'..');
+const source=fs.readFileSync(path.join(rootDir,'visualization/question1.html'),'utf8');
+const script=source.match(/
+
diff --git a/visualization/question1.html b/visualization/question1.html
new file mode 100644
index 0000000..d8627ec
--- /dev/null
+++ b/visualization/question1.html
@@ -0,0 +1,185 @@
+
+
+
带误差的交会定位
+
+
+
+
+
+
+
+
+ 定位区域
+ 直径线段及直径圆
+ 最小包围圆
+ □ 检测点 + 合成真值 ◎ 覆盖反例顶点
+
+
+
+ 检测点与示向度
+
+
+ | 测点 | x / m | y / m | 示向度 / ° | 误差半角 / ° |
+
+
+
+
+
+
diff --git a/visualization/standalone.css b/visualization/standalone.css
new file mode 100644
index 0000000..98c496c
--- /dev/null
+++ b/visualization/standalone.css
@@ -0,0 +1,25 @@
+:root {
+ color-scheme:light dark;
+ --background:light-dark(#ffffff,#191c21);
+ --foreground:light-dark(#24282e,#eceff3);
+ --border:light-dark(#d9dfe5,#424a55);
+ --muted-foreground:light-dark(#56616c,#b4bdc8);
+ --viz-series-1:light-dark(#146e9a,#6cc8ed);
+ --viz-series-2:light-dark(#c6503a,#ff947d);
+ --viz-series-3:light-dark(#198064,#64d6ac);
+ --font-size-base:14px;
+}
+body { max-width:1000px; margin:0 auto; padding:24px; background:var(--background);color:var(--foreground);font:var(--font-size-base)/1.6 system-ui,"Microsoft YaHei",sans-serif; }
+h2 {font-size:1.4em;font-weight:500;margin-block:0 18px;}
+.viz-controls {display:flex;gap:16px;flex-wrap:wrap;align-items:end;}
+.form-label {display:flex;flex-direction:column;gap:6px;flex:1 1 180px;min-width:0;}
+.form-select {font:inherit;color:var(--foreground);background:var(--background);border:1px solid var(--border);padding:8px;max-width:100%;border-radius:5px;}
+.form-range {width:100%;min-height:30px;accent-color:var(--viz-series-1);}
+.text-small {font-size:12px;}
+.tabular-nums {font-variant-numeric:tabular-nums;}
+.table-responsive {overflow-x:auto;}
+.table {width:100%;border-collapse:collapse;}
+.table th,.table td {padding:6px 8px;border-bottom:1px solid var(--border);}
+.table th {font-weight:500;text-align:left;}
+.table .text-end {text-align:right;}
+@media(max-width:480px) {body{padding:12px;}.form-label{flex-basis:100%;}.form-select{font-size:16px;}}