CounterDroneBackend/test/unit/CounterDrone.Core.Tests/DetectionCalculatorTests.cs
tian d8470bad30 Phase 10: 探测实时链路开发 + planner 诊断 + 硬编码消除
- 3D球冠探测: IsInCoverage, DetectionEntity, Tick 第5步扫描
- 探测融合: EarliestDetection 采样法, break 修复
- planner 诊断: HasInterceptWindow 拦截窗口检查
- TryGenerateFireEvents 返回拒绝原因
- Summary 含失败原因+建议值(探测范围/弧长)
- PlanningFailed 事件: 引擎在规划失败时发出事件
- 硬编码消除: Phase2Duration→AmmunitionSpec
  ExpansionFactor/TimingSafetyMargin→PlannerConfig
  速度从 waypoint.Speed 读取(不用 TypicalSpeed)
- TestData 改为从 seeded 数据库读取(不再内嵌 JSON)
- 默认数据加 3D球冠参数,巡航导弹速度300→200
  空基航路10km→20km, 位置调整
- 222 测试全通过
2026-06-16 14:10:23 +08:00

265 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
using Xunit;
namespace CounterDrone.Core.Tests
{
public class DetectionCalculatorTests
{
private static List<Waypoint> Line(float x0, float z0, float x1, float z1)
=> new()
{
new Waypoint { PosX = x0, PosY = 500, PosZ = z0 },
new Waypoint { PosX = x1, PosY = 500, PosZ = z1 },
};
// ═══════════════════════════════════════
// EffectiveRange — 天气衰减
// ═══════════════════════════════════════
[Fact]
public void EffectiveRange_EO_VisibilityBelowBase_ScalesDown()
{
// 光电基准 10000m能见度 5000m → 有效 = 10000 × (5000/10000) = 5000
float r = DetectionCalculator.EffectiveRange(0, 10000, 0, 5000);
Assert.Equal(5000f, r, 0);
}
[Fact]
public void EffectiveRange_EO_VisibilityAboveBase_NoEffect()
{
// 光电基准 10000m能见度 15000m → 有效 = 10000min(1, 1.5)=1
float r = DetectionCalculator.EffectiveRange(0, 10000, 0, 15000);
Assert.Equal(10000f, r, 0);
}
[Fact]
public void EffectiveRange_Radar_UnaffectedByVisibility()
{
// 雷达 15000m能见度只有 1000m雷达不受影响
float r = DetectionCalculator.EffectiveRange(15000, 0, 0, 1000);
Assert.Equal(15000f, r, 0);
}
[Fact]
public void EffectiveRange_IR_UnaffectedByVisibility()
{
float r = DetectionCalculator.EffectiveRange(0, 0, 8000, 500);
Assert.Equal(8000f, r, 0);
}
[Fact]
public void EffectiveRange_TakesMaxOfMethods()
{
// 雷达 12000 + 光电 6000(能见度 3000 → 3000) + 红外 8000 → max=12000
float r = DetectionCalculator.EffectiveRange(12000, 6000, 8000, 3000);
Assert.Equal(12000f, r, 0);
}
[Fact]
public void EffectiveRange_EO_StrongestWhenGoodWeather()
{
// 雷达 8000 + 光电 15000(能见度 15000) + 红外 10000 → max=15000光电胜出
float r = DetectionCalculator.EffectiveRange(8000, 15000, 10000, 15000);
Assert.Equal(15000f, r, 0);
}
// ═══════════════════════════════════════
// EarliestDetection — 最早探测点
// ═══════════════════════════════════════
[Fact]
public void EarliestDetection_DroneEntersCircle_ReturnsEntryArc()
{
// 航路 X:0→10000 Z:0探测源在 X=7000半径 5000
// 探测圆边界在 X=2000 和 X=12000无人机从 X=0 飞向 10000在 X=2000 进入
// 弧长 = 2000
var route = Line(0, 0, 10000, 0);
var src = new DetectionSource
{
Position = new Vector3(7000, 0, 0),
RadarRange = 5000, Accuracy = 100,
};
var (arc, acc) = DetectionCalculator.EarliestDetection(route, new() { src }, 10000);
Assert.InRange(arc, 1950f, 2050f); // X=2000 进入圆
Assert.Equal(100f, acc, 0);
}
[Fact]
public void EarliestDetection_DroneStartsInside_ReturnsZero()
{
// 航路起点已在探测圆内
var route = Line(6000, 0, 10000, 0);
var src = new DetectionSource
{
Position = new Vector3(5000, 0, 0),
RadarRange = 5000, Accuracy = 50,
};
var (arc, _) = DetectionCalculator.EarliestDetection(route, new() { src }, 10000);
Assert.Equal(0f, arc, 0);
}
[Fact]
public void EarliestDetection_NeverEnters_ReturnsMaxValue()
{
// 航路离探测源很远
var route = Line(0, 50000, 10000, 50000);
var src = new DetectionSource
{
Position = new Vector3(5000, 0, 0),
RadarRange = 1000,
};
var (arc, _) = DetectionCalculator.EarliestDetection(route, new() { src }, 10000);
Assert.Equal(float.MaxValue, arc);
}
[Fact]
public void EarliestDetection_MultipleSources_TakesEarliest()
{
// 两个探测源源A 探测圆边界 X=8000源B 边界 X=3000
// 无人机从 X=0 飞,最早在 X=3000 被源B 发现
var route = Line(0, 0, 20000, 0);
var sources = new List<DetectionSource>
{
new() { Position = new Vector3(12000, 0, 0), RadarRange = 4000, Accuracy = 200 }, // 边界 X=8000
new() { Position = new Vector3(7000, 0, 0), RadarRange = 4000, Accuracy = 80 }, // 边界 X=3000
};
var (arc, acc) = DetectionCalculator.EarliestDetection(route, sources, 10000);
Assert.InRange(arc, 2950f, 3050f);
Assert.Equal(80f, acc, 0); // 用源B的精度
}
[Fact]
public void EarliestDetection_NoSources_ReturnsZeroArc()
{
// 无探测设备,弧长=0上帝视角从起点算
var route = Line(0, 0, 10000, 0);
var (arc, acc) = DetectionCalculator.EarliestDetection(route, new List<DetectionSource>(), 10000);
Assert.Equal(0f, arc, 0);
Assert.Equal(float.MaxValue, acc); // 无精度信息
}
[Fact]
public void EarliestDetection_EO_WeatherShortensDetection()
{
// 光电 10000m能见度 4000 → 有效 4000m
// 探测源在 X=9000有效圆边界 X=5000 和 X=13000
// 无人机从 X=0 飞,在 X=5000 进入
var route = Line(0, 0, 15000, 0);
var src = new DetectionSource
{
Position = new Vector3(9000, 0, 0),
EORange = 10000, Accuracy = 100,
};
var (arc, _) = DetectionCalculator.EarliestDetection(route, new() { src }, 4000);
Assert.InRange(arc, 4950f, 5050f);
}
// ═══════════════════════════════════════
// SpreadRadius — 精度换算散布
// ═══════════════════════════════════════
[Fact]
public void SpreadRadius_EqualsAccuracy()
{
Assert.Equal(100f, DetectionCalculator.SpreadRadius(100f), 0);
Assert.Equal(0f, DetectionCalculator.SpreadRadius(0f), 0);
}
// ═══════════════════════════════════════
// IsInCoverage — 3D 球冠判定
// ═══════════════════════════════════════
[Fact]
public void IsInCoverage_Inside3DCap_ReturnsTrue()
{
// 设备在 (0,0,0),目标在 (3000, 500, 4000) → 水平距离5000高度差500
// 俯仰角 ≈ atan2(500, 5000) ≈ 5.7°
// 无限制球冠 → 通过
var pos = new Vector3(3000, 500, 4000);
var det = new Vector3(0, 0, 0);
var result = DetectionCalculator.IsInCoverage(pos, det, 6000f,
float.MaxValue, float.MaxValue, float.MaxValue, float.MaxValue);
Assert.True(result);
}
[Fact]
public void IsInCoverage_HorizontalTooFar_ReturnsFalse()
{
// 水平距离 8000 > 有效范围 5000
var pos = new Vector3(6000, 500, 6000);
var det = new Vector3(0, 0, 0);
var result = DetectionCalculator.IsInCoverage(pos, det, 5000f,
float.MaxValue, float.MaxValue, float.MaxValue, float.MaxValue);
Assert.False(result);
}
[Fact]
public void IsInCoverage_BelowMinDetectAlt_ReturnsFalse()
{
// 高度门限 200-1000目标在 100m → 被拒绝
var pos = new Vector3(3000, 100, 4000);
var det = new Vector3(0, 0, 0);
var result = DetectionCalculator.IsInCoverage(pos, det, 6000f,
float.MaxValue, float.MaxValue, 200f, 1000f);
Assert.False(result);
}
[Fact]
public void IsInCoverage_AboveMaxDetectAlt_ReturnsFalse()
{
// 高度门限 200-1000目标在 1200m → 被拒绝
var pos = new Vector3(3000, 1200, 4000);
var det = new Vector3(0, 0, 0);
var result = DetectionCalculator.IsInCoverage(pos, det, 6000f,
float.MaxValue, float.MaxValue, 200f, 1000f);
Assert.False(result);
}
[Fact]
public void IsInCoverage_ElevationTooLow_ReturnsFalse()
{
// 俯仰角限 -2° 到 30°目标几乎在正下方(-89°) → 被拒绝
var pos = new Vector3(1, -5000, 1);
var det = new Vector3(0, 0, 0);
var result = DetectionCalculator.IsInCoverage(pos, det, 6000f,
-2f, 30f, float.MaxValue, float.MaxValue);
Assert.False(result);
}
[Fact]
public void IsInCoverage_ElevationTooHigh_ReturnsFalse()
{
// 俯仰角限 -2° 到 30°目标仰角 ~45° → 被拒绝
var pos = new Vector3(3000, 3000, 4000);
var det = new Vector3(0, 0, 0);
var result = DetectionCalculator.IsInCoverage(pos, det, 6000f,
-2f, 30f, float.MaxValue, float.MaxValue);
Assert.False(result);
}
[Fact]
public void IsInCoverage_DirectlyAbove_WithElevationLimit_ReturnsTrue()
{
// 正顶horizDist≈0即使有俯仰角限制也应通过物理上正上方肯定可见
var pos = new Vector3(0, 1000, 0);
var det = new Vector3(0, 0, 0);
var result = DetectionCalculator.IsInCoverage(pos, det, 2000f,
-2f, 30f, float.MaxValue, float.MaxValue);
Assert.True(result);
}
[Fact]
public void IsInCoverage_UnlimitedCaps_EquivalentTo2DCircle()
{
// 所有限制为 MaxValue无限制等价于 2D 圆
var pos = new Vector3(3000, float.MaxValue / 2, 4000);
var det = new Vector3(0, 0, 0);
var result = DetectionCalculator.IsInCoverage(pos, det, 6000f,
float.MaxValue, float.MaxValue, float.MaxValue, float.MaxValue);
Assert.True(result);
}
}
}