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.
123 lines
5.5 KiB
C#
123 lines
5.5 KiB
C#
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}");
|
||
}
|
||
}
|
||
}
|