From 8515f7821a50ecf0df9792610ec9113b249022cd Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Sun, 14 Jun 2026 14:49:53 +0800 Subject: [PATCH] feat(v0.5.0): weather into planner + unified physics model Planner no longer writes local physics formulas. All kinematics/geometry/damage go through shared utility classes (Kinematics/RouteGeometry/CloudExpansionModel/DamageAssessment). Core: RouteGeometry (route geometry), PlannerConfig + planner_config.json (config externalization). Planner route-aware layout (offset along tangent), cloud overlap 20pct. DroneEntity arc-length driven. Fixes: PathInSphere cloud-frame correction (root cause), GaussianPuffDispersion hardcoded Sunny, ComputeEffectiveRadius cloud age, planner wind offset, remove drone wind bias. Tests 167 to 191, 41s. Windy scenarios pass. --- CHANGELOG.md | 51 +++++ VERSION | 2 +- data/planner_config.json | 17 ++ docs/design/architecture/总体架构设计.md | 50 +++++ .../tasks/实施计划与任务跟踪.md | 31 ++- .../2026-06-14-天气纳入规划与物理模型统一.md | 35 ++++ .../Algorithms/AlgorithmTypes.cs | 12 +- .../Algorithms/CloudExpansionModel.cs | 9 +- .../Algorithms/DefaultDefensePlanner.cs | 142 +++++++------ .../Algorithms/GaussianPuffDispersion.cs | 4 +- .../Algorithms/Kinematics.cs | 12 ++ .../Algorithms/PlannerConfig.cs | 71 +++++++ .../Algorithms/RouteGeometry.cs | 131 ++++++++++++ .../Simulation/DroneEntity.cs | 44 ++--- .../Simulation/SimulationEngine.cs | 32 +-- .../CounterDrone.Core/CounterDrone.Core.dll | Bin 123904 -> 135168 bytes .../Scripts/Managers/ManagerVerification.cs | 2 +- .../Scripts/Managers/SimulationBootstrap.cs | 2 +- .../Scripts/Managers/SimulationRunner.cs | 2 +- .../DefensePlannerTests.cs | 129 +++++++++++- .../DispersionModelTests.cs | 48 +++++ .../DroneEntityTests.cs | 53 ++++- .../CounterDrone.Core.Tests/EdgeCaseTests.cs | 6 +- .../FullPipelineTests.cs | 132 ++++++++++++- .../PlannerConfigTests.cs | 95 +++++++++ .../RouteGeometryTests.cs | 187 ++++++++++++++++++ .../SimulationEngineTests.cs | 2 +- .../TestPlannerConfig.cs | 21 ++ 28 files changed, 1161 insertions(+), 161 deletions(-) create mode 100644 data/planner_config.json create mode 100644 docs/requirements/changes/2026-06-14-天气纳入规划与物理模型统一.md create mode 100644 src/CounterDrone.Core/Algorithms/PlannerConfig.cs create mode 100644 src/CounterDrone.Core/Algorithms/RouteGeometry.cs create mode 100644 test/unit/CounterDrone.Core.Tests/PlannerConfigTests.cs create mode 100644 test/unit/CounterDrone.Core.Tests/RouteGeometryTests.cs create mode 100644 test/unit/CounterDrone.Core.Tests/TestPlannerConfig.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cc8d66..eb32c82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,57 @@ --- +## [0.5.0] - 2026-06-14 + +### Added — 物理模型统一架构 + +- **RouteGeometry 静态工具类**:航路几何唯一实现(总弧长/弧长→位置/点→最近弧长/切向量/到达时间)。planner(预测)和 DroneEntity(执行)共用,消除本地折线插值 +- **PlannerConfig + planner_config.json**:planner 策略参数全部外置(重叠系数、威胁类型系数、弹药匹配表、临界/上限概率阈值)。代码零默认值,文件缺失即抛异常 +- 单元测试:RouteGeometry 16 项、PlannerConfig 6 项、DroneEntity L 形多 waypoint 1 项、Z 向航路感知 1 项 + +### Changed — planner 不再写本地物理公式 + +- **DefaultDefensePlanner 航路感知布局**:云团 offset 沿航路切向(`RouteGeometry.TangentAt`),不再写死 X 轴;穿越点用 `RouteGeometry.PositionAt`,到达时间用 `RouteGeometry.TravelTimeTo`——支持任意方向/折线航路 +- **云团重叠**:间距从 `2R`(相切)改为 `2R×(1−重叠系数)`,重叠 20% 由配置驱动,消除相切处的密度空洞 +- **DroneEntity 弧长驱动**:运动改为 `_traveledArc += speed×dt` + `RouteGeometry.PositionAt`,删除 `CurrentWaypointIndex` 逐段插值、`dist<1.0` 阈值、`step>=dist` snap 丢位移等本地逻辑 +- **CloudExpansionModel.RoundsNeeded** 签名:`effectiveRadius` 参数改为 `spacing`,间距由调用方(planner)按重叠系数传入,公式仍在共享模块 +- **策略参数从配置读**:威胁类型系数、弹药匹配表、临界概率(0.5)、拦截概率上限(0.95)全部从 `PlannerConfig` 读,planner 内零硬编码 + +### Fixed — planner 与引擎物理一致性 + +- **PathInSphere 云团参考系修正**:毁伤判定改在云团参考系计算(`drone.Pos − cloud.Center`),修正云团在 tick 内移动导致的每 tick ~2m 系统误差 +- **ComputeEffectiveRadius 云龄 bug**:从 `ArrivalTime×2`(无人机飞行时间,概念错误)改为 `expansionTime`(云团自身膨胀时长) + +### Removed + +- SimulationEngine 所有硬编码桌面路径的诊断写入(`planner_targets.csv`/`cloud_actual.csv`/`path_in_cloud.txt`/`_hitLog`/`_totalPathInCloud`)——这些造成集成测试并行时文件竞争(flaky 失败根因) +- DefaultDefensePlanner 的硬编码 `MatchTable`/`TypeCoefficient` 字典(移入配置) + +### Metrics + +- 测试 167 → **191**(+24),全量通过 41s +- 关键验证:活塞+西风、空基+东风有风场景击毁成功;Z 向航路云团沿航路分布;L 形折线多 waypoint 运动正确 + +--- + +## [0.4.1] - 2026-06-14 + +### Added + +- 天气纳入 Planner 规划:抛撒点风偏预补偿。云团生成后会在 `expansionTime` 内被风吹偏 `windVec × expansionTime`,Planner 逆风预置抛撒点 `cloudGen = 穿越点 − windVec × expansionTime`,使云团漂移后中心正好回到无人机航路上 +- 单元测试:`DispersionModelTests` Phase3 天气差异 2 项(雾 vs 晴、夜 vs 晴);`DefensePlannerTests` 风偏补偿 4 项(无风/东风/西风/北风方向性) + +### Fixed + +- `GaussianPuffDispersion.Tick` Phase3 写死 `WeatherType.Sunny` 的 bug:原代码 `GetStabilityClass((WeatherType)0, windSpeed)` 导致任何天气下扩散行为都相同,预估(`CloudExpansionModel` 已正确读 env)与运行时模型不一致。改为存储 `env` 字段,使用真实 `env.WeatherType` + +### Changed + +- `DefaultDefensePlanner.GenerateFireEventsAt` 区分无人机穿越点 `tx,tz`(用于 `txArrival` 计算)与云团生成点 `cloudGenX,Z`(弹药瞄准目标 + FireEvent 输出) +- 移除 `Solve` 中 `e.TargetX = mid.X + offset` 的覆盖(会抹掉风偏补偿;多发散布偏移已由 `targetOffset` 参数处理) + +--- + ## [0.4.0] - 2026-06-13 ### Added diff --git a/VERSION b/VERSION index 1d0ba9e..8f0916f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.0 +0.5.0 diff --git a/data/planner_config.json b/data/planner_config.json new file mode 100644 index 0000000..c51d281 --- /dev/null +++ b/data/planner_config.json @@ -0,0 +1,17 @@ +{ + "CloudOverlapRatio": 0.2, + "CriticalProbabilityThreshold": 0.5, + "MaxInterceptProbability": 0.95, + "TypeCoefficient": { + "HighSpeed": 4.0, + "FixedWing": 2.0, + "Piston": 2.0, + "Rotor": 1.0, + "Electric": 1.0 + }, + "AmmoMatch": { + "Electric": "InertGas", + "Piston": "InertGas", + "Jet": "ActiveMaterial" + } +} diff --git a/docs/design/architecture/总体架构设计.md b/docs/design/architecture/总体架构设计.md index 36acee6..6876635 100644 --- a/docs/design/architecture/总体架构设计.md +++ b/docs/design/architecture/总体架构设计.md @@ -1004,6 +1004,56 @@ static readonly Dictionary MatchTable = new() - 最危险 = 无人机刚好擦边 ``` +##### 风偏补偿(天气纳入规划) + +云团生成后会被风吹偏。为使云团中心在无人机到达穿越点时正好漂移回航路,抛撒点须**逆风预置**: + +``` +穿越点 (tx, tz) = 无人机航路上的目标点(不变,用于 txArrival 计算) +抛撒点 (cloudGenX, cloudGenZ) = (tx, tz) − windVec × expansionTime + windVec = Kinematics.WindToVector(WindDirection, WindSpeed) // 只影响 X/Z + expansionTime ≈ 30s(Phase 2 湍流膨胀时长) +``` + +- **弹药瞄准抛撒点**:地基 `dist = |cloudGen − platform|`;空基 `distToCloud = |cloudGen − platform|` +- **FireEvent.TargetX/Z = cloudGen**:引擎据此生成云团,云团被风吹 `windVec × expansionTime` 后回到穿越点 +- **天气(稳定度)间接影响**:`CloudExpansionModel.RadiusAt` 已根据 `env.WeatherType` 推导 Pasquill 稳定度,影响 `expansionTime` 与有效半径估算;修复 `GaussianPuffDispersion` 写死 Sunny 的 bug 后,预估与运行时一致 + +> `tx, tz`(穿越点)与 `cloudGenX, cloudGenZ`(抛撒点)在有风时不同,无风时相同。原代码混淆二者,导致有风场景规划错误。 + +##### 物理模型统一原则(planner 与引擎共用) + +**planner 禁止写任何本地运动学/几何/毁伤公式**,全部调用与引擎共享的独立工具类。每个物理量只有一个实现: + +``` +共享工具类(唯一实例) planner 用途 引擎用途 +───────────────────────────────────────────────────────────────── +Kinematics(质点运动学) 弹道/风矢量/到达时间 MunitionEntity/DroneEntity +RouteGeometry(航路几何) 穿越点弧长/位置/切向 DroneEntity 位置更新 +CloudExpansionModel(云团膨胀) 有效半径/膨胀时间/弹药数 CloudEntity 半径演化 +DamageAssessment(毁伤几何) (暂未直接用,估算用模型) PathInSphere 穿云路径 +``` + +- **RouteGeometry**:纯静态工具类。`TotalLength`/`PositionAt(arcLen)`/`ArcLengthNearestTo(x,z)`/`TangentAt(arcLen)`/`TravelTimeTo(arcLen,speed)`。planner 用它定位穿越点、沿航路分布多发、算到达时间;DroneEntity 用它做弧长驱动的位置更新(`_traveledArc += speed×dt` → `PositionAt`)。 +- **云团重叠**:间距 `= 2R × (1 − CloudOverlapRatio)`,重叠比例由 `planner_config.json` 驱动(默认 20%)。`CloudExpansionModel.RoundsNeeded` 接受 `spacing` 参数,公式仍在共享模块,planner 只传配置值。 + +##### 配置外置(planner_config.json) + +planner 所有策略参数从 `data/planner_config.json` 读取,代码零默认值: + +```json +{ + "CloudOverlapRatio": 0.2, + "CriticalProbabilityThreshold": 0.5, + "MaxInterceptProbability": 0.95, + "TypeCoefficient": { "HighSpeed": 4.0, "FixedWing": 2.0, ... }, + "AmmoMatch": { "Electric": "InertGas", "Piston": "InertGas", "Jet": "ActiveMaterial" } +} +``` + +- `PlannerConfig.Load(IPathProvider)` 从 dataRoot 加载;文件缺失或字段非法即抛异常 +- `DefaultDefensePlanner` 构造函数必传 `PlannerConfig` + #### 6.5.3 Step C:反推平台部署 ``` diff --git a/docs/implementation/tasks/实施计划与任务跟踪.md b/docs/implementation/tasks/实施计划与任务跟踪.md index fdf9192..1fac28d 100644 --- a/docs/implementation/tasks/实施计划与任务跟踪.md +++ b/docs/implementation/tasks/实施计划与任务跟踪.md @@ -1,8 +1,8 @@ # 实施计划与任务跟踪 > **项目**:反无人机仿真系统后端 -> **文档版本**:V1.2 -> **更新日期**:2026-06-12 +> **文档版本**:V1.3 +> **更新日期**:2026-06-14 --- @@ -16,7 +16,7 @@ Phase 4 ✅ 仿真引擎 Phase 5 ✅ 报告生成 Phase 6 ✅ Unity 集成(桥接层 + 示例项目) Phase 7 ✅ 打磨收尾 -Phase 8 ⬜ 待开发 +Phase 8 🔄 待开发(天气/物理模型统一已完成) ──────────────────────── 已完成 P1-P7 ``` @@ -139,11 +139,13 @@ Phase 8 ⬜ 待开发 | 指标 | 值 | |------|------| -| 测试总数 | **141** | -| 行覆盖率 | **95.4%** | -| 分支覆盖率 | **80.5%** | -| 执行时间 | ~26 秒 | +| 测试总数 | **191** | +| 行覆盖率 | **95.4%**(待重测) | +| 分支覆盖率 | **80.5%**(待重测) | +| 执行时间 | ~41 秒 | | Core 程序集 | `CounterDrone.Core.dll` (.NET Standard 2.1) | +| 共享物理工具类 | `Kinematics` / `RouteGeometry` / `CloudExpansionModel` / `DamageAssessment` | +| 全局配置 | `data/planner_config.json`(planner 策略参数,代码零默认值) | | Unity 项目 | `src/Unity/`(Unity 2022.3.62) | | Unity Manager | 8 个 MonoBehaviour 桥接 + Bootstrap + SqliteConnectionTracker | | 零 Unity 依赖 | ✅ Core 可脱离 Unity 独立运行和测试 | @@ -154,11 +156,24 @@ Phase 8 ⬜ 待开发 > UI/视觉/动画属于前端同事范畴,以下仅列后端 Core 需实现的功能。 +### 8.0 天气与物理模型统一(✅ 已完成) + +| # | 功能 | 说明 | +|---|------|------| +| 8.0.1 | 天气纳入扩散模型 | ✅ 修复 `GaussianPuffDispersion` 写死 Sunny 的 bug,Phase3 用真实 `env.WeatherType` 推导 Pasquill 稳定度 | +| 8.0.2 | Planner 风偏补偿 | ✅ 抛撒点逆风预置(`cloudGen = 穿越点 − windVec×expansionTime`),云团漂移后回到航路 | +| 8.0.3 | 去除无人机风偏叠加 | ✅ DroneEntity 不再叠加风位移(真实无人机有飞控修正),planner 与引擎速度模型一致 | +| 8.0.4 | PathInSphere 云团参考系修正 | ✅ 毁伤判定改在云团参考系(`drone.Pos − cloud.Center`),修正移动球导致的每 tick ~2m 系统误差 | +| 8.0.5 | ComputeEffectiveRadius 云龄 bug | ✅ 从 `ArrivalTime×2`(无人机飞行时间,概念错误)改为 `expansionTime`(云团自身膨胀时长) | +| 8.0.6 | 物理模型统一架构 | ✅ 新增 `RouteGeometry` 静态工具类,planner 与引擎共用航路几何;planner 删除所有本地物理公式 | +| 8.0.7 | 配置外置 | ✅ 新增 `PlannerConfig` + `planner_config.json`,策略参数(重叠系数、类型系数、弹药匹配、概率阈值)全部从配置读,代码零默认值 | +| 8.0.8 | 云团重叠布局 | ✅ 间距 `2R×(1−重叠比例)`,默认重叠 20%,消除相切处密度空洞;offset 沿航路切向,支持任意方向/折线航路 | + ### 8.1 仿真增强 | # | 功能 | 说明 | |---|------|------| -| 8.1.1 | 探测设备搜索逻辑 | `DetectionEntity` 类已存在,需接入 `SimulationEngine` 实现探测→火控链路闭环 | +| 8.1.1 | 探测设备搜索逻辑 | `DetectionEntity` 类已存在,需接入 `SimulationEngine` 实现探测→火控链路闭环。天气(能见度/日夜)对光电/红外探测距离的衰减在此实现 | | 8.1.2 | 蜂群运动模型 | `FormationMode.Swarm` 枚举已定义,需差异化行为(随机扰动、个体差异) | | 8.1.3 | 空基平台 + DefensePlanner | ✅ 五步规划引擎,通道模型,物理间隔错发,路径积分毁伤判定 | ✅ | | 8.1.4 | 预置典型目标库 | 具体无人机型号 JSON 配置(如 DJI Mavic 3、Shahed-136 等),导入 `TargetConfig` 默认值 | diff --git a/docs/requirements/changes/2026-06-14-天气纳入规划与物理模型统一.md b/docs/requirements/changes/2026-06-14-天气纳入规划与物理模型统一.md new file mode 100644 index 0000000..6c4bc36 --- /dev/null +++ b/docs/requirements/changes/2026-06-14-天气纳入规划与物理模型统一.md @@ -0,0 +1,35 @@ +# 天气纳入 Planner 规划与物理模型统一 + +- **日期**:2026-06-14 +- **提出人**:tian +- **关联需求**:技术要求终版 2.2.3(云团扩散效果受气象条件影响) +- **优先级**:高 + +## 变更描述 + +将天气/风要素纳入 DefensePlanner 规划,并在此过程中发现并修复 planner 与仿真引擎物理模型分裂的根本问题。 + +### 起因 +原 planner 未考虑天气对云团漂移的影响,有风场景下规划方案与仿真结果不一致。深入排查后发现根因不止于风偏补偿,而是 planner 大量本地重写了运动学/几何公式(直线距离、写死 X 轴 offset、`2R` 相切假设等),与引擎的实际行为(折线航路、云团移动、PathInSphere 几何)系统性偏离。 + +### 变更内容 +1. **天气纳入扩散模型**:修复 `GaussianPuffDispersion` 写死 `WeatherType.Sunny` 的 bug,Phase3 高斯扩散改用真实环境天气推导 Pasquill 稳定度 +2. **Planner 风偏补偿**:抛撒点逆风预置,云团生成后漂移 expansionTime 秒回到无人机穿越点 +3. **PathInSphere 云团参考系修正**:毁伤判定改在云团参考系计算,修正云团在 tick 内移动导致的每 tick ~2m 系统误差(关键根因) +4. **物理模型统一架构**:新增 `RouteGeometry` 静态工具类,planner 与引擎共用航路几何;planner 删除所有本地物理公式 +5. **配置外置**:新增 `PlannerConfig` + `planner_config.json`,策略参数(重叠系数、类型系数、弹药匹配、概率阈值)全部从配置读,代码零默认值 +6. **云团重叠布局**:间距 `2R×(1−重叠比例)`,默认重叠 20%,offset 沿航路切向,支持任意方向/折线航路 + +## 影响范围 + +- [x] 接口变更:`DefaultDefensePlanner` 构造函数必传 `PlannerConfig`;`CloudExpansionModel.RoundsNeeded` 签名变更(`effectiveRadius` → `spacing`) +- [ ] 数据库变更 +- [ ] UI 变更 +- [x] 文档变更:架构设计 6.5 节、CHANGELOG、VERSION、实施计划 + +## 验收 + +- 全量测试 167 → **191**(+24),41s 通过 +- 活塞+西风 5m/s、空基+东风 5m/s 有风场景击毁成功(非边界) +- Z 向航路云团沿航路分布(不再写死 X 轴) +- L 形折线多 waypoint 运动正确 diff --git a/src/CounterDrone.Core/Algorithms/AlgorithmTypes.cs b/src/CounterDrone.Core/Algorithms/AlgorithmTypes.cs index 9839c38..5433b6c 100644 --- a/src/CounterDrone.Core/Algorithms/AlgorithmTypes.cs +++ b/src/CounterDrone.Core/Algorithms/AlgorithmTypes.cs @@ -108,14 +108,16 @@ namespace CounterDrone.Core.Algorithms /// 综合优先级 = 威胁指数 / (到达时间 + 1) public float Priority => ArrivalTime > -1 ? ThreatIndex / (ArrivalTime + 1f) : ThreatIndex; - /// 预计到达航路中点的时间(秒) + /// 预计到达航路中点的时间(秒)。 + /// 物理含义:匀速直线运动从航路起点到中点的飞行时间。 public float GetArrivalTime() { if (Waypoints.Count < 2) return 0; - var start = new Vector3((float)Waypoints[0].PosX, (float)Waypoints[0].PosY, (float)Waypoints[0].PosZ); - var end = new Vector3((float)Waypoints[^1].PosX, (float)Waypoints[^1].PosY, (float)Waypoints[^1].PosZ); - var speed = (float)Target.TypicalSpeed / 3.6f; - return start.DistanceTo(end) / speed / 2f; + var s = Waypoints[0]; + var e = Waypoints[^1]; + float midX = ((float)s.PosX + (float)e.PosX) / 2f; + float midZ = ((float)s.PosZ + (float)e.PosZ) / 2f; + return Kinematics.TravelTime((float)s.PosX, (float)s.PosZ, midX, midZ, (float)Target.TypicalSpeed); } } diff --git a/src/CounterDrone.Core/Algorithms/CloudExpansionModel.cs b/src/CounterDrone.Core/Algorithms/CloudExpansionModel.cs index 1e6a9b8..713b207 100644 --- a/src/CounterDrone.Core/Algorithms/CloudExpansionModel.cs +++ b/src/CounterDrone.Core/Algorithms/CloudExpansionModel.cs @@ -69,12 +69,11 @@ namespace CounterDrone.Core.Algorithms /// 覆盖指定距离需要的云团数量 /// 需要覆盖的距离 (m) - /// 使用哪个半径值(null=默认TurbulentRadius) - public int RoundsNeeded(float requiredCoverage, float? effectiveRadius = null) + /// 相邻云团中心间距 (m),默认=2R(相切)。重叠时由调用方传入更小值。 + public int RoundsNeeded(float requiredCoverage, float? spacing = null) { - float R = effectiveRadius ?? TurbulentRadius; - float spacing = 2f * R; - return Math.Max(1, (int)Math.Ceiling(requiredCoverage / spacing)); + float s = spacing ?? 2f * TurbulentRadius; + return Math.Max(1, (int)Math.Ceiling(requiredCoverage / s)); } /// 达到指定半径所需时间 (s) diff --git a/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs b/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs index 00ff7f5..22c8ac2 100644 --- a/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs +++ b/src/CounterDrone.Core/Algorithms/DefaultDefensePlanner.cs @@ -5,36 +5,23 @@ using CounterDrone.Core.Models; namespace CounterDrone.Core.Algorithms { - /// 默认防御规划器 — 五步流水线,全部使用真实物理计算 + /// 默认防御规划器 — 五步流水线,全部调用与引擎共享的物理工具类 public class DefaultDefensePlanner : IDefensePlanner { private readonly List _ammoCatalog; private readonly IDamageModel _damageModel; + private readonly PlannerConfig _config; - public DefaultDefensePlanner(List ammoCatalog, IDamageModel damageModel = null) + public DefaultDefensePlanner(List ammoCatalog, PlannerConfig config, + IDamageModel damageModel = null) { _ammoCatalog = ammoCatalog ?? throw new ArgumentNullException(nameof(ammoCatalog)); + _config = config ?? throw new ArgumentNullException(nameof(config)); _damageModel = damageModel ?? new DamageModelRouter(); if (_ammoCatalog.Count == 0) throw new ArgumentException("弹药规格目录不能为空"); } - private static readonly Dictionary MatchTable = new() - { - { PowerType.Electric, AerosolType.InertGas }, - { PowerType.Piston, AerosolType.InertGas }, - { PowerType.Jet, AerosolType.ActiveMaterial }, - }; - - private static readonly Dictionary TypeCoefficient = new() - { - { TargetType.HighSpeed, 4f }, - { TargetType.FixedWing, 2f }, - { TargetType.Piston, 2f }, - { TargetType.Rotor, 1f }, - { TargetType.Electric, 1f }, - }; - // ═══════════════════════════════════════════════ // 五步流水线 // ═══════════════════════════════════════════════ @@ -59,14 +46,14 @@ namespace CounterDrone.Core.Algorithms foreach (var t in threats) { t.ArrivalTime = t.GetArrivalTime(); - t.ThreatIndex = CalcThreatIndex(t.Target); + t.ThreatIndex = CalcThreatIndex(_config, t.Target); } var sorted = threats.OrderByDescending(t => t.Priority).ToList(); // Step 2-4: 贪心分配求解 // 预先检查:所有需要的弹药类型都在目录中 var neededTypes = sorted - .Select(t => MatchAmmo((PowerType)t.Target.PowerType)) + .Select(t => MatchAmmo(_config, (PowerType)t.Target.PowerType)) .Distinct() .ToList(); foreach (var t in neededTypes) @@ -87,9 +74,9 @@ namespace CounterDrone.Core.Algorithms // Step 1: 威胁指数 // ═══════════════════════════════════════════════ - private static float CalcThreatIndex(TargetConfig target) + private static float CalcThreatIndex(PlannerConfig config, TargetConfig target) { - float typeCoef = TypeCoefficient.GetValueOrDefault((TargetType)target.TargetType, 1f); + float typeCoef = config.TypeCoefficient.GetValueOrDefault((TargetType)target.TargetType, 1f); float speedCoef = (float)target.TypicalSpeed / 60f; return typeCoef * speedCoef; } @@ -98,9 +85,9 @@ namespace CounterDrone.Core.Algorithms // Step 2: 弹药匹配 // ═══════════════════════════════════════════════ - private static AerosolType MatchAmmo(PowerType power) + private static AerosolType MatchAmmo(PlannerConfig config, PowerType power) { - return MatchTable.GetValueOrDefault(power, AerosolType.InertGas); + return config.AmmoMatch.GetValueOrDefault(power, AerosolType.InertGas); } // ═══════════════════════════════════════════════ @@ -129,11 +116,11 @@ namespace CounterDrone.Core.Algorithms } // 计算总弹药需求 - var neededAmmo = MatchAmmo((PowerType)threat.Target.PowerType); + var neededAmmo = MatchAmmo(_config, (PowerType)threat.Target.PowerType); var ammo = _ammoCatalog.First(a => a.AerosolType == (int)neededAmmo); // 单机需求 - var (effectiveRadius, expansionTime, turbulentRadius) = ComputeEffectiveRadius(threat, ammo, env); + var (effectiveRadius, expansionTime, turbulentRadius) = ComputeEffectiveRadius(ammo, env); int singleNeeded = CalcRoundsNeeded(threat, ammo, env, false, turbulentRadius); // 横向编队:每种 Width = Quantity 个独立车道 @@ -147,7 +134,8 @@ namespace CounterDrone.Core.Algorithms int totalRoundsNeeded = singleNeeded * yLanes; // 逐单元分配:每个单元锁定一个 Y 车道 - float spacing = 2f * turbulentRadius; + // 云团间距 = 2R × (1 - 重叠比例),重叠由配置驱动,保证有效区互相覆盖 + float spacing = 2f * turbulentRadius * (1f - _config.CloudOverlapRatio); int[] laneNeeded = new int[yLanes]; float[] laneBaseTime = new float[yLanes]; bool[] laneBaseSet = new bool[yLanes]; @@ -171,7 +159,6 @@ namespace CounterDrone.Core.Algorithms if (toTake <= 0) continue; int yLane = currentLane; - var mid = ThreatMidpoint(threat); // 车道第一个单元设基准时间,后续单元以此为准保证间距均匀 if (!laneBaseSet[yLane]) { @@ -191,7 +178,7 @@ namespace CounterDrone.Core.Algorithms foreach (var e in fe) { e.FireTime = laneBaseTime[yLane] + (baseRoundInLane + ch) * stagger; - e.TargetX = mid.X + offset; + // TargetX/Z 保留 GenerateFireEventsAt 算出的风偏补偿后抛撒点 cloudGenX/Z e.PlatformIndex = unitIdx * c.Unit.TotalChannels + ch; } fevents.AddRange(fe); @@ -230,7 +217,7 @@ namespace CounterDrone.Core.Algorithms foreach (var threat in sortedThreats) { var a2 = _ammoCatalog.FirstOrDefault(s => - s.AerosolType == (int)MatchAmmo((PowerType)threat.Target.PowerType)); + s.AerosolType == (int)MatchAmmo(_config, (PowerType)threat.Target.PowerType)); int totalRounds = plan.Assignments .Where(a => a.DroneGroupId == threat.GroupId) .Sum(a => a.RoundsFired); @@ -253,10 +240,10 @@ namespace CounterDrone.Core.Algorithms List availableUnits, CombatScene env) { var candidates = new List(); - var neededAmmo = MatchAmmo((PowerType)threat.Target.PowerType); + var neededAmmo = MatchAmmo(_config, (PowerType)threat.Target.PowerType); var ammo = _ammoCatalog.First(a => a.AerosolType == (int)neededAmmo); - var (ammoEff, _, _) = ComputeEffectiveRadius(threat, ammo, env); + var (ammoEff, _, _) = ComputeEffectiveRadius(ammo, env); foreach (var unit in availableUnits) { @@ -296,7 +283,7 @@ namespace CounterDrone.Core.Algorithms float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f; float neededExposure = _damageModel.RequiredExposureSeconds((TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, ammoType); float actualExposure = effectiveR * 2f / avgSpeed; - float prob = Math.Min(0.95f, actualExposure / neededExposure); + float prob = Math.Min(_config.MaxInterceptProbability, actualExposure / neededExposure); return new InterceptCandidate { @@ -330,7 +317,7 @@ namespace CounterDrone.Core.Algorithms float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f; float neededExposure = _damageModel.RequiredExposureSeconds((TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, ammoType); float actualExposure = effectiveR * 2f / avgSpeed; - float prob = Math.Min(0.95f, actualExposure / neededExposure); + float prob = Math.Min(_config.MaxInterceptProbability, actualExposure / neededExposure); return new InterceptCandidate { @@ -349,14 +336,14 @@ namespace CounterDrone.Core.Algorithms // ═══════════════════════════════════════════════ private (float effectiveRadius, float expansionTime, float rPhase2) ComputeEffectiveRadius( - DroneGroup threat, AmmunitionSpec ammo, CombatScene env) + AmmunitionSpec ammo, CombatScene env) { var model = new CloudExpansionModel(ammo, env); - float tPhase2 = 30f; - float rPhase2 = model.RadiusAt(tPhase2); + float rPhase2 = model.RadiusAt(30f); float expansionTime = model.TimeToReach(rPhase2); - float halfTime = threat.ArrivalTime * 2f; // 总飞行时间的一半 - float effectiveR = model.RadiusAt(halfTime); + // 云团被穿过时的真实年龄 = expansionTime(云团生成后膨胀到有效半径的时长)。 + // 这是云团自身的物理量,与无人机飞行时间无关。 + float effectiveR = model.RadiusAt(expansionTime); return (effectiveR, expansionTime, rPhase2); } @@ -368,14 +355,16 @@ namespace CounterDrone.Core.Algorithms float neededExposure = _damageModel.RequiredExposureSeconds( (TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, (AerosolType)ammo.AerosolType); float requiredCoverage = neededExposure * avgSpeed; - return cloudModel.RoundsNeeded(requiredCoverage); + // 间距由配置的重叠比例驱动,公式在 CloudExpansionModel(共享) + float spacing = 2f * turbulentRadius * (1f - _config.CloudOverlapRatio); + return cloudModel.RoundsNeeded(requiredCoverage, spacing); } private float ComputeInterceptProbability(DroneGroup threat, AmmunitionSpec ammo, int rounds, CombatScene env) { float avgSpeed = (float)threat.Target.TypicalSpeed / 3.6f; - var (effectiveR, _, _) = ComputeEffectiveRadius(threat, ammo, env); + var (effectiveR, _, _) = ComputeEffectiveRadius(ammo, env); float spacing = 2f * effectiveR; float actualCoverage = spacing * (rounds - 1) + 2f * effectiveR; float actualExposure = actualCoverage / avgSpeed; @@ -383,7 +372,7 @@ namespace CounterDrone.Core.Algorithms var aerosolType = (AerosolType)ammo.AerosolType; float neededExposure = _damageModel.RequiredExposureSeconds((TargetType)threat.Target.TargetType, (PowerType)threat.Target.PowerType, aerosolType); - return Math.Min(0.95f, actualExposure / neededExposure); + return Math.Min(_config.MaxInterceptProbability, actualExposure / neededExposure); } // ═══════════════════════════════════════════════ @@ -395,12 +384,11 @@ namespace CounterDrone.Core.Algorithms int yLane, int yLanes, float formationWidth) { var events = new List(); + var wps = threat.Waypoints; + if (wps == null || wps.Count < 2) return events; - var mid = ThreatMidpoint(threat); var cloudModel = new CloudExpansionModel(ammo, env); - // 云龄 = expansionTime,expansionTime = TimeToReach(RadiusAt(30s)) ≈ 30s - // 考虑弹间间隔,最后云的龄 = expansionTime - (rounds-1)*stagger - // 用保守值:取 expansionTime 的 90%(实际间距导致龄差 ~3s) + // 云龄 = expansionTime(云团生成后膨胀到有效半径的时长) float effectiveAge = cloudModel.TimeToReach(cloudModel.TurbulentRadius) * 0.9f; float effectiveR = cloudModel.RadiusAt(effectiveAge); float expansionTime = cloudModel.TimeToReach(effectiveR); @@ -410,27 +398,39 @@ namespace CounterDrone.Core.Algorithms if (densityAtPassage < (float)ammo.EffectiveConcentration) return events; // 云团到达时已稀释失效 - // 编队 Y 偏移:按车道分布 - float laneSpacingZ = yLanes > 1 ? formationWidth / (yLanes - 1) : 0f; - float tz = mid.Z + yLane * laneSpacingZ; + float typicalSpeed = (float)threat.Target.TypicalSpeed; + if (typicalSpeed <= 0) return events; - float tx = mid.X + targetOffset; + // 航路感知布局:穿越点弧长 = 航路中点弧长 + 沿航路偏移 + // targetOffset 现在是沿航路切向的弧长偏移(不再是 X 分量),支持任意方向航路 + var mid = ThreatMidpoint(threat); + float midArc = RouteGeometry.ArcLengthNearestTo(wps, mid.X, mid.Z); + float crossArc = midArc + targetOffset; - // 无人机到达目标点时间 - float droneSpeed = (float)threat.Target.TypicalSpeed / 3.6f; - if (droneSpeed <= 0) return events; - float startX = (float)threat.Waypoints[0].PosX; - float startZ = (float)threat.Waypoints[0].PosZ; - float endX = (float)threat.Waypoints[^1].PosX; - float endZ = (float)threat.Waypoints[^1].PosZ; - float totalDist = Kinematics.Distance2D(startX, startZ, endX, endZ); - if (totalDist <= 0) return events; - float distToTx = Kinematics.Distance2D(startX, startZ, tx, tz); - distToTx = Math.Max(0, Math.Min(totalDist, distToTx)); - float txArrival = distToTx / droneSpeed; + // 编队横向偏移:按车道分布在航路法向上 + // 法向 = 切向旋转 90°,用 RouteGeometry.TangentAt 取穿越点处航路方向 + var (tanX, tanZ) = RouteGeometry.TangentAt(wps, crossArc); + float normalX = -tanZ, normalZ = tanX; // 切向逆时针 90° = 左侧法向 + float laneOffset = yLanes > 1 ? (yLane - (yLanes - 1) / 2f) * (formationWidth / Math.Max(1, yLanes - 1)) : 0f; + + // 穿越点 = 航路上 crossArc 处 + 横向车道偏移 + var (routeX, routeY, routeZ) = RouteGeometry.PositionAt(wps, crossArc); + float tx = routeX + normalX * laneOffset; + float ty = routeY; + float tz = routeZ + normalZ * laneOffset; + + // 无人机到达穿越点的时间:沿航路匀速飞行 crossArc 弧长 + float txArrival = RouteGeometry.TravelTimeTo(wps, crossArc >= 0 ? crossArc : 0, typicalSpeed); float recommendedTiming = txArrival - expansionTime; if (recommendedTiming <= 0f) return events; + // 风偏补偿:云团生成后会被风吹偏,补偿时长 = 从生成到被穿过。 + // 每朵云的生成时刻不同(受发射/飞行时间影响),但 planner 此处用 expansionTime + // 作为云团从生成到被穿过的时长(recommendedTiming 已对齐),各发独立用各自的 expansionTime。 + var (wx, _, wz) = Kinematics.WindToVector((WindDirection)env.WindDirection, (float)env.WindSpeed); + float cloudGenX = tx - wx * expansionTime; + float cloudGenZ = tz - wz * expansionTime; + float fireTime; if (unit.Type == PlatformType.AirBased) { @@ -439,12 +439,10 @@ namespace CounterDrone.Core.Algorithms float releaseAlt = unit.ReleaseAltitude; float cruiseSpd = unit.CruiseSpeed; float fallTime = Kinematics.AirDropFallTime(releaseAlt, (float)threat.Target.TypicalAltitude); - // 载具飞向云端方向,漂移距离 = 巡航速度 × 落体时间 float driftDist = cruiseSpd * fallTime; float distToCloud = Kinematics.Distance3D( unit.Position.X, unit.Position.Y, unit.Position.Z, - tx, releaseAlt, tz); - // 投放点间距 = 总距离 - 漂移,投后弹药滑翔至预期云位 + cloudGenX, releaseAlt, cloudGenZ); float flightDist = Math.Max(0f, distToCloud - driftDist); float flightTime = flightDist / cruiseSpd; fireTime = recommendedTiming - flightTime - fallTime; @@ -454,8 +452,8 @@ namespace CounterDrone.Core.Algorithms if (unit.MuzzleVelocity <= 0) throw new InvalidOperationException($"地基单元 {unit.Id}: MuzzleVelocity={unit.MuzzleVelocity} 必须>0"); float mv = unit.MuzzleVelocity; - float dx = tx - unit.Position.X; - float dz = tz - unit.Position.Z; + float dx = cloudGenX - unit.Position.X; + float dz = cloudGenZ - unit.Position.Z; float dist = (float)Math.Sqrt(dx * dx + dz * dz); float heightDiff = (float)threat.Target.TypicalAltitude - unit.Position.Y; float shellTime = Kinematics.ParabolicShellTime(dist, heightDiff, mv); @@ -468,9 +466,9 @@ namespace CounterDrone.Core.Algorithms { FireTime = fireTime, PlatformIndex = 0, - TargetX = tx, - TargetY = mid.Y, - TargetZ = tz, + TargetX = cloudGenX, + TargetY = ty, + TargetZ = cloudGenZ, MuzzleVelocity = unit.Type == PlatformType.AirBased ? 0f : unit.MuzzleVelocity, }); @@ -495,7 +493,7 @@ namespace CounterDrone.Core.Algorithms while (rounds > 1) { // 简化:弹药减半 → 概率减半 - if ((float)rounds / assignment.RoundsFired < 0.5f) break; + if ((float)rounds / assignment.RoundsFired < _config.CriticalProbabilityThreshold) break; rounds--; } @@ -513,7 +511,7 @@ namespace CounterDrone.Core.Algorithms } critical.MergedSchedule.Sort((a, b) => a.FireTime.CompareTo(b.FireTime)); - critical.OverallProbability = 0.5f; + critical.OverallProbability = _config.CriticalProbabilityThreshold; critical.Summary = $"临界方案:刚好满足 50% 拦截概率"; return critical; } diff --git a/src/CounterDrone.Core/Algorithms/GaussianPuffDispersion.cs b/src/CounterDrone.Core/Algorithms/GaussianPuffDispersion.cs index 7f72d9e..4c47e30 100644 --- a/src/CounterDrone.Core/Algorithms/GaussianPuffDispersion.cs +++ b/src/CounterDrone.Core/Algorithms/GaussianPuffDispersion.cs @@ -12,6 +12,7 @@ namespace CounterDrone.Core.Algorithms public class GaussianPuffDispersion : ICloudDispersionModel { private AmmunitionSpec _ammo = null!; + private CombatScene _env = null!; private float _elapsed; private float _currentRadius; private float _currentDensity; @@ -33,6 +34,7 @@ namespace CounterDrone.Core.Algorithms public void Initialize(AmmunitionSpec ammo, CombatScene env, Vector3 releasePos, float releaseTime) { _ammo = ammo; + _env = env; _elapsed = 0f; _inPhase3 = false; @@ -75,7 +77,7 @@ namespace CounterDrone.Core.Algorithms { // Phase 3: 高斯扩散 var x = Math.Max(1f, windSpeed * (_elapsed - 30f)); - var cls = Kinematics.GetStabilityClass((WeatherType)0, windSpeed); + var cls = Kinematics.GetStabilityClass((WeatherType)_env.WeatherType, windSpeed); var sY = Kinematics.SigmaY(cls, x); var sZ = Kinematics.SigmaZ(cls, x); _currentDensity = Kinematics.GaussianPeakConcentration((float)_ammo.SourceStrength, sY, sZ); diff --git a/src/CounterDrone.Core/Algorithms/Kinematics.cs b/src/CounterDrone.Core/Algorithms/Kinematics.cs index 01b0e65..9528f88 100644 --- a/src/CounterDrone.Core/Algorithms/Kinematics.cs +++ b/src/CounterDrone.Core/Algorithms/Kinematics.cs @@ -162,6 +162,18 @@ namespace CounterDrone.Core.Algorithms return (float)Math.Sqrt(dx * dx + dz * dz); } + /// 匀速直线运动从一点到另一点的飞行时间(秒)。 + /// 物理模型:无人机/平台沿直线匀速飞行,时间 = 距离 / 速度。 + /// speedKmh 为 0 时抛异常(速度必须 > 0,由调用方保证)。 + /// 速度 km/h(与 TypicalSpeed 单位一致) + public static float TravelTime(float fromX, float fromZ, float toX, float toZ, float speedKmh) + { + if (speedKmh <= 0) + throw new ArgumentException("速度必须 > 0", nameof(speedKmh)); + float dist = Distance2D(fromX, fromZ, toX, toZ); + return dist / (speedKmh / 3.6f); + } + /// 点到三维点距离 public static float Distance3D(float x1, float y1, float z1, float x2, float y2, float z2) { diff --git a/src/CounterDrone.Core/Algorithms/PlannerConfig.cs b/src/CounterDrone.Core/Algorithms/PlannerConfig.cs new file mode 100644 index 0000000..c20fd09 --- /dev/null +++ b/src/CounterDrone.Core/Algorithms/PlannerConfig.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; +using CounterDrone.Core.Models; + +namespace CounterDrone.Core.Algorithms +{ + /// 防御规划器配置 — 全局策略参数,从 planner_config.json 加载。 + /// 代码零默认值,所有字段必须从配置文件读取;文件缺失或字段缺失即抛异常。 + public class PlannerConfig + { + /// 云团重叠比例(0=相切,0.2=重叠 20%)。间距 = 2R × (1 − 重叠比例)。 + public float CloudOverlapRatio { get; set; } + + /// 临界方案概率阈值(DeriveCritical 用) + public float CriticalProbabilityThreshold { get; set; } + + /// 拦截概率上限(封顶值) + public float MaxInterceptProbability { get; set; } + + /// 威胁类型系数表(TargetType → 系数) + public Dictionary TypeCoefficient { get; set; } = new(); + + /// 弹药匹配表(PowerType → AerosolType) + public Dictionary AmmoMatch { get; set; } = new(); + + private const string ConfigFileName = "planner_config.json"; + + /// 从 dataRoot 加载配置。文件缺失或字段非法即抛异常。 + public static PlannerConfig Load(string dataRoot) + { + if (string.IsNullOrEmpty(dataRoot)) + throw new ArgumentException("dataRoot 不能为空", nameof(dataRoot)); + string path = Path.Combine(dataRoot, ConfigFileName); + if (!File.Exists(path)) + throw new FileNotFoundException($"planner 配置文件不存在: {path}"); + + string json = File.ReadAllText(path); + var config = JsonSerializer.Deserialize(json, JsonOptions) + ?? throw new InvalidDataException($"planner 配置解析失败: {path}"); + config.Validate(); + return config; + } + + /// 从 IPathProvider 加载(便捷重载) + public static PlannerConfig Load(IPathProvider paths) + => Load(paths?.GetDataRoot() ?? throw new ArgumentNullException(nameof(paths))); + + private void Validate() + { + if (CloudOverlapRatio < 0f || CloudOverlapRatio >= 1f) + throw new InvalidDataException($"CloudOverlapRatio 必须在 [0, 1),实际 {CloudOverlapRatio}"); + if (CriticalProbabilityThreshold <= 0f || CriticalProbabilityThreshold >= 1f) + throw new InvalidDataException($"CriticalProbabilityThreshold 必须在 (0, 1),实际 {CriticalProbabilityThreshold}"); + if (MaxInterceptProbability <= 0f || MaxInterceptProbability > 1f) + throw new InvalidDataException($"MaxInterceptProbability 必须在 (0, 1],实际 {MaxInterceptProbability}"); + if (TypeCoefficient == null || TypeCoefficient.Count == 0) + throw new InvalidDataException("TypeCoefficient 不能为空"); + if (AmmoMatch == null || AmmoMatch.Count == 0) + throw new InvalidDataException("AmmoMatch 不能为空"); + } + + private static readonly JsonSerializerOptions JsonOptions = new() + { + Converters = { new JsonStringEnumConverter() }, + PropertyNameCaseInsensitive = true, + }; + } +} diff --git a/src/CounterDrone.Core/Algorithms/RouteGeometry.cs b/src/CounterDrone.Core/Algorithms/RouteGeometry.cs new file mode 100644 index 0000000..5dae24c --- /dev/null +++ b/src/CounterDrone.Core/Algorithms/RouteGeometry.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using CounterDrone.Core.Models; + +namespace CounterDrone.Core.Algorithms +{ + /// 航路几何工具——对 waypoint 序列做纯几何运算。 + /// 无状态、纯函数,与 Kinematics/DamageAssessment 同范式。 + /// planner(预测)和 DroneEntity(执行)共用,确保两边航路模型一致。 + public static class RouteGeometry + { + /// 航路总弧长(米):所有相邻 waypoint 三维距离之和。 + public static float TotalLength(IReadOnlyList wps) + { + if (wps == null || wps.Count < 2) return 0f; + float total = 0f; + for (int i = 0; i < wps.Count - 1; i++) + total += Kinematics.Distance3D( + (float)wps[i].PosX, (float)wps[i].PosY, (float)wps[i].PosZ, + (float)wps[i + 1].PosX, (float)wps[i + 1].PosY, (float)wps[i + 1].PosZ); + return total; + } + + /// 沿航路弧长 s 处的三维位置(分段线性插值)。 + /// s≤0 返回起点,s≥TotalLength 返回终点。 + public static (float X, float Y, float Z) PositionAt(IReadOnlyList wps, float arcLength) + { + if (wps == null || wps.Count == 0) return (0f, 0f, 0f); + if (wps.Count == 1 || arcLength <= 0f) + return ((float)wps[0].PosX, (float)wps[0].PosY, (float)wps[0].PosZ); + + float remaining = arcLength; + for (int i = 0; i < wps.Count - 1; i++) + { + float segLen = Kinematics.Distance3D( + (float)wps[i].PosX, (float)wps[i].PosY, (float)wps[i].PosZ, + (float)wps[i + 1].PosX, (float)wps[i + 1].PosY, (float)wps[i + 1].PosZ); + if (remaining <= segLen) + { + float t = segLen > 0.0001f ? remaining / segLen : 0f; + return ( + (float)(wps[i].PosX + (wps[i + 1].PosX - wps[i].PosX) * t), + (float)(wps[i].PosY + (wps[i + 1].PosY - wps[i].PosY) * t), + (float)(wps[i].PosZ + (wps[i + 1].PosZ - wps[i].PosZ) * t)); + } + remaining -= segLen; + } + // 超过总弧长,返回终点 + var last = wps[^1]; + return ((float)last.PosX, (float)last.PosY, (float)last.PosZ); + } + + /// 航路上离目标点 (x,z) 最近的点对应的弧长(水平投影最近)。 + /// 用于 planner 定位"无人机穿越点在航路上的弧长位置"。 + /// 算法:逐段求点到线段的最近点,取全局最近者。 + public static float ArcLengthNearestTo(IReadOnlyList wps, float x, float z) + { + if (wps == null || wps.Count < 2) return 0f; + + float bestArc = 0f; + float bestDist = float.MaxValue; + float accumArc = 0f; + + for (int i = 0; i < wps.Count - 1; i++) + { + float ax = (float)wps[i].PosX, az = (float)wps[i].PosZ; + float bx = (float)wps[i + 1].PosX, bz = (float)wps[i + 1].PosZ; + float segDx = bx - ax, segDz = bz - az; + float segLenSq = segDx * segDx + segDz * segDz; + + float t = 0f; + if (segLenSq > 0.0001f) + { + t = ((x - ax) * segDx + (z - az) * segDz) / segLenSq; + t = Math.Max(0f, Math.Min(1f, t)); + } + float projX = ax + segDx * t; + float projZ = az + segDz * t; + float distSq = (projX - x) * (projX - x) + (projZ - z) * (projZ - z); + + if (distSq < bestDist) + { + bestDist = distSq; + float segLen = (float)Math.Sqrt(segLenSq); + bestArc = accumArc + t * segLen; + } + accumArc += (float)Math.Sqrt(segLenSq); + } + return bestArc; + } + + /// 从航路起点匀速运动到弧长 s 处的飞行时间(秒)。 + /// 匀速直线模型,时间 = 弧长 / 速度。内部调用 Kinematics.TravelTime。 + /// 速度 km/h(与 TypicalSpeed 单位一致),必须 > 0 + public static float TravelTimeTo(IReadOnlyList wps, float arcLength, float speedKmh) + { + if (speedKmh <= 0) + throw new ArgumentException("速度必须 > 0", nameof(speedKmh)); + // 弧长 = 距离,匀速直线时间 = 距离/速度 + // 用 Kinematics.TravelTime 保持单位换算唯一(km/h → m/s 在 Kinematics 内) + return arcLength / (speedKmh / 3.6f); + } + + /// 沿航路方向给定弧长 s 处的水平单位切向量 (DirX, DirZ)。 + /// 用于 planner 沿航路方向布云(offset 沿切向,不再写死 X 轴)。 + /// 弧长超出范围时取末段方向。 + public static (float DirX, float DirZ) TangentAt(IReadOnlyList wps, float arcLength) + { + if (wps == null || wps.Count < 2) return (1f, 0f); + + float remaining = Math.Max(0f, arcLength); + for (int i = 0; i < wps.Count - 1; i++) + { + float dx = (float)wps[i + 1].PosX - (float)wps[i].PosX; + float dz = (float)wps[i + 1].PosZ - (float)wps[i].PosZ; + float segLen = (float)Math.Sqrt(dx * dx + dz * dz); + if (remaining <= segLen || i == wps.Count - 2) + { + if (segLen < 0.0001f) return (1f, 0f); + return (dx / segLen, dz / segLen); + } + remaining -= segLen; + } + // 兜底:末段方向 + float ldx = (float)wps[^1].PosX - (float)wps[^2].PosX; + float ldz = (float)wps[^1].PosZ - (float)wps[^2].PosZ; + float llen = (float)Math.Sqrt(ldx * ldx + ldz * ldz); + return llen < 0.0001f ? (1f, 0f) : (ldx / llen, ldz / llen); + } + } +} diff --git a/src/CounterDrone.Core/Simulation/DroneEntity.cs b/src/CounterDrone.Core/Simulation/DroneEntity.cs index b82620a..8ecf7e2 100644 --- a/src/CounterDrone.Core/Simulation/DroneEntity.cs +++ b/src/CounterDrone.Core/Simulation/DroneEntity.cs @@ -27,6 +27,8 @@ namespace CounterDrone.Core.Simulation public float PrevZ { get; private set; } public float FormationOffsetX { get; set; } public float FormationOffsetY { get; set; } + /// 沿航路已飞行弧长(米),由 RouteGeometry 驱动 + private float _traveledArc; public DroneEntity(string id, string groupId, TargetConfig config, List route, int formationIndex, float lateralSpacing, int longitudinalIndex, float longitudinalSpacing, FormationMode mode) @@ -103,40 +105,34 @@ namespace CounterDrone.Core.Simulation { if (Status != DroneStatus.Flying) return; if (Route.Count == 0) return; - if (CurrentWaypointIndex >= Route.Count - 1) + + // 运动模型统一在 RouteGeometry:弧长推进 + 位置查询。 + // 无人机严格沿航路匀速飞行,不受风偏影响(风偏已移除,见风偏补偿说明)。 + // windSpeed/windDir 保留签名供未来飞控模型使用,当前不参与运动。 + float totalLen = RouteGeometry.TotalLength(Route); + if (totalLen <= 0f) { Status = DroneStatus.ReachedTarget; return; } - var target = Route[CurrentWaypointIndex + 1]; - var speedMs = TypicalSpeed / 3.6f; - var dx = (float)(target.PosX - PosX); - var dy = (float)(target.PosY - PosY); - var dz = (float)(target.PosZ - PosZ); - var dist = (float)Math.Sqrt(dx * dx + dy * dy + dz * dz); - var step = speedMs * deltaTime; + float speedMs = TypicalSpeed / 3.6f; + _traveledArc += speedMs * deltaTime; - if (dist < 1.0f || step >= dist) + if (_traveledArc >= totalLen) { - PosX = (float)target.PosX; - PosY = (float)target.PosY; - PosZ = (float)target.PosZ; - CurrentWaypointIndex++; - if (CurrentWaypointIndex >= Route.Count - 1) - Status = DroneStatus.ReachedTarget; + var end = Route[^1]; + PosX = (float)end.PosX; + PosY = (float)end.PosY; + PosZ = (float)end.PosZ; + Status = DroneStatus.ReachedTarget; return; } - var ratio = step / dist; - PosX += dx * ratio; - PosY += dy * ratio; - PosZ += dz * ratio; - - (float wx, float wy, float wz) = Kinematics.WindToVector(windDir, windSpeed); - PosX += wx * deltaTime; - PosY += wy * deltaTime; - PosZ += wz * deltaTime; + var (x, y, z) = RouteGeometry.PositionAt(Route, _traveledArc); + PosX = x; + PosY = y; + PosZ = z; } public void ApplyDamage(float damage) diff --git a/src/CounterDrone.Core/Simulation/SimulationEngine.cs b/src/CounterDrone.Core/Simulation/SimulationEngine.cs index 1a89aff..3ef9ba9 100644 --- a/src/CounterDrone.Core/Simulation/SimulationEngine.cs +++ b/src/CounterDrone.Core/Simulation/SimulationEngine.cs @@ -47,8 +47,6 @@ namespace CounterDrone.Core.Simulation private readonly List _fireSchedule = new(); private int _nextFireIndex; private readonly List _allEvents = new(); - private System.Text.StringBuilder _hitLog = new(); - private float _totalPathInCloud; private SQLiteConnection _frameDb = null!; private string _taskId = string.Empty; private readonly IDefensePlanner _planner; @@ -130,8 +128,6 @@ namespace CounterDrone.Core.Simulation _munitions.Clear(); _clouds.Clear(); _allEvents.Clear(); - _hitLog.Clear(); - _hitLog.AppendLine("droneX,droneY,cloudX,cloudY,dist,radius,inside"); FrameIndex = 0; SimulationTime = 0; _entityCounter = 0; @@ -146,19 +142,9 @@ namespace CounterDrone.Core.Simulation { var result = _planner.Plan(fireUnits, threats, _scene); SetFireSchedule(result.Best.MergedSchedule); - // 诊断:输出 Planner 云团预期位置 - var lines = new List { "idx,fireTime,tx,ty,tz,mv" }; - for (int i = 0; i < _fireSchedule.Count; i++) - { - var f = _fireSchedule[i]; - lines.Add($"{i},{f.FireTime:F3},{f.TargetX:F1},{f.TargetY:F1},{f.TargetZ:F1},{f.MuzzleVelocity:F0}"); - } - System.IO.File.WriteAllLines(@"C:\Users\Tellme\Desktop\planner_targets.csv", lines); } } - // 初始化 cloud dump - System.IO.File.WriteAllText(@"C:\Users\Tellme\Desktop\cloud_actual.csv", "m.id,arrivalTime,px,py,pz,cloud.id,tx,ty,tz\n"); _frameDb = _frameStore.CreateOrOpen(_taskId); State = SimulationState.Running; } @@ -231,8 +217,6 @@ namespace CounterDrone.Core.Simulation _munitions.Remove(m); OnCloudGenerated?.Invoke(cloud); frameEvents.Add(new SimEvent { Type = SimEventType.CloudGenerated, OccurredAt = m.ArrivalTime, Description = "云团生成" }); - System.IO.File.AppendAllText(@"C:\Users\Tellme\Desktop\cloud_actual.csv", - $"{m.Id},{m.ArrivalTime:F3},{m.PosX:F1},{m.PosY:F1},{m.PosZ:F1},{cloud.Id},{m.TargetX:F1},{m.TargetY:F1},{m.TargetZ:F1}\n"); } } @@ -262,19 +246,24 @@ namespace CounterDrone.Core.Simulation } // 6. 毁伤判定:积分路径段在云内的时间 + // 在云团参考系计算:云团中心在该参考系为原点。 + // 云团本 tick 内也在移动(风偏),故 Prev 帧的云团中心 = 本帧中心 − 风位移。 + // 不做此修正的话 PathInSphere 会把云团当静止球,每 tick 引入 ~2m 误差。 + var (wvx, wvy, wvz) = Kinematics.WindToVector((WindDirection)_scene.WindDirection, (float)_scene.WindSpeed); + float cloudDx = wvx * scaledDt, cloudDy = wvy * scaledDt, cloudDz = wvz * scaledDt; foreach (var drone in _drones.Where(d => d.Status == DroneStatus.Flying)) { foreach (var cloud in _clouds) { + float ccx = cloud.Dispersion.Center.X, ccy = cloud.Dispersion.Center.Y, ccz = cloud.Dispersion.Center.Z; + // 无人机线段换算到云团参考系:相对位置 = drone - cloud float inCloudTime = DamageAssessment.PathInSphere( - drone.PrevX, drone.PrevY, drone.PrevZ, - drone.PosX, drone.PosY, drone.PosZ, - cloud.Dispersion.Center.X, cloud.Dispersion.Center.Y, cloud.Dispersion.Center.Z, + drone.PrevX - (ccx - cloudDx), drone.PrevY - (ccy - cloudDy), drone.PrevZ - (ccz - cloudDz), + drone.PosX - ccx, drone.PosY - ccy, drone.PosZ - ccz, + 0f, 0f, 0f, cloud.Dispersion.EffectiveRadius); if (inCloudTime > 0 && cloud.Dispersion.CoreDensity >= (float)_ammoSpec.EffectiveConcentration) { - _totalPathInCloud += inCloudTime; - System.IO.File.AppendAllText(@"C:\Users\Tellme\Desktop\path_debug.txt", $"t={SimulationTime:F1} drone={drone.PosX:F1} cloud={cloud.Dispersion.Center.X:F1} path={inCloudTime:F1} total={_totalPathInCloud:F1}\n"); float exposureIncrement = inCloudTime / (drone.TypicalSpeed / 3.6f); drone.ExposureTime += exposureIncrement; var dmg = _damageModel.CalculateDamage(drone.TargetType, drone.PowerType, cloud.AerosolType, cloud.Dispersion.CoreDensity, drone.ExposureTime, exposureIncrement); @@ -309,7 +298,6 @@ namespace CounterDrone.Core.Simulation if (_drones.All(d => d.Status != DroneStatus.Flying)) { State = SimulationState.Completed; - System.IO.File.WriteAllText(@"C:\Users\Tellme\Desktop\path_in_cloud.txt", $"totalPath={_totalPathInCloud:F1}m"); OnSimulationEnded?.Invoke(); frameEvents.Add(new SimEvent { Type = SimEventType.SimulationEnd, OccurredAt = SimulationTime }); } diff --git a/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.dll b/src/Unity/Assets/Plugins/CounterDrone.Core/CounterDrone.Core.dll index b75bf2f408e8abec6a0f220654cda8db7bbb71f8..34d7ca735bcfbe2009a778c70f0f072882c78af5 100644 GIT binary patch literal 135168 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 H68L`sfm>p` literal 123904 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�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%0AdbT??O)kF^8-7pr?SC!_^1S(?HDOcq$g=a1e7i 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 T1*>Nt`Um3ok8J+GYJvX? { droneGroup }, detail.Scene); + var result = new DefaultDefensePlanner(ammoCatalog, PlannerConfig.Load(paths)).Plan(fireUnits, new List { droneGroup }, detail.Scene); scenarioMgr.SaveCloud(taskId, new CloudDispersal { PositionX = (droneGroup.Waypoints[0].PosX + droneGroup.Waypoints[^1].PosX) / 2, diff --git a/src/Unity/Assets/Scripts/Managers/SimulationBootstrap.cs b/src/Unity/Assets/Scripts/Managers/SimulationBootstrap.cs index 0985d8c..a459f71 100644 --- a/src/Unity/Assets/Scripts/Managers/SimulationBootstrap.cs +++ b/src/Unity/Assets/Scripts/Managers/SimulationBootstrap.cs @@ -67,7 +67,7 @@ namespace CounterDrone.Unity // 推荐方案 var detail = scenario.GetTaskDetail(taskId); var ammoCatalog = db.Table().ToList(); - var planner = new DefaultDefensePlanner(ammoCatalog); + var planner = new DefaultDefensePlanner(ammoCatalog, PlannerConfig.Load(paths)); var droneGroup = new DroneGroup { GroupId = "default", diff --git a/src/Unity/Assets/Scripts/Managers/SimulationRunner.cs b/src/Unity/Assets/Scripts/Managers/SimulationRunner.cs index 9ebe475..fcaa156 100644 --- a/src/Unity/Assets/Scripts/Managers/SimulationRunner.cs +++ b/src/Unity/Assets/Scripts/Managers/SimulationRunner.cs @@ -48,7 +48,7 @@ namespace CounterDrone.Unity new RoutePlanRepository(_db), new WaypointRepository(_db)); _frameStore = new FrameDataStore(_paths); - _engine = new SimulationEngine(_scenario, _frameStore, new DamageModelRouter(), _paths, new DefaultDefensePlanner(DefaultAmmunition.GetAll())); + _engine = new SimulationEngine(_scenario, _frameStore, new DamageModelRouter(), _paths, new DefaultDefensePlanner(DefaultAmmunition.GetAll(), PlannerConfig.Load(_paths))); } public void LoadAndStart(string taskId) diff --git a/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs b/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs index 4c1a392..94af12c 100644 --- a/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs +++ b/test/unit/CounterDrone.Core.Tests/DefensePlannerTests.cs @@ -59,7 +59,7 @@ namespace CounterDrone.Core.Tests private PlannerResult Plan(List units, DroneGroup threat, CombatScene? env = null) - => new DefaultDefensePlanner(TestAmmo).Plan(units, new() { threat }, env ?? new CombatScene()); + => new DefaultDefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(units, new() { threat }, env ?? new CombatScene()); // ═══════════════════════════════════════ // 威胁排序 @@ -233,7 +233,7 @@ namespace CounterDrone.Core.Tests [Fact] public void Allocation_MultiUnit_PoolsChannels() { - var result = new DefaultDefensePlanner(TestAmmo).Plan( + var result = new DefaultDefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan( new() { MakeGroundUnit("u0", 5000), MakeGroundUnit("u1", 5100) }, new() { MakeThreat(speed: 200) }, new CombatScene()); Assert.True(result.Best.ThreatsEngaged == 1); @@ -259,8 +259,8 @@ namespace CounterDrone.Core.Tests // 边界 // ═══════════════════════════════════════ - [Fact] public void Edge_NoUnits_Unengaged() => Assert.Equal(1, new DefaultDefensePlanner(TestAmmo).Plan(new(), new() { MakeThreat() }, new CombatScene()).Best.ThreatsUnengaged); - [Fact] public void Edge_NoThreats_Empty() => Assert.Equal(0, new DefaultDefensePlanner(TestAmmo).Plan(new() { MakeGroundUnit("u0", 5000) }, new(), new CombatScene()).Best.ThreatsEngaged); + [Fact] public void Edge_NoUnits_Unengaged() => Assert.Equal(1, new DefaultDefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(new(), new() { MakeThreat() }, new CombatScene()).Best.ThreatsUnengaged); + [Fact] public void Edge_NoThreats_Empty() => Assert.Equal(0, new DefaultDefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan(new() { MakeGroundUnit("u0", 5000) }, new(), new CombatScene()).Best.ThreatsEngaged); [Fact] public void Edge_OutOfRange_Ground() @@ -293,7 +293,7 @@ namespace CounterDrone.Core.Tests { var a = MakeThreat(PowerType.Piston, 120); a.GroupId = "g0"; var b = MakeThreat(PowerType.Jet, 300); b.GroupId = "g1"; - var result = new DefaultDefensePlanner(TestAmmo).Plan( + var result = new DefaultDefensePlanner(TestAmmo, TestPlannerConfig.Instance).Plan( new() { MakeGroundUnit("u0", 5000), MakeGroundUnit("u1", 5100) }, new() { a, b }, new CombatScene()); Assert.Equal(2, result.Best.ThreatsEngaged); @@ -310,5 +310,124 @@ namespace CounterDrone.Core.Tests Assert.True(result.Critical.Assignments.Count > 0); Assert.True(result.Critical.OverallProbability >= 0.4f); } + + // ═══════════════════════════════════════ + // P1 风偏补偿:云团生成后会被风吹偏 windVec × expansionTime, + // Planner 须逆风预置抛撒点,使云团漂移回无人机穿越点。 + // tx,tz = 无人机穿越点(航路中点,不变) + // cloudGenX/Z = 抛撒点(FireEvent.TargetX/Z,含风偏补偿) + // ═══════════════════════════════════════ + + [Fact] + public void WindOffset_NoWind_NoDirectionalBias() + { + // 无风时,无论 WindDirection 取何值,抛撒点都应相同 + // (风偏补偿 = windVec × expansionTime,windVec=0 时补偿为 0) + var threat = MakeThreat(startX: 0, endX: 10000); + + // 风向取不同值,但风速都是 0 + var envN = new CombatScene { WindSpeed = 0, WindDirection = (int)WindDirection.N }; + var envE = new CombatScene { WindSpeed = 0, WindDirection = (int)WindDirection.E }; + + var rN = Plan(new() { MakeGroundUnit("u0", 5000) }, threat, envN); + var rE = Plan(new() { MakeGroundUnit("u0", 5000) }, threat, envE); + + float xN = rN.Best.MergedSchedule[0].TargetX; + float xE = rE.Best.MergedSchedule[0].TargetX; + Assert.True(Math.Abs(xN - xE) < 1f, + $"无风时风向不应影响抛撒点:N={xN:F1}, E={xE:F1}"); + } + + [Fact] + public void WindOffset_EastWind_TargetShiftedUpwind() + { + // 东风(WindDirection.E):WindToVector 返回 (speed, 0, 0),云团被吹向 +X。 + // 抛撒点应逆风预置 → TargetX < 航路中点 X=5000 + var threat = MakeThreat(startX: 0, endX: 10000); + var env = new CombatScene { WindSpeed = 10f, WindDirection = (int)WindDirection.E }; + + var calmResult = Plan(new() { MakeGroundUnit("u0", 5000) }, threat, new CombatScene { WindSpeed = 0 }); + var windyResult = Plan(new() { MakeGroundUnit("u0", 5000) }, threat, env); + + float calmX = calmResult.Best.MergedSchedule[0].TargetX; + float windyX = windyResult.Best.MergedSchedule[0].TargetX; + Assert.True(windyX < calmX, + $"东风下抛撒点 X={windyX:F1} 应小于无风 X={calmX:F1}(逆风预置补偿)"); + // 偏移量级合理性:expansionTime≈30s,风速 10m/s → 偏移约 300m + Assert.True(calmX - windyX > 100f, + $"偏移量 {calmX - windyX:F1}m 应有显著量级(期望 ~300m)"); + } + + [Fact] + public void WindOffset_WestWind_TargetShiftedOpposite() + { + // 西风:风矢量 (-speed, 0, 0),云团被吹向 -X。 + // 抛撒点应在 +X 方向(TargetX > 中点),与东风对称 + var threat = MakeThreat(startX: 0, endX: 10000); + var env = new CombatScene { WindSpeed = 10f, WindDirection = (int)WindDirection.W }; + + var calmResult = Plan(new() { MakeGroundUnit("u0", 5000) }, threat, new CombatScene { WindSpeed = 0 }); + var windyResult = Plan(new() { MakeGroundUnit("u0", 5000) }, threat, env); + + float calmX = calmResult.Best.MergedSchedule[0].TargetX; + float windyX = windyResult.Best.MergedSchedule[0].TargetX; + Assert.True(windyX > calmX, + $"西风下抛撒点 X={windyX:F1} 应大于无风 X={calmX:F1}(与东风相反方向)"); + } + + [Fact] + public void WindOffset_NorthWind_ShiftsZ() + { + // 北风:WindToVector 返回 (0, 0, speed),云团被吹向 +Z。 + // 抛撒点应在 -Z 方向(TargetZ 更小) + var threat = MakeThreat(startX: 0, endX: 10000); + threat.Waypoints[0].PosZ = 100; + threat.Waypoints[1].PosZ = 100; + var env = new CombatScene { WindSpeed = 10f, WindDirection = (int)WindDirection.N }; + + var calmResult = Plan(new() { MakeGroundUnit("u0", 5000) }, threat, new CombatScene { WindSpeed = 0 }); + var windyResult = Plan(new() { MakeGroundUnit("u0", 5000) }, threat, env); + + float calmZ = calmResult.Best.MergedSchedule[0].TargetZ; + float windyZ = windyResult.Best.MergedSchedule[0].TargetZ; + Assert.True(windyZ < calmZ, + $"北风下抛撒点 Z={windyZ:F1} 应小于无风 Z={calmZ:F1}(Z 方向逆风补偿)"); + } + + // ═══════════════════════════════════════ + // 航路感知布局:云团沿真实航路方向分布,不写死 X 轴。 + // Z 向航路(南北飞)的多发云团,X 坐标应集中在航路 X 附近(非发散)。 + // ═══════════════════════════════════════ + + [Fact] + public void RouteAware_ZDirectionRoute_CloudsOnRoute() + { + // Z 向航路:从 (5000,0) 飞向 (5000,10000),即沿 +Z 方向 + // 之前 offset 写死 X 轴时,多发云团会沿 X 发散到 5000±N×spacing,偏离航路 + // 改用 RouteGeometry 后,offset 沿航路切向(+Z),云团应集中在 X=5000 + var threat = new DroneGroup + { + GroupId = "default", + Target = new TargetConfig + { + TargetType = (int)TargetType.Piston, PowerType = (int)PowerType.Piston, + Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500, + }, + Waypoints = new List + { + new() { PosX = 5000, PosY = 500, PosZ = 0, Speed = 200 }, + new() { PosX = 5000, PosY = 500, PosZ = 10000, Speed = 200 }, + }, + }; + // 火力单元放在航路起点附近(X=5000) + var unit = MakeGroundUnit("u0", 5000); + var result = Plan(new() { unit }, threat, new CombatScene()); + + // 所有抛撒点的 X 应集中在 5000 附近(±少量风偏,此处无风) + // 若 offset 仍写死 X 轴,多发会沿 X 散开到 5000±N×40 + Assert.All(result.Best.MergedSchedule, fe => + Assert.True(Math.Abs(fe.TargetX - 5000) < 50f, + $"Z 向航路下抛撒点 X={fe.TargetX:F1} 应在航路 X=5000 附近(±50m),而非沿 X 发散")); + } } } diff --git a/test/unit/CounterDrone.Core.Tests/DispersionModelTests.cs b/test/unit/CounterDrone.Core.Tests/DispersionModelTests.cs index c266140..fac37c3 100644 --- a/test/unit/CounterDrone.Core.Tests/DispersionModelTests.cs +++ b/test/unit/CounterDrone.Core.Tests/DispersionModelTests.cs @@ -70,5 +70,53 @@ namespace CounterDrone.Core.Tests Assert.True(inert.EffectiveConcentration is >= 0.0001 and < 0.1, "有效浓度阈值应 ≥ 0.0001"); Assert.True(inert.SourceStrength is > 1 and < 100, "源强应 1~100kg"); } + + // ═══════════════════════════════════════ + // P0 修复:Phase3 高斯扩散应使用环境真实天气的稳定度 + // 原先 Tick 中 GetStabilityClass 写死 (WeatherType)0 (Sunny), + // 导致任何天气下 Phase3 扩散行为都相同。 + // ═══════════════════════════════════════ + + [Fact] + public void Phase3_DifferentWeather_ProduceDifferentDensity() + { + // 同样风速下,雾天(E稳定)扩散慢 → 浓度高;晴天低风(A极不稳定)扩散快 → 浓度低 + // 两模型都推进到 Phase3 (>30s),仅天气不同 + float windSpeed = 3f; + + var fogModel = new GaussianPuffDispersion(); + fogModel.Initialize(Ammo(), new CombatScene { WeatherType = (int)WeatherType.Fog, WindSpeed = windSpeed }, + new Algorithms.Vector3(0, 0, 0), 0f); + fogModel.Tick(40f, windSpeed, WindDirection.N); // 进入 Phase3 + + var sunnyModel = new GaussianPuffDispersion(); + sunnyModel.Initialize(Ammo(), new CombatScene { WeatherType = (int)WeatherType.Sunny, WindSpeed = windSpeed }, + new Algorithms.Vector3(0, 0, 0), 0f); + sunnyModel.Tick(40f, windSpeed, WindDirection.N); + + // 雾天稳定度高 → 云团收得紧 → 中心浓度显著高于晴天低风 + Assert.True(fogModel.CoreDensity > sunnyModel.CoreDensity, + $"雾天浓度={fogModel.CoreDensity:F6} 应高于晴天={sunnyModel.CoreDensity:F6}(P0: 天气应真实影响扩散)"); + } + + [Fact] + public void Phase3_Night_MoreStableThanSunny() + { + // 夜间(F极稳定) vs 晴天低风(A极不稳定),风速相同 + float windSpeed = 2f; + + var nightModel = new GaussianPuffDispersion(); + nightModel.Initialize(Ammo(), new CombatScene { WeatherType = (int)WeatherType.Night, WindSpeed = windSpeed }, + new Algorithms.Vector3(0, 0, 0), 0f); + nightModel.Tick(40f, windSpeed, WindDirection.N); + + var sunnyModel = new GaussianPuffDispersion(); + sunnyModel.Initialize(Ammo(), new CombatScene { WeatherType = (int)WeatherType.Sunny, WindSpeed = windSpeed }, + new Algorithms.Vector3(0, 0, 0), 0f); + sunnyModel.Tick(40f, windSpeed, WindDirection.N); + + Assert.True(nightModel.CoreDensity > sunnyModel.CoreDensity, + $"夜间浓度={nightModel.CoreDensity:F6} 应高于晴天={sunnyModel.CoreDensity:F6}"); + } } } diff --git a/test/unit/CounterDrone.Core.Tests/DroneEntityTests.cs b/test/unit/CounterDrone.Core.Tests/DroneEntityTests.cs index 4c74089..748736e 100644 --- a/test/unit/CounterDrone.Core.Tests/DroneEntityTests.cs +++ b/test/unit/CounterDrone.Core.Tests/DroneEntityTests.cs @@ -76,16 +76,6 @@ namespace CounterDrone.Core.Tests Assert.Equal(DroneStatus.Destroyed, drone.Status); } - [Fact] - public void Wind_AffectsPosition() - { - var drone = new DroneEntity("d1", "g1", CreateConfig(), - CreateRoute(0, 1000, 300, 60), 0, 50, 0, 0, FormationMode.Single); - float xBefore = drone.PosX; - drone.Update(1f, 20f, WindDirection.E); - Assert.True(drone.PosX > xBefore + 15f); - } - [Fact] public void Formation_Lateral_ZOffsetsDiffer() { @@ -130,5 +120,48 @@ namespace CounterDrone.Core.Tests var d1 = new DroneEntity("d1", "g1", CreateConfig(), route, 1, 30, 0, 0, FormationMode.Swarm); Assert.True(d0.PosX != d1.PosX || d0.PosZ != d1.PosZ); } + + // ═══════════════════════════════════════ + // RouteGeometry 驱动的多 waypoint 折线运动 + // 验证无人机沿 L 形航路(X 段→Z 段)正确移动并最终到达终点 + // ═══════════════════════════════════════ + + [Fact] + public void MultiWaypoint_LShape_MovesAlongBothSegments() + { + // L 形航路:(0,0) → (100,0) → (100,100),总长 200m,速度 60km/h≈16.67m/s + var route = new List + { + new() { PosX = 0, PosY = 300, PosZ = 0, Altitude = 300, Speed = 60 }, + new() { PosX = 100, PosY = 300, PosZ = 0, Altitude = 300, Speed = 60 }, + new() { PosX = 100, PosY = 300, PosZ = 100, Altitude = 300, Speed = 60 }, + }; + var drone = new DroneEntity("d1", "g1", CreateConfig(speed: 60), route, + 0, 0, 0, 0, FormationMode.Single); + + // 推进到第一段中点(弧长 50):应在 X=50, Z=0 + drone.Update(50f / (60f / 3.6f), 0, WindDirection.N); + Assert.Equal(50f, drone.PosX, 1); + Assert.Equal(0f, drone.PosZ, 1); + Assert.Equal(DroneStatus.Flying, drone.Status); + + // 推进到拐点(弧长 100):应在 X=100, Z=0 + drone.Update(50f / (60f / 3.6f), 0, WindDirection.N); + Assert.Equal(100f, drone.PosX, 1); + Assert.Equal(0f, drone.PosZ, 1); + Assert.Equal(DroneStatus.Flying, drone.Status); + + // 推进到第二段中点(弧长 150):应在 X=100, Z=50 + drone.Update(50f / (60f / 3.6f), 0, WindDirection.N); + Assert.Equal(100f, drone.PosX, 1); + Assert.Equal(50f, drone.PosZ, 1); + Assert.Equal(DroneStatus.Flying, drone.Status); + + // 推进到终点(弧长 200):应在 X=100, Z=100,状态 ReachedTarget + drone.Update(50f / (60f / 3.6f), 0, WindDirection.N); + Assert.Equal(100f, drone.PosX, 1); + Assert.Equal(100f, drone.PosZ, 1); + Assert.Equal(DroneStatus.ReachedTarget, drone.Status); + } } } diff --git a/test/unit/CounterDrone.Core.Tests/EdgeCaseTests.cs b/test/unit/CounterDrone.Core.Tests/EdgeCaseTests.cs index 109c3f2..68cce2b 100644 --- a/test/unit/CounterDrone.Core.Tests/EdgeCaseTests.cs +++ b/test/unit/CounterDrone.Core.Tests/EdgeCaseTests.cs @@ -65,7 +65,7 @@ namespace CounterDrone.Core.Tests }); var engine = new SimulationEngine(_scenario, new FrameDataStore(new TestPathProvider(_testDir)), - new DamageModelRouter(), new TestPathProvider(_testDir), new DefaultDefensePlanner(DefaultAmmunition.GetAll())); + new DamageModelRouter(), new TestPathProvider(_testDir), new DefaultDefensePlanner(DefaultAmmunition.GetAll(), TestPlannerConfig.Instance)); engine.Initialize(_taskId); engine.TimeScale = 4f; @@ -107,7 +107,7 @@ namespace CounterDrone.Core.Tests }); var engine = new SimulationEngine(_scenario, new FrameDataStore(new TestPathProvider(_testDir)), - new DamageModelRouter(), new TestPathProvider(_testDir), new DefaultDefensePlanner(DefaultAmmunition.GetAll())); + new DamageModelRouter(), new TestPathProvider(_testDir), new DefaultDefensePlanner(DefaultAmmunition.GetAll(), TestPlannerConfig.Instance)); engine.Initialize(_taskId); engine.TimeScale = 4f; @@ -148,7 +148,7 @@ namespace CounterDrone.Core.Tests }); var engine = new SimulationEngine(_scenario, new FrameDataStore(new TestPathProvider(_testDir)), - new DamageModelRouter(), new TestPathProvider(_testDir), new DefaultDefensePlanner(DefaultAmmunition.GetAll())); + new DamageModelRouter(), new TestPathProvider(_testDir), new DefaultDefensePlanner(DefaultAmmunition.GetAll(), TestPlannerConfig.Instance)); engine.Initialize(_taskId); engine.TimeScale = 4f; diff --git a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs index 60aa6a1..2f2705d 100644 --- a/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs +++ b/test/unit/CounterDrone.Core.Tests/FullPipelineTests.cs @@ -49,7 +49,7 @@ namespace CounterDrone.Core.Tests private SimulationEngine RunSimulation(int maxTicks, float tickDt = 1f / 20f, float timeScale = 8f) { - var engine = new SimulationEngine(_scenario, _frameStore, new DamageModelRouter(), _paths, new DefaultDefensePlanner(_ammoCatalog)); + var engine = new SimulationEngine(_scenario, _frameStore, new DamageModelRouter(), _paths, new DefaultDefensePlanner(_ammoCatalog, TestPlannerConfig.Instance)); engine.Initialize(_taskId); engine.TimeScale = timeScale; // 8倍速加速 @@ -249,6 +249,72 @@ namespace CounterDrone.Core.Tests Assert.True(eng.Drones.All(d => d.Status == DroneStatus.Destroyed), msg); } + // ═══════════════════════════════════════════════ + // 场景 3b:活塞式 + 西风 5m/s — 验证风偏补偿后仍能拦截 + // 航路 X:0→5000 Z:0;西风 WindToVector(W)=(-5,0,0) 把云团吹向 -X + // Planner 应逆风预置抛撒点(+X方向),云团漂移 ~150m 后回到航路 + // ═══════════════════════════════════════════════ + + [Fact] + public void Scenario_Piston_Windy_InertGasIntercept() + { + var task = _scenario.CreateTask("活塞+西风拦截测试", ""); + _taskId = task.Id; + + _scenario.SaveScene(_taskId, new CombatScene + { + WeatherType = (int)WeatherType.Sunny, + WindSpeed = 5, + WindDirection = (int)WindDirection.W, + }); + _scenario.SaveTarget(_taskId, new TargetConfig + { + GroupId = "default", TargetType = (int)TargetType.Piston, + PowerType = (int)PowerType.Piston, + Quantity = 1, + TypicalSpeed = 200, TypicalAltitude = 500, + }); + _scenario.SaveRoute(_taskId, "default", new RoutePlan + { + FormationMode = (int)FormationMode.Formation, + LateralSpacing = 50, + LateralCount = 1, + LongitudinalCount = 1, + }, + new List + { + new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 150 }, + new Waypoint { PosX = 5000, PosY = 500, PosZ = 0, Speed = 150 }, + }); + _scenario.SaveDeployment(_taskId, new List + { + MakeEquipment(DefaultFireUnits.GetById("ground-light"), AerosolType.InertGas, 1, 1500, 0, 50), + }); + _scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 }); + + var eng = RunSimulation(8000); + var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched); + var clouds = eng.Events.Count(e => e.Type == SimEventType.CloudGenerated); + + var dump = new System.Text.StringBuilder(); + dump.AppendLine("=== 活塞+西风5m/s ==="); + dump.AppendLine("cloudCreatedAt,centerX,centerY,centerZ,radius,density,elapsed"); + foreach (var c in eng.Clouds) + dump.AppendLine($"{c.CreatedAt:F3},{c.Dispersion.Center.X:F1},{c.Dispersion.Center.Y:F1},{c.Dispersion.Center.Z:F1},{c.Dispersion.EffectiveRadius:F2},{c.Dispersion.CoreDensity:F6},{c.Dispersion.Elapsed:F1}"); + dump.AppendLine("=== 无人机 ==="); + var drone = eng.Drones[0]; + dump.AppendLine($"status={drone.Status} hp={drone.Hp:F3} exposureTime={drone.ExposureTime:F3}"); + dump.AppendLine($"endPos=({drone.PosX:F1},{drone.PosY:F1},{drone.PosZ:F1})"); + System.IO.File.WriteAllText(@"C:\Users\Tellme\Desktop\piston_windy.txt", dump.ToString()); + + var msg = $"PistonWindy: launched={launched} clouds={clouds}"; + foreach (var d in eng.Drones) + msg += $" [{d.Status} hp={d.Hp:F2}]"; + Assert.True(launched > 0, msg); + Assert.True(eng.Drones.All(d => d.Status == DroneStatus.Destroyed), msg); + } + + // ═══════════════════════════════════════════════ // 场景 4:喷气发动机 → 算法推荐活性材料 // ═══════════════════════════════════════════════ @@ -351,5 +417,69 @@ namespace CounterDrone.Core.Tests Assert.True(launched > 0, msg); Assert.True(drone.Hp < 0.1f, msg); } + + // ═══════════════════════════════════════════════ + // 场景 5b:空基平台 + 东风 5m/s — 验证风偏补偿后仍能拦截 + // 航路 X:0→10000 Z:0;东风 WindToVector(E)=(5,0,0) 把云团吹向 +X + // Planner 应逆风预置抛撒点(-X方向),空基平台投放后云团漂移回航路 + // ═══════════════════════════════════════════════ + + [Fact] + public void Scenario_AirBased_Windy_PlatformFliesAndDrops() + { + var task = _scenario.CreateTask("空基+东风拦截测试", ""); + _taskId = task.Id; + + _scenario.SaveScene(_taskId, new CombatScene + { + WeatherType = (int)WeatherType.Sunny, + WindSpeed = 5, + WindDirection = (int)WindDirection.E, + }); + _scenario.SaveTarget(_taskId, new TargetConfig + { + GroupId = "default", + TargetType = (int)TargetType.Piston, + PowerType = (int)PowerType.Piston, + Quantity = 1, + TypicalSpeed = 150, + TypicalAltitude = 500, + }); + _scenario.SaveRoute(_taskId, "default", new RoutePlan { FormationMode = (int)FormationMode.Single }, + new List + { + new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 150 }, + new Waypoint { PosX = 10000, PosY = 500, PosZ = 0, Speed = 150 }, + }); + + _scenario.SaveDeployment(_taskId, new List + { + MakeEquipment(DefaultFireUnits.GetById("air-standard"), AerosolType.InertGas, 3, 1500, 1000, 0), + }); + _scenario.SaveCloudDispersal(_taskId, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 }); + + var eng = RunSimulation(8000); + + Assert.All(eng.Events.Where(e => e.Type == SimEventType.MunitionLaunched), + e => Assert.Contains("空基", e.Description)); + + var drone = eng.Drones[0]; + var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched); + var clouds = eng.Events.Count(e => e.Type == SimEventType.CloudGenerated); + + var dump = new System.Text.StringBuilder(); + dump.AppendLine("=== 空基+东风5m/s ==="); + dump.AppendLine("cloudCreatedAt,centerX,centerY,centerZ,radius,density,elapsed"); + foreach (var c in eng.Clouds) + dump.AppendLine($"{c.CreatedAt:F3},{c.Dispersion.Center.X:F1},{c.Dispersion.Center.Y:F1},{c.Dispersion.Center.Z:F1},{c.Dispersion.EffectiveRadius:F2},{c.Dispersion.CoreDensity:F6},{c.Dispersion.Elapsed:F1}"); + dump.AppendLine("=== 无人机 ==="); + dump.AppendLine($"status={drone.Status} hp={drone.Hp:F3} exposureTime={drone.ExposureTime:F3}"); + dump.AppendLine($"endPos=({drone.PosX:F1},{drone.PosY:F1},{drone.PosZ:F1})"); + System.IO.File.WriteAllText(@"C:\Users\Tellme\Desktop\air_windy.txt", dump.ToString()); + + var msg = $"AirBasedWindy: launched={launched} clouds={clouds} status={drone.Status} hp={drone.Hp:F2}"; + Assert.True(launched > 0, msg); + Assert.True(drone.Hp < 0.1f, msg); + } } } diff --git a/test/unit/CounterDrone.Core.Tests/PlannerConfigTests.cs b/test/unit/CounterDrone.Core.Tests/PlannerConfigTests.cs new file mode 100644 index 0000000..cd7f209 --- /dev/null +++ b/test/unit/CounterDrone.Core.Tests/PlannerConfigTests.cs @@ -0,0 +1,95 @@ +using System; +using System.IO; +using CounterDrone.Core.Algorithms; +using CounterDrone.Core.Models; +using Xunit; + +namespace CounterDrone.Core.Tests +{ + public class PlannerConfigTests : IDisposable + { + private readonly string _testDir; + + public PlannerConfigTests() + { + _testDir = Path.Combine(Path.GetTempPath(), $"cd_cfg_{Guid.NewGuid():N}"); + Directory.CreateDirectory(_testDir); + } + + public void Dispose() + { + if (Directory.Exists(_testDir)) Directory.Delete(_testDir, true); + } + + private void WriteConfig(string json) + => File.WriteAllText(Path.Combine(_testDir, "planner_config.json"), json); + + private static readonly string ValidJson = @" +{ + ""CloudOverlapRatio"": 0.2, + ""CriticalProbabilityThreshold"": 0.5, + ""MaxInterceptProbability"": 0.95, + ""TypeCoefficient"": { + ""HighSpeed"": 4.0, ""FixedWing"": 2.0, ""Piston"": 2.0, + ""Rotor"": 1.0, ""Electric"": 1.0 + }, + ""AmmoMatch"": { + ""Electric"": ""InertGas"", ""Piston"": ""InertGas"", ""Jet"": ""ActiveMaterial"" + } +}"; + + [Fact] + public void Load_ValidJson_ReturnsAllFields() + { + WriteConfig(ValidJson); + var cfg = PlannerConfig.Load(_testDir); + Assert.Equal(0.2f, cfg.CloudOverlapRatio); + Assert.Equal(0.5f, cfg.CriticalProbabilityThreshold); + Assert.Equal(0.95f, cfg.MaxInterceptProbability); + Assert.Equal(4.0f, cfg.TypeCoefficient[TargetType.HighSpeed]); + Assert.Equal(AerosolType.InertGas, cfg.AmmoMatch[PowerType.Piston]); + Assert.Equal(AerosolType.ActiveMaterial, cfg.AmmoMatch[PowerType.Jet]); + } + + [Fact] + public void Load_MissingFile_Throws() + { + // 不写文件 + Assert.Throws(() => PlannerConfig.Load(_testDir)); + } + + [Fact] + public void Load_MissingField_Throws() + { + // 缺 CriticalProbabilityThreshold + WriteConfig(@"{ ""CloudOverlapRatio"": 0.2, ""MaxInterceptProbability"": 0.95, + ""TypeCoefficient"": {""Piston"": 2.0}, ""AmmoMatch"": {""Piston"": ""InertGas""} }"); + Assert.Throws(() => PlannerConfig.Load(_testDir)); + } + + [Fact] + public void Load_OverlapOutOfRange_Throws() + { + // CloudOverlapRatio = 1.0 非法(必须 < 1) + var bad = ValidJson.Replace("0.2", "1.0"); + WriteConfig(bad); + Assert.Throws(() => PlannerConfig.Load(_testDir)); + } + + [Fact] + public void Load_EmptyDataRoot_Throws() + { + Assert.Throws(() => PlannerConfig.Load("")); + } + + [Fact] + public void Load_EnumParsedFromString() + { + // 验证枚举用字符串(非数字)也能正确解析 + WriteConfig(ValidJson); + var cfg = PlannerConfig.Load(_testDir); + Assert.Equal(1.0f, cfg.TypeCoefficient[TargetType.Rotor]); + Assert.Equal(AerosolType.InertGas, cfg.AmmoMatch[PowerType.Electric]); + } + } +} diff --git a/test/unit/CounterDrone.Core.Tests/RouteGeometryTests.cs b/test/unit/CounterDrone.Core.Tests/RouteGeometryTests.cs new file mode 100644 index 0000000..eb246ff --- /dev/null +++ b/test/unit/CounterDrone.Core.Tests/RouteGeometryTests.cs @@ -0,0 +1,187 @@ +using System.Collections.Generic; +using CounterDrone.Core.Algorithms; +using CounterDrone.Core.Models; +using Xunit; + +namespace CounterDrone.Core.Tests +{ + public class RouteGeometryTests + { + private static List Line(float x0, float z0, float x1, float z1, float y = 500f) + => new() + { + new Waypoint { PosX = x0, PosY = y, PosZ = z0 }, + new Waypoint { PosX = x1, PosY = y, PosZ = z1 }, + }; + + private static List LShape() + => new() + { + new Waypoint { PosX = 0, PosY = 500, PosZ = 0 }, // 起点 + new Waypoint { PosX = 100, PosY = 500, PosZ = 0 }, // 拐点(X 段 100m) + new Waypoint { PosX = 100, PosY = 500, PosZ = 100 }, // 终点(Z 段 100m) + }; + + // ═══════════════════════════════════════ + // TotalLength + // ═══════════════════════════════════════ + + [Fact] + public void TotalLength_StraightLine() + { + var wps = Line(0, 0, 3000, 4000); // 3-4-5 → 5000m + Assert.Equal(5000f, RouteGeometry.TotalLength(wps), 1); + } + + [Fact] + public void TotalLength_LShape() + { + var wps = LShape(); // 100 + 100 = 200m + Assert.Equal(200f, RouteGeometry.TotalLength(wps), 1); + } + + [Fact] + public void TotalLength_SingleWaypoint_Zero() + { + var wps = new List { new Waypoint { PosX = 5, PosY = 5, PosZ = 5 } }; + Assert.Equal(0f, RouteGeometry.TotalLength(wps)); + } + + // ═══════════════════════════════════════ + // PositionAt + // ═══════════════════════════════════════ + + [Fact] + public void PositionAt_Start_ReturnsFirstWaypoint() + { + var wps = Line(0, 0, 1000, 0); + var (x, y, z) = RouteGeometry.PositionAt(wps, 0f); + Assert.Equal(0f, x, 1); + Assert.Equal(0f, z, 1); + } + + [Fact] + public void PositionAt_Midpoint_LinearInterp() + { + var wps = Line(0, 0, 1000, 0); + var (x, _, _) = RouteGeometry.PositionAt(wps, 500f); // 中点 + Assert.Equal(500f, x, 1); + } + + [Fact] + public void PositionAt_LShape_Corner() + { + var wps = LShape(); // 0→(100,0)→(100,100) + // 弧长 100 = 拐点 + var (x, _, z) = RouteGeometry.PositionAt(wps, 100f); + Assert.Equal(100f, x, 1); + Assert.Equal(0f, z, 1); + // 弧长 150 = Z 段中点 + var (x2, _, z2) = RouteGeometry.PositionAt(wps, 150f); + Assert.Equal(100f, x2, 1); + Assert.Equal(50f, z2, 1); + } + + [Fact] + public void PositionAt_BeyondEnd_ReturnsLastWaypoint() + { + var wps = Line(0, 0, 1000, 0); + var (x, _, _) = RouteGeometry.PositionAt(wps, 99999f); + Assert.Equal(1000f, x, 1); + } + + // ═══════════════════════════════════════ + // ArcLengthNearestTo + // ═══════════════════════════════════════ + + [Fact] + public void ArcLengthNearestTo_PointOnLine() + { + var wps = Line(0, 0, 1000, 0); // X 轴上 + // 点 (300, 0) 在航路上,弧长应=300 + Assert.Equal(300f, RouteGeometry.ArcLengthNearestTo(wps, 300f, 0f), 1); + } + + [Fact] + public void ArcLengthNearestTo_PointOffLine_Projects() + { + var wps = Line(0, 0, 1000, 0); // X 轴 + // 点 (300, 50) 投影回 X 轴弧长=300 + Assert.Equal(300f, RouteGeometry.ArcLengthNearestTo(wps, 300f, 50f), 1); + } + + [Fact] + public void ArcLengthNearestTo_LShape_CorrectSegment() + { + var wps = LShape(); // 0→(100,0)→(100,100) + // 点 (50, 0) 在第一段,弧长=50 + Assert.Equal(50f, RouteGeometry.ArcLengthNearestTo(wps, 50f, 0f), 1); + // 点 (100, 50) 在第二段(X 段 100 + Z 段 50),弧长=150 + Assert.Equal(150f, RouteGeometry.ArcLengthNearestTo(wps, 100f, 50f), 1); + } + + // ═══════════════════════════════════════ + // TravelTimeTo + // ═══════════════════════════════════════ + + [Fact] + public void TravelTimeTo_UniformSpeed() + { + // 弧长 1000m,速度 36km/h=10m/s → 100s + Assert.Equal(100f, RouteGeometry.TravelTimeTo(Line(0, 0, 1000, 0), 1000f, 36f), 1); + } + + [Fact] + public void TravelTimeTo_ZeroSpeed_Throws() + { + Assert.Throws( + () => RouteGeometry.TravelTimeTo(Line(0, 0, 1000, 0), 1000f, 0f)); + } + + // ═══════════════════════════════════════ + // TangentAt + // ═══════════════════════════════════════ + + [Fact] + public void TangentAt_XDirection_IsUnitX() + { + var wps = Line(0, 0, 1000, 0); // +X 方向 + var (dx, dz) = RouteGeometry.TangentAt(wps, 500f); + Assert.Equal(1f, dx, 3); + Assert.Equal(0f, dz, 3); + } + + [Fact] + public void TangentAt_ZDirection_IsUnitZ() + { + var wps = Line(0, 0, 0, 1000); // +Z 方向 + var (dx, dz) = RouteGeometry.TangentAt(wps, 500f); + Assert.Equal(0f, dx, 3); + Assert.Equal(1f, dz, 3); + } + + [Fact] + public void TangentAt_LShape_CornerTransition() + { + var wps = LShape(); + // 第一段(弧长 50):+X + var (dx1, dz1) = RouteGeometry.TangentAt(wps, 50f); + Assert.Equal(1f, dx1, 3); + Assert.Equal(0f, dz1, 3); + // 第二段(弧长 150):+Z + var (dx2, dz2) = RouteGeometry.TangentAt(wps, 150f); + Assert.Equal(0f, dx2, 3); + Assert.Equal(1f, dz2, 3); + } + + [Fact] + public void TangentAt_AlwaysUnitLength() + { + // 任意方向直线,切向量必须是单位向量 + var wps = Line(0, 0, 3000, 4000); // 3-4-5 方向 + var (dx, dz) = RouteGeometry.TangentAt(wps, 100f); + float mag = (float)System.Math.Sqrt(dx * dx + dz * dz); + Assert.Equal(1f, mag, 3); + } + } +} diff --git a/test/unit/CounterDrone.Core.Tests/SimulationEngineTests.cs b/test/unit/CounterDrone.Core.Tests/SimulationEngineTests.cs index 3047d25..66db8b5 100644 --- a/test/unit/CounterDrone.Core.Tests/SimulationEngineTests.cs +++ b/test/unit/CounterDrone.Core.Tests/SimulationEngineTests.cs @@ -36,7 +36,7 @@ namespace CounterDrone.Core.Tests new RoutePlanRepository(_mainDb), new WaypointRepository(_mainDb)); _engine = new SimulationEngine(_scenarioService, new FrameDataStore(paths), - new DamageModelRouter(), paths, new DefaultDefensePlanner(DefaultAmmunition.GetAll())); + new DamageModelRouter(), paths, new DefaultDefensePlanner(DefaultAmmunition.GetAll(), TestPlannerConfig.Instance)); } public void Dispose() diff --git a/test/unit/CounterDrone.Core.Tests/TestPlannerConfig.cs b/test/unit/CounterDrone.Core.Tests/TestPlannerConfig.cs new file mode 100644 index 0000000..31352ba --- /dev/null +++ b/test/unit/CounterDrone.Core.Tests/TestPlannerConfig.cs @@ -0,0 +1,21 @@ +using System; +using System.IO; +using CounterDrone.Core.Algorithms; + +namespace CounterDrone.Core.Tests +{ + ///

测试用 PlannerConfig 加载辅助。 + /// 从仓库 data/planner_config.json 加载真实配置,确保测试与生产配置一致。 + public static class TestPlannerConfig + { + private static readonly Lazy _instance = new(() => + { + // bin\Debug\net10.0 → 上 6 层到仓库根,再进 data 目录 + string baseDir = AppDomain.CurrentDomain.BaseDirectory; + string repoRoot = Path.GetFullPath(Path.Combine(baseDir, "..", "..", "..", "..", "..", "..")); + return PlannerConfig.Load(Path.Combine(repoRoot, "data")); + }); + + public static PlannerConfig Instance => _instance.Value; + } +}