CounterDroneBackend/test/unit/CounterDrone.Core.Tests/DispersionModelTests.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

129 lines
5.9 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 CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
using Xunit;
namespace CounterDrone.Core.Tests
{
public class DispersionModelTests
{
private static AmmunitionSpec Ammo() => TestData.Ammo.First(a => a.AerosolType == (int)AerosolType.InertGas);
[Fact]
public void Phase1_InitialRadius_FromBurstCharge()
{
var m = new GaussianPuffDispersion();
m.Initialize(Ammo(), new CombatScene { WindSpeed = 0 }, new Algorithms.Vector3(0, 0, 0), 0f);
// R₀ = 3.3 × 0.3^0.32 ≈ 2.2m
Assert.True(m.Radius > 1.5f && m.Radius < 5f, $"R₀={m.Radius:F2}");
}
[Fact]
public void Phase2_Radius_GrowsWithSqrtT()
{
var m = new GaussianPuffDispersion();
m.Initialize(Ammo(), new CombatScene { WindSpeed = 0 }, new Algorithms.Vector3(0, 0, 0), 0f);
var r1 = m.Radius;
m.Tick(9f, 0f, WindDirection.N);
var r2 = m.Radius;
m.Tick(7f, 0f, WindDirection.N); // total 16s
var r3 = m.Radius;
// 0→9s: Δ = k×3 = 15; 9→16s: Δ = k×(4-3) = 5
Assert.True(r3 > r2 && r2 > r1, "半径应单调递增");
}
[Fact]
public void Phase2_Density_DropsWithVolume()
{
var m = new GaussianPuffDispersion();
m.Initialize(Ammo(), new CombatScene { WindSpeed = 0 }, new Algorithms.Vector3(0, 0, 0), 0f);
var d1 = m.CoreDensity;
m.Tick(10f, 0f, WindDirection.N);
Assert.True(m.CoreDensity < d1, "膨胀后密度应下降");
}
[Fact]
public void Phase3_SwitchesAfter30s()
{
var m = new GaussianPuffDispersion();
m.Initialize(Ammo(), new CombatScene { WindSpeed = 0 }, new Algorithms.Vector3(0, 0, 0), 0f);
m.Tick(35f, 0f, WindDirection.N);
Assert.False(m.IsDissipated, "35s不应消散");
}
[Fact]
public void Dissipates_AfterMaxDuration()
{
var m = new GaussianPuffDispersion();
var a = new AmmunitionSpec
{
Id = "test", AerosolType = (int)AerosolType.InertGas,
InitialRadius = 3.8, CoreDensity = 1.5, EdgeDensity = 0.1,
InitialTemperature = 1800, BuoyancyFactor = 0.3,
EffectiveConcentration = 0.0001, MaxRadius = 100, MaxDuration = 2,
SourceStrength = 10, BurstChargeKg = 1.5, TurbulentExpansionK = 3,
};
m.Initialize(a, new CombatScene { WindSpeed = 0 }, new Algorithms.Vector3(0, 0, 0), 0f);
m.Tick(3f, 0f, WindDirection.N);
Assert.True(m.IsDissipated);
}
[Fact]
public void DefaultAmmo_ParametersInReasonableRange()
{
var inert = TestData.Ammo.First(a => a.AerosolType == (int)AerosolType.InertGas);
Assert.True(inert.BurstChargeKg is > 0.01 and < 5, "爆发药应 0.01~5kg");
Assert.True(inert.TurbulentExpansionK is > 1 and < 20, "湍流系数应 1~20");
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}");
}
}
}