using CounterDrone.Core.Algorithms; using CounterDrone.Core.Models; using Xunit; namespace CounterDrone.Core.Tests { public class DispersionModelTests { private static AmmunitionSpec Ammo() => TestData.Defaults.Ammunition.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.Defaults.Ammunition.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}"); } } }