- 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 测试全通过
265 lines
11 KiB
C#
265 lines
11 KiB
C#
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 → 有效 = 10000(min(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);
|
||
}
|
||
}
|
||
}
|