CounterDroneBackend/test/unit/CounterDrone.Core.Tests/DispersionModelTests.cs
tian 8515f7821a feat(v0.5.0): weather into planner + unified physics model
Planner no longer writes local physics formulas. All kinematics/geometry/damage go through shared utility classes (Kinematics/RouteGeometry/CloudExpansionModel/DamageAssessment).

Core: RouteGeometry (route geometry), PlannerConfig + planner_config.json (config externalization). Planner route-aware layout (offset along tangent), cloud overlap 20pct. DroneEntity arc-length driven.

Fixes: PathInSphere cloud-frame correction (root cause), GaussianPuffDispersion hardcoded Sunny, ComputeEffectiveRadius cloud age, planner wind offset, remove drone wind bias.

Tests 167 to 191, 41s. Windy scenarios pass.
2026-06-14 14:49:53 +08:00

123 lines
5.5 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() => DefaultAmmunition.GetByType(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 = Ammo();
a.MaxDuration = 2;
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 = DefaultAmmunition.GetByType(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}");
}
}
}