第一问建模完成
@@ -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. 正式测试时间安排,务必尽早演练并保留正式机会。
|
||||
|
||||
下一步最有价值的工作仍然是:**协议计时基线 + 几何内核 + 全向闭环**。完成这条链路后,再用演练数据决定是否加入加速策略。
|
||||
@@ -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加速投入。
|
||||
|
||||
@@ -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` 和本局案例编码。参见完整解答中的采集说明。
|
||||
|
||||
@@ -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。
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"]
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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}
|
||||
@@ -1,5 +1,4 @@
|
||||
def main():
|
||||
print("Hello from math!")
|
||||
from geometry.__main__ import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"status": "empty",
|
||||
"vertices": [],
|
||||
"diameter_m": null,
|
||||
"diameter_infinite": false,
|
||||
"tolerance_m": 1.0000000000000001e-07,
|
||||
"message": "测向角域没有公共点;检查频道、数据及误差界"
|
||||
}
|
||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
@@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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": "存在非零衰退方向,区域无界,直径为无穷大"
|
||||
}
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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()
|
||||
@@ -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='<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>B题问题一交会定位</title><style>'+theme+'</style><body>'+fragment+'</body></html>'
|
||||
(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()
|
||||
@@ -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()
|
||||
@@ -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))
|
||||
@@ -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(/<script>([\s\S]*?)<\/script>/)[1];
|
||||
class Element {
|
||||
constructor(tag) {this.tag=tag;this.attrs={};this.children=[];this.style={};this.events={};this.value='';this.max='3';this.textContent='';}
|
||||
setAttribute(k,v){this.attrs[k]=String(v);}
|
||||
appendChild(x){this.children.push(x);return x;}
|
||||
replaceChildren(...x){this.children=x;this.textContent='';}
|
||||
addEventListener(k,f){this.events[k]=f;}
|
||||
add(o){this.children.push(o);}
|
||||
getBoundingClientRect(){return {width:width};}
|
||||
}
|
||||
const esc=s=>String(s).replace(/&/g,'&').replace(/</g,'<').replace(/"/g,'"');
|
||||
const palette={light:{'--foreground':'#24282e','--border':'#d9dfe5','--viz-series-1':'#146e9a','--viz-series-2':'#c6503a','--viz-series-3':'#198064'},dark:{'--foreground':'#eceff3','--border':'#424a55','--viz-series-1':'#6cc8ed','--viz-series-2':'#ff947d','--viz-series-3':'#64d6ac'}};
|
||||
function serialize(el,theme){
|
||||
if(el.tag==='text')Object.assign(el.attrs,{fill:palette[theme]['--foreground'],'font-size':12,'font-family':'Microsoft YaHei, sans-serif'});
|
||||
const attrs=Object.entries(el.attrs).map(([k,v])=>`${k}="${esc(String(v).replace(/var\(([^)]+)\)/g,(_,n)=>palette[theme][n]||n))}"`).join(' ');
|
||||
return `<${el.tag} ${attrs}>${esc(el.textContent)}${el.children.map(x=>serialize(x,theme)).join('')}</${el.tag}>`;
|
||||
}
|
||||
function text(el){return el.textContent+el.children.map(text).join(' ');}
|
||||
const elements={};
|
||||
for(const key of ['case','step','view','plot','step-label','stats','status','rows'])elements[key]=new Element(key==='plot'?'svg':'div');
|
||||
elements.view.value='region';
|
||||
const root=new Element('div');
|
||||
root.querySelector=s=>{const m=s.match(/data-q1="([^"]+)"/);assert(m&&elements[m[1]],'missing selector '+s);return elements[m[1]];};
|
||||
const document={getElementById:id=>{assert.equal(id,'q1-localization');return root;},createElement:tag=>new Element(tag),createElementNS:(_,tag)=>new Element(tag)};
|
||||
let width=736;
|
||||
vm.runInNewContext(script,{document,Option:function(label,value){this.label=label;this.value=String(value);},ResizeObserver:class{constructor(f){this.f=f;}observe(){}},Number,Math,String,Object,console},{timeout:5000});
|
||||
const output=path.join(rootDir,'output/q1/figures');fs.mkdirSync(output,{recursive:true});
|
||||
let checks=0;
|
||||
for(let i=0;i<elements.case.children.length;i++) {
|
||||
elements.case.value=String(i);elements.case.events.change();
|
||||
for(let n=1;n<=+elements.step.max;n++) {
|
||||
elements.step.value=String(n);elements.step.events.input();
|
||||
assert.equal(elements['step-label'].textContent,`${n} / ${elements.step.max}`);
|
||||
for(const v of ['global','region']) {
|
||||
elements.view.value=v;elements.view.events.change();
|
||||
const svg=serialize(elements.plot,'light');
|
||||
assert(!/NaN|undefined|Infinity/.test(svg));
|
||||
assert(svg.includes('data-axis="x"')&&svg.includes('data-axis="y"'));
|
||||
checks++;
|
||||
}
|
||||
}
|
||||
}
|
||||
for(const w of [360,736])for(const theme of ['light','dark'])for(const index of [1,elements.case.children.length-1]) {
|
||||
width=w;elements.case.value=String(index);elements.case.events.change();
|
||||
elements.view.value='region';elements.view.events.change();
|
||||
const height=elements.plot.attrs.viewBox.split(' ')[3];
|
||||
const svg=serialize(elements.plot,theme).replace('<svg ',`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${height}" `);
|
||||
const filename=(index===1?'counterexample':'practice')+`-${w}-${theme}.svg`;
|
||||
fs.writeFileSync(path.join(output,filename),svg,'utf8');
|
||||
}
|
||||
const report={interaction_states_checked:checks,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.'};
|
||||
fs.writeFileSync(path.join(rootDir,'output/q1/visualization_checks.json'),JSON.stringify(report,null,2));
|
||||
console.log(report);
|
||||
@@ -0,0 +1,131 @@
|
||||
import math
|
||||
import random
|
||||
import unittest
|
||||
|
||||
from geometry import Measurement, solve, smallest_enclosing_circle
|
||||
from geometry.localization import halfplanes, distance
|
||||
|
||||
|
||||
class GeometryTests(unittest.TestCase):
|
||||
def test_equilateral_from_legal_bearings(self):
|
||||
h = 20 * math.sqrt(3)
|
||||
stations = [Measurement(-1000, 0, 1),
|
||||
Measurement(540, -500 * math.sqrt(3), 121),
|
||||
Measurement(520, h + 500 * math.sqrt(3), 241)]
|
||||
result = solve(stations)
|
||||
self.assertEqual(result['status'], 'polygon')
|
||||
self.assertEqual(len(result['vertices']), 3)
|
||||
self.assertAlmostEqual(result['diameter_m'], 40, places=7)
|
||||
self.assertAlmostEqual(result['minimum_enclosing_circle']['radius_m'], 40/math.sqrt(3), places=7)
|
||||
self.assertFalse(result['diameter_circle']['covers'])
|
||||
self.assertFalse(result['guaranteed_clearable_at_mec_center'])
|
||||
|
||||
def test_no_observations_and_single_bearing(self):
|
||||
self.assertEqual(solve([])['status'], 'unbounded')
|
||||
r = solve([Measurement(20, 30, 359.8)])
|
||||
self.assertEqual(r['status'], 'unbounded')
|
||||
self.assertTrue(r['diameter_infinite'])
|
||||
self.assertIsNone(r['diameter_m'])
|
||||
|
||||
def test_inconsistent(self):
|
||||
result = solve([Measurement(-100, 0, 180), Measurement(100, 0, 0)])
|
||||
self.assertEqual(result['status'], 'empty')
|
||||
|
||||
def test_exact_segment_and_point(self):
|
||||
segment = [Measurement(0, 0, 0, 0), Measurement(100, 0, 180, 0)]
|
||||
r = solve(segment)
|
||||
self.assertEqual(r['status'], 'segment')
|
||||
self.assertAlmostEqual(r['diameter_m'], 100)
|
||||
self.assertTrue(r['diameter_circle']['covers'])
|
||||
r = solve(segment + [Measurement(30, -30, 90, 0)])
|
||||
self.assertEqual(r['status'], 'point')
|
||||
self.assertAlmostEqual(r['vertices'][0][0], 30)
|
||||
self.assertAlmostEqual(r['diameter_m'], 0)
|
||||
|
||||
def test_forward_ray_not_line(self):
|
||||
r = solve([Measurement(0, 0, 0, 0), Measurement(-1, 0, 180, 0)])
|
||||
self.assertEqual(r['status'], 'empty')
|
||||
|
||||
def test_angle_wrap_and_repeated_station(self):
|
||||
a = [Measurement(-500, -1, 359.9), Measurement(0, -500, 90)]
|
||||
b = [Measurement(-500, -1, -.1), Measurement(0, -500, 450)]
|
||||
self.assertAlmostEqual(solve(a)['diameter_m'], solve(b)['diameter_m'])
|
||||
self.assertAlmostEqual(solve(a+a)['diameter_m'], solve(a)['diameter_m'])
|
||||
|
||||
def test_close_parallel_remains_finite(self):
|
||||
# Two nearly parallel wedges can still be bounded, very far away.
|
||||
r = solve([Measurement(0, 0, 1), Measurement(0, 100, -1.000001)])
|
||||
self.assertEqual(r['status'], 'polygon')
|
||||
self.assertGreater(r['diameter_m'], 1e9)
|
||||
|
||||
def test_circle_known_shapes(self):
|
||||
c, r = smallest_enclosing_circle([(0,0),(2,0),(2,2),(0,2)])
|
||||
self.assertEqual(c, (1,1))
|
||||
self.assertAlmostEqual(r, math.sqrt(2))
|
||||
c, r = smallest_enclosing_circle([(0,0),(2,0),(.4,.2)])
|
||||
self.assertAlmostEqual(r, 1)
|
||||
c, r = smallest_enclosing_circle([(0,0),(1,0),(3,0)])
|
||||
self.assertAlmostEqual(r, 1.5)
|
||||
|
||||
def test_random_bounded_truth_containment_and_monotonicity(self):
|
||||
rng = random.Random(20260910)
|
||||
for _ in range(100):
|
||||
target = (rng.uniform(-300,300), rng.uniform(-300,300))
|
||||
ms = []
|
||||
for i in range(4):
|
||||
ang = (i*90 + rng.uniform(-20,20))*math.pi/180
|
||||
radius = rng.uniform(300,1200)
|
||||
p = (target[0]+radius*math.cos(ang), target[1]+radius*math.sin(ang))
|
||||
bearing = math.degrees(math.atan2(target[1]-p[1],target[0]-p[0]))
|
||||
ms.append(Measurement(*p, bearing+rng.uniform(-.99,.99)))
|
||||
r3, r4 = solve(ms[:3]), solve(ms)
|
||||
self.assertEqual(r4['status'], 'polygon')
|
||||
self.assertLessEqual(r4['diameter_m'], r3['diameter_m']+1e-5)
|
||||
for m in ms:
|
||||
for a,b,c in halfplanes(m):
|
||||
self.assertLessEqual(a*target[0]+b*target[1], c+1e-7)
|
||||
mec = r4['minimum_enclosing_circle']
|
||||
self.assertLessEqual(distance(mec['center'], target), mec['radius_m']+1e-5)
|
||||
for v in r4['vertices']:
|
||||
self.assertLessEqual(distance(mec['center'], v), mec['radius_m']+1e-5)
|
||||
|
||||
def test_translation_rotation_scaling(self):
|
||||
ms = [Measurement(-500,0,1), Measurement(0,-500,89), Measurement(400,400,225)]
|
||||
original = solve(ms)
|
||||
a=.37
|
||||
changed = [Measurement(1e6+3*(m.x*math.cos(a)-m.y*math.sin(a)),
|
||||
-2e6+3*(m.x*math.sin(a)+m.y*math.cos(a)),
|
||||
m.bearing_deg+math.degrees(a)) for m in ms]
|
||||
moved=solve(changed)
|
||||
self.assertAlmostEqual(moved['diameter_m'], original['diameter_m']*3, places=6)
|
||||
|
||||
def test_invalid_input(self):
|
||||
for args in [(float('nan'),0,0),(0,0,float('inf')),(0,0,0,90),(0,0,0,-1),(True,0,0)]:
|
||||
with self.assertRaises(ValueError): Measurement(*args)
|
||||
|
||||
def test_independent_rectangle_clipping_reference(self):
|
||||
# A second construction: clip a demonstrably inactive finite box.
|
||||
# The production algorithm never uses this box.
|
||||
rng=random.Random(4001)
|
||||
for _ in range(60):
|
||||
ms=[]
|
||||
for x,y in [(-500,-300),(600,-200),(0,700)]:
|
||||
ms.append(Measurement(x,y,math.degrees(math.atan2(-y,-x))+rng.uniform(-.9,.9)))
|
||||
poly=[(-2000.,-2000.),(2000.,-2000.),(2000.,2000.),(-2000.,2000.)]
|
||||
for m in ms:
|
||||
for a,b,c in halfplanes(m):
|
||||
out=[]
|
||||
for p,q in zip(poly,poly[1:]+poly[:1]):
|
||||
f=a*p[0]+b*p[1]-c;g=a*q[0]+b*q[1]-c
|
||||
if f<=0:out.append(p)
|
||||
if f*g<0:
|
||||
t=f/(f-g);out.append((p[0]+t*(q[0]-p[0]),p[1]+t*(q[1]-p[1])))
|
||||
poly=out
|
||||
self.assertTrue(poly)
|
||||
self.assertTrue(all(max(abs(x),abs(y))<1900 for x,y in poly))
|
||||
expected=max(math.dist(p,q) for p in poly for q in poly)
|
||||
self.assertAlmostEqual(solve(ms)['diameter_m'],expected,places=6)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,107 @@
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from geometry import Measurement, solve, smallest_enclosing_circle, first_bearing_candidate_disk
|
||||
from geometry.localization import _enclosing_circle_reference, distance
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class CertificateTests(unittest.TestCase):
|
||||
def test_incremental_circle_against_exhaustive_reference(self):
|
||||
rng = random.Random(271828)
|
||||
for _ in range(250):
|
||||
points = [(rng.uniform(-20, 20), rng.uniform(-20, 20))
|
||||
for _ in range(rng.randrange(1, 20))]
|
||||
expected_c, expected_r = _enclosing_circle_reference(points)
|
||||
center, radius = smallest_enclosing_circle(points)
|
||||
self.assertAlmostEqual(radius, expected_r, places=7)
|
||||
self.assertLess(distance(center, expected_c), 1e-6)
|
||||
self.assertTrue(all(distance(center, p) <= radius for p in points))
|
||||
|
||||
def test_circle_large_cocircular_duplicate_and_collinear_sets(self):
|
||||
# More than Python's recursion limit, with analytically known answers.
|
||||
points = [(1e6+30*math.cos(i*math.tau/1500),
|
||||
-2e6+30*math.sin(i*math.tau/1500)) for i in range(1500)]
|
||||
center, radius = smallest_enclosing_circle(points+points[:100])
|
||||
self.assertLess(distance(center, (1e6, -2e6)), 1e-7)
|
||||
self.assertAlmostEqual(radius, 30, places=7)
|
||||
c, r = smallest_enclosing_circle([(i, i*2) for i in range(1500)])
|
||||
self.assertAlmostEqual(r, 1499*math.sqrt(5)/2)
|
||||
self.assertEqual(c, (749.5, 1499))
|
||||
self.assertEqual(smallest_enclosing_circle([(3, 4)]*10), ((3, 4), 0))
|
||||
|
||||
def test_witness_and_clearance_margin_from_saved_observations(self):
|
||||
path = next((ROOT/'output/q1/practice').glob('*_observations.json'))
|
||||
rows = json.loads(path.read_text(encoding='utf-8'))['measurements']
|
||||
result = solve([Measurement(**m) for m in rows])
|
||||
circle = result['diameter_circle']
|
||||
self.assertFalse(circle['covers'])
|
||||
self.assertIn(circle['witness_vertex'], result['vertices'])
|
||||
self.assertAlmostEqual(distance(circle['witness_vertex'], circle['center'])-circle['radius_m'],
|
||||
circle['max_excess_m'])
|
||||
for i in circle['outside_vertex_indices']:
|
||||
self.assertGreater(distance(result['vertices'][i], circle['center']),
|
||||
circle['radius_m']+circle['coverage_tolerance_m'])
|
||||
cert = result['clearance_certificate']
|
||||
self.assertGreater(cert['remaining_margin_m'], 7.9)
|
||||
self.assertTrue(result['guaranteed_clearable_at_mec_center'])
|
||||
self.assertAlmostEqual(cert['remaining_margin_m']+cert['radius_with_margin_m'], 20)
|
||||
|
||||
def test_clearance_at_threshold_is_conservative(self):
|
||||
for length, expected in [(39.999, True), (40, False), (40.001, False)]:
|
||||
r = solve([Measurement(0, 0, 0, 0), Measurement(length, 0, 180, 0)])
|
||||
self.assertEqual(r['guaranteed_clearable_at_mec_center'], expected)
|
||||
self.assertTrue(r['diameter_circle']['covers'])
|
||||
self.assertIsNone(r['diameter_circle']['witness_vertex'])
|
||||
|
||||
def test_first_sector_disk_covers_all_angles_and_distances(self):
|
||||
for angle in [0, 359.9, -180]:
|
||||
for delta in [0, 1, 1.005, 45]:
|
||||
m = Measurement(-800, 1200, angle, delta)
|
||||
cert = first_bearing_candidate_disk(m)
|
||||
enclosing = cert['sector_enclosing_circle']
|
||||
candidate = cert['sufficient_candidate_disk']
|
||||
for j in range(21):
|
||||
for k in range(11):
|
||||
phi = math.radians(angle-delta+2*delta*j/20)
|
||||
p = (m.x+150*k*math.cos(phi), m.y+150*k*math.sin(phi))
|
||||
self.assertLessEqual(distance(p, enclosing['center']), enclosing['radius_m']+1e-9)
|
||||
if candidate:
|
||||
for t in range(8):
|
||||
q = (candidate['center'][0]+candidate['radius_m']*math.cos(t*math.tau/8),
|
||||
candidate['center'][1]+candidate['radius_m']*math.sin(t*math.tau/8))
|
||||
self.assertLessEqual(distance(p, q), 1000+1e-9)
|
||||
if delta <= 1.005:
|
||||
self.assertTrue(cert['candidate_nonempty_certified'])
|
||||
self.assertGreater(candidate['radius_m'], 249.88)
|
||||
|
||||
def test_candidate_equality_and_no_certificate(self):
|
||||
equality = first_bearing_candidate_disk(Measurement(0, 0, 0, 0), max_range_m=2000)
|
||||
self.assertTrue(equality['candidate_nonempty_certified'])
|
||||
self.assertEqual(equality['sufficient_candidate_disk']['radius_m'], 0)
|
||||
no_certificate = first_bearing_candidate_disk(Measurement(0, 0, 0), max_range_m=3000)
|
||||
self.assertIsNone(no_certificate['sufficient_candidate_disk'])
|
||||
self.assertNotIn('candidate_empty', no_certificate)
|
||||
self.assertTrue(no_certificate['distance_only'])
|
||||
with self.assertRaises(ValueError):
|
||||
first_bearing_candidate_disk(Measurement(0, 0, 0, 46))
|
||||
|
||||
def test_cli_explicit_rounding_margin(self):
|
||||
base = [sys.executable, '-m', 'geometry', 'examples/q1/triangle.json']
|
||||
outputs = [json.loads(subprocess.check_output(base+args, cwd=ROOT, encoding='utf-8'))
|
||||
for args in [[], ['--rounding-margin-deg', '0.005']]]
|
||||
self.assertEqual(outputs[0]['error_policy']['effective_half_angles_deg'], [1, 1, 1])
|
||||
self.assertEqual(outputs[1]['error_policy']['effective_half_angles_deg'], [1.005]*3)
|
||||
self.assertGreater(outputs[1]['diameter_m'], outputs[0]['diameter_m'])
|
||||
bad = subprocess.run(base+['--rounding-margin-deg', '-1'], cwd=ROOT, capture_output=True)
|
||||
self.assertNotEqual(bad.returncode, 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Offline verification of captured practice evidence; never contacts simulator."""
|
||||
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from geometry import Measurement, solve
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class PracticeReplayTests(unittest.TestCase):
|
||||
def test_action_log_and_derived_geometry(self):
|
||||
folder=ROOT/'output/q1/practice'
|
||||
files=list(folder.glob('*_actions.json'))
|
||||
self.assertTrue(files, '缺少本次演练采集证据')
|
||||
for path in files:
|
||||
events=json.loads(path.read_text(encoding='utf-8'))
|
||||
pos=(0,0);channel=1;virtual=0
|
||||
self.assertEqual(events[0]['path'],'/enter')
|
||||
self.assertEqual(events[-1]['path'],'/exit')
|
||||
self.assertEqual(events[-1]['response']['exit_reason'],'user_exit')
|
||||
ids=[]
|
||||
for event in events:
|
||||
self.assertIn(event['path'],('/enter','/measure','/exit'))
|
||||
request,response=event['request'],event['response']
|
||||
self.assertTrue(response['accepted'])
|
||||
ids.append(request['request_id'])
|
||||
if event['path']=='/measure':
|
||||
new=(request['position']['x'],request['position']['y'])
|
||||
virtual+=math.dist(pos,new)/5+5+(request['channel']!=channel)
|
||||
pos,channel=new,request['channel']
|
||||
self.assertAlmostEqual(virtual,response['virtual_time_s'],places=5)
|
||||
self.assertEqual(len(ids),len(set(ids)))
|
||||
observation_path=path.with_name(path.name.replace('_actions','_observations'))
|
||||
data=json.loads(observation_path.read_text(encoding='utf-8'))
|
||||
ms=[Measurement(**m) for m in data['measurements']]
|
||||
recomputed=solve(ms)
|
||||
self.assertAlmostEqual(recomputed['diameter_m'],data['result']['diameter_m'],places=9)
|
||||
self.assertEqual(data['source'],'official_practice')
|
||||
self.assertIsNone(data['ground_truth'])
|
||||
directions=[e for e in events if e['response'].get('measure_result')=='direction']
|
||||
self.assertEqual(directions[0]['request']['position'],directions[1]['request']['position'])
|
||||
self.assertEqual(directions[0]['response']['svd_deg'],directions[1]['response']['svd_deg'])
|
||||
|
||||
|
||||
if __name__=='__main__': unittest.main()
|
||||
@@ -0,0 +1,185 @@
|
||||
<div id="q1-localization">
|
||||
<style>
|
||||
#q1-localization .q1-plot { width:100%; display:block; overflow:hidden; }
|
||||
#q1-localization .q1-legend { display:flex; flex-wrap:wrap; gap:12px; margin-block:12px; }
|
||||
#q1-localization .q1-legend span { display:inline-flex; align-items:center; gap:6px; }
|
||||
#q1-localization .q1-swatch { display:inline-block; width:18px; height:3px; }
|
||||
#q1-localization .q1-stats { display:flex; flex-wrap:wrap; gap:12px 24px; margin-block:12px; }
|
||||
#q1-localization .q1-status { margin-block:8px; }
|
||||
#q1-localization .q1-plot text { fill:var(--foreground); font-size:12px; font-weight:400; }
|
||||
#q1-localization details { margin-block:12px; }
|
||||
#q1-localization summary { cursor:pointer; }
|
||||
</style>
|
||||
<h2>带误差的交会定位</h2>
|
||||
<div class="viz-controls">
|
||||
<label class="form-label">观测案例
|
||||
<select class="form-select" data-q1="case" aria-label="观测案例"></select>
|
||||
</label>
|
||||
<label class="form-label">视图
|
||||
<select class="form-select" data-q1="view" aria-label="视图">
|
||||
<option value="region">定位区域放大</option>
|
||||
<option value="global">检测点全景</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-label">已加入测点 <span data-q1="step-label" class="tabular-nums"></span>
|
||||
<input class="form-range" data-q1="step" type="range" min="1" max="3" value="3" step="1" aria-label="已加入测点数量">
|
||||
</label>
|
||||
</div>
|
||||
<div class="q1-stats tabular-nums" aria-live="polite" data-q1="stats"></div>
|
||||
<svg class="q1-plot" data-q1="plot" role="img" aria-label="检测角域、定位多边形、直径线段和两个包围圆"></svg>
|
||||
<div class="q1-legend text-small">
|
||||
<span><i class="q1-swatch" style="background:var(--viz-series-1)"></i>定位区域</span>
|
||||
<span><i class="q1-swatch" style="background:var(--viz-series-2)"></i>直径线段及直径圆</span>
|
||||
<span><i class="q1-swatch" style="background:var(--viz-series-3)"></i>最小包围圆</span>
|
||||
<span>□ 检测点 + 合成真值 ◎ 覆盖反例顶点</span>
|
||||
</div>
|
||||
<div class="q1-status text-small" data-q1="status" aria-live="polite"></div>
|
||||
<details>
|
||||
<summary>检测点与示向度</summary>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm tabular-nums">
|
||||
<thead><tr><th>测点</th><th class="text-end">x / m</th><th class="text-end">y / m</th><th class="text-end">示向度 / °</th><th class="text-end">误差半角 / °</th></tr></thead>
|
||||
<tbody data-q1="rows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
<script>
|
||||
(() => {
|
||||
const root=document.getElementById('q1-localization');
|
||||
const cases=/*__Q1_DATA__*/;
|
||||
const get=name=>root.querySelector('[data-q1="'+name+'"]');
|
||||
const picker=get('case'), step=get('step'), view=get('view'), svg=get('plot');
|
||||
const NS='http://www.w3.org/2000/svg';
|
||||
const fmt=x=>Number.isFinite(x)?x.toLocaleString('zh-CN',{maximumFractionDigits:4}):'—';
|
||||
const names={polygon:'有界多边形',segment:'线段',point:'单点',empty:'空集',unbounded:'无界区域'};
|
||||
function node(tag,attrs={},parent=svg,text=null) {
|
||||
const el=document.createElementNS(NS,tag);
|
||||
Object.entries(attrs).forEach(([k,v])=>el.setAttribute(k,String(v)));
|
||||
if(text!==null)el.textContent=text;
|
||||
parent.appendChild(el);return el;
|
||||
}
|
||||
cases.forEach((c,i)=>picker.add(new Option(c.name,i)));
|
||||
picker.value=String(Math.max(0,cases.findIndex(c=>c.source==='official_practice')));
|
||||
|
||||
function load() {
|
||||
const c=cases[+picker.value];
|
||||
step.max=String(c.measurements.length);step.value=step.max;draw();
|
||||
}
|
||||
function draw() {
|
||||
const c=cases[+picker.value], n=+step.value, ms=c.measurements.slice(0,n), r=c.frames[n-1];
|
||||
get('step-label').textContent=n+' / '+c.measurements.length;
|
||||
get('stats').replaceChildren();
|
||||
const values=[['区域',names[r.status]],['直径 D',r.diameter_infinite?'∞':fmt(r.diameter_m)+' m'],
|
||||
['最小包围半径',r.minimum_enclosing_circle?fmt(r.minimum_enclosing_circle.radius_m)+' m':'—']];
|
||||
values.forEach(([label,value])=>{const span=document.createElement('span');span.textContent=label+':'+value;get('stats').appendChild(span);});
|
||||
const source=c.source==='official_practice'?'官方演练观测;真实源位置未公开':'本地合成案例';
|
||||
get('status').textContent=source+' · '+(r.status==='unbounded'?'阴影只显示视窗内部分,真实区域无界':
|
||||
r.status==='empty'?'这些观测在给定误差界下不相容':
|
||||
'直径圆'+(r.diameter_circle.covers?'可以覆盖':'不能覆盖,最大超出 '+fmt(r.diameter_circle.max_excess_m)+' m')+
|
||||
' · 清除半径余量 '+fmt(r.clearance_certificate.remaining_margin_m)+' m');
|
||||
get('rows').replaceChildren();
|
||||
ms.forEach((m,i)=>{
|
||||
const tr=document.createElement('tr');
|
||||
[String(i+1),fmt(m.x),fmt(m.y),fmt(m.bearing_deg),fmt(m.half_angle_deg)].forEach((v,j)=>{
|
||||
const td=document.createElement('td');td.textContent=v;if(j)td.className='text-end';tr.appendChild(td);
|
||||
});get('rows').appendChild(tr);
|
||||
});
|
||||
svg.replaceChildren();
|
||||
const width=Math.max(280,root.getBoundingClientRect().width);
|
||||
const height=Math.max(310,Math.min(440,width*.65));
|
||||
svg.setAttribute('viewBox',`0 0 ${width} ${height}`);
|
||||
svg.style.height=height+'px';
|
||||
node('title',{},svg,c.name+':'+names[r.status]);
|
||||
node('desc',{},svg,get('status').textContent);
|
||||
const l=64,t=20,right=20,bottom=46,w=width-l-right,h=height-t-bottom;
|
||||
const finite=['polygon','segment','point'].includes(r.status);
|
||||
let pts=(view.value==='region'&&finite)?r.vertices.map(p=>[...p]):ms.map(m=>[m.x,m.y]);
|
||||
if(!pts.length)pts=[[0,0]];
|
||||
if(view.value==='global'&&finite)pts.push(...r.vertices);
|
||||
if(c.target)pts.push(c.target);
|
||||
if(finite&&view.value==='region') {
|
||||
[r.diameter_circle,r.minimum_enclosing_circle].forEach(circle=>{
|
||||
const [x,y]=circle.center,d=circle.radius_m;
|
||||
pts.push([x-d,y-d],[x+d,y+d]);
|
||||
});
|
||||
}
|
||||
const xs=pts.map(p=>p[0]),ys=pts.map(p=>p[1]);
|
||||
const cx=(Math.min(...xs)+Math.max(...xs))/2, cy=(Math.min(...ys)+Math.max(...ys))/2;
|
||||
let dx=Math.max(...xs)-Math.min(...xs),dy=Math.max(...ys)-Math.min(...ys);
|
||||
const minSpan=finite&&view.value==='region'?Math.max(2,(r.diameter_m||0)*.08):200;
|
||||
const scale=Math.min(w/Math.max(dx*1.3,minSpan),h/Math.max(dy*1.3,minSpan));
|
||||
const xmin=cx-w/(2*scale),xmax=cx+w/(2*scale),ymin=cy-h/(2*scale),ymax=cy+h/(2*scale);
|
||||
const X=x=>l+(x-xmin)*scale,Y=y=>t+h-(y-ymin)*scale;
|
||||
const defs=node('defs');const clip=node('clipPath',{id:'q1-clip'},defs);
|
||||
node('rect',{x:l,y:t,width:w,height:h},clip);
|
||||
for(let i=0;i<4;i++) {
|
||||
const x=xmin+(xmax-xmin)*i/3, y=ymin+(ymax-ymin)*i/3;
|
||||
node('line',{x1:X(x),x2:X(x),y1:t,y2:t+h,stroke:'var(--border)'});
|
||||
node('line',{x1:l,x2:l+w,y1:Y(y),y2:Y(y),stroke:'var(--border)'});
|
||||
const tick=z=>Math.abs(z)>=1e6?z.toExponential(1):Math.abs(xmax-xmin)<20?z.toFixed(1):Math.round(z).toString();
|
||||
node('text',{x:X(x),y:t+h+19,'text-anchor':i===0?'start':i===3?'end':'middle'},svg,tick(x));
|
||||
node('text',{x:l-8,y:Y(y)+4,'text-anchor':'end'},svg,tick(y));
|
||||
}
|
||||
node('text',{x:l+w/2,y:height-7,'text-anchor':'middle',class:'axis-title','data-axis':'x'},svg,'x / m');
|
||||
node('text',{x:14,y:t+h/2,'text-anchor':'middle',transform:`rotate(-90 14 ${t+h/2})`,class:'axis-title','data-axis':'y'},svg,'y / m');
|
||||
const layer=node('g',{'clip-path':'url(#q1-clip)'});
|
||||
let displayPoly;
|
||||
if(finite)displayPoly=r.vertices;
|
||||
else if(r.status==='empty')displayPoly=[];
|
||||
else {
|
||||
// Display clipping ONLY: Python's result remains explicitly unbounded.
|
||||
displayPoly=[[xmin,ymin],[xmax,ymin],[xmax,ymax],[xmin,ymax]];
|
||||
ms.forEach(m=>{
|
||||
const theta=m.bearing_deg*Math.PI/180,d=m.half_angle_deg*Math.PI/180;
|
||||
const normals=[[Math.sin(theta-d),-Math.cos(theta-d)],[-Math.sin(theta+d),Math.cos(theta+d)],[-Math.cos(theta),-Math.sin(theta)]];
|
||||
normals.forEach(([a,b])=>{
|
||||
const bound=a*m.x+b*m.y,out=[];
|
||||
for(let i=0;i<displayPoly.length;i++) {
|
||||
const p=displayPoly[i],q=displayPoly[(i+1)%displayPoly.length];
|
||||
const f=a*p[0]+b*p[1]-bound,g=a*q[0]+b*q[1]-bound;
|
||||
if(f<=1e-8)out.push(p);
|
||||
if((f<0&&g>0)||(f>0&&g<0))out.push([p[0]+(q[0]-p[0])*f/(f-g),p[1]+(q[1]-p[1])*f/(f-g)]);
|
||||
}displayPoly=out;
|
||||
});
|
||||
});
|
||||
}
|
||||
if(displayPoly.length>=3)node('polygon',{points:displayPoly.map(p=>X(p[0])+','+Y(p[1])).join(' '),fill:'var(--viz-series-1)','fill-opacity':.18,stroke:'var(--viz-series-1)','stroke-width':1.5},layer);
|
||||
const reach=Math.hypot(xmax-xmin,ymax-ymin)+Math.max(...ms.map(m=>Math.hypot(m.x-cx,m.y-cy)))*2;
|
||||
ms.forEach((m,i)=>{
|
||||
[-m.half_angle_deg,0,m.half_angle_deg].forEach(offset=>{
|
||||
const a=(m.bearing_deg+offset)*Math.PI/180;
|
||||
node('line',{x1:X(m.x),y1:Y(m.y),x2:X(m.x+reach*Math.cos(a)),y2:Y(m.y+reach*Math.sin(a)),
|
||||
stroke:'var(--foreground)','stroke-opacity':offset===0?.35:.22,'stroke-width':1,'stroke-dasharray':offset===0?'none':'4 4'},layer);
|
||||
});
|
||||
if(m.x>=xmin&&m.x<=xmax&&m.y>=ymin&&m.y<=ymax) {
|
||||
node('rect',{x:X(m.x)-4,y:Y(m.y)-4,width:8,height:8,fill:'var(--foreground)'},layer);
|
||||
node('text',{x:Math.min(l+w-22,X(m.x)+9),y:Math.max(t+14,Y(m.y)-9)},layer,'S'+(i+1));
|
||||
}
|
||||
});
|
||||
if(finite) {
|
||||
[r.diameter_circle,r.minimum_enclosing_circle].forEach((circle,i)=>{
|
||||
node('circle',{cx:X(circle.center[0]),cy:Y(circle.center[1]),r:circle.radius_m*scale,
|
||||
fill:'none',stroke:`var(--viz-series-${i+2})`,'stroke-width':2,'stroke-dasharray':i===0?'6 4':'none'},layer);
|
||||
});
|
||||
const [a,b]=r.diameter_pair;
|
||||
node('line',{x1:X(a[0]),y1:Y(a[1]),x2:X(b[0]),y2:Y(b[1]),stroke:'var(--viz-series-2)','stroke-width':2.5},layer);
|
||||
r.vertices.forEach((p,i)=>{
|
||||
const mark=node('circle',{cx:X(p[0]),cy:Y(p[1]),r:4,fill:'var(--viz-series-1)'},layer);
|
||||
node('title',{},mark,`V${i+1} (${fmt(p[0])}, ${fmt(p[1])})`);
|
||||
});
|
||||
const witness=r.diameter_circle.witness_vertex;
|
||||
if(witness)node('circle',{cx:X(witness[0]),cy:Y(witness[1]),r:8,
|
||||
fill:'none',stroke:'var(--viz-series-2)','stroke-width':2},layer);
|
||||
}
|
||||
if(c.target&&c.target[0]>=xmin&&c.target[0]<=xmax&&c.target[1]>=ymin&&c.target[1]<=ymax) {
|
||||
const x=X(c.target[0]),y=Y(c.target[1]);
|
||||
node('path',{d:`M${x-5},${y}h10 M${x},${y-5}v10`,stroke:'var(--foreground)','stroke-width':2},layer);
|
||||
}
|
||||
node('rect',{x:l,y:t,width:w,height:h,fill:'none',stroke:'var(--border)','data-chart-frame':''});
|
||||
}
|
||||
picker.addEventListener('change',load);step.addEventListener('input',draw);view.addEventListener('change',draw);
|
||||
new ResizeObserver(draw).observe(root);
|
||||
load();
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
@@ -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;}}
|
||||