using System.Collections.Generic; using CounterDrone.Core.Algorithms; using CounterDrone.Core.Models; using Xunit; namespace CounterDrone.Core.Tests { public class DetectionCalculatorTests { private static List 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 { 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(), 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); } } }