默认弹药参数校准:爆发药1.5kg(发烟罐典型值) + 三阶段扩散模型 + 集成测试流程验证, 129测试通过

This commit is contained in:
tian 2026-06-12 10:47:54 +08:00
parent 8b71351a7b
commit c060030e0a
20 changed files with 436 additions and 248 deletions

View File

@ -783,66 +783,106 @@ public class AmmunitionSpec
> 第三方以 JSON 配置文件形式交付 `AmmunitionSpec` 数组,系统启动时加载到 SQLite `AmmunitionSpec` 表。
### 6.3 扩散模型设计 — 高斯烟团模型Gaussian Puff
### 6.2.1 系统内置默认值(第三方提供前使用)
基于 155mm 炮弹载荷的物理合理估计:
| 参数 | 惰性气体弹 | 活性材料弹 | 活性燃料弹 |
|------|------|------|------|
| 药剂质量 (SourceStrength) | 10 kg | 12 kg | 10 kg |
| 爆发药 TNT 当量 (BurstChargeKg) | 1.5 kg | 4.0 kg | 1.5 kg |
| 有效浓度阈值 (EffectiveConcentration) | 0.0001 kg/m³ | 0.0002 kg/m³ | 0.0001 kg/m³ |
| 湍流扩散系数 (TurbulentExpansionK) | 3.0 | 4.0 | 3.0 |
| 最大半径 (MaxRadius) | 100 m | 80 m | 90 m |
| 最大持续时间 (MaxDuration) | 120 s | 90 s | 100 s |
| Phase 1 初始半径 (R₀) | 3.75 m | 5.14 m | 3.75 m |
> **数据来源**10kg烟幕剂抛射药/烟幕剂质量比 5%-20%TNT当量取 1.5kg(典型值)。活性材料弹取爆炸分散型上限 4kg。
### 6.3 扩散模型设计 — 高斯烟团模型Gaussian PuffV2
#### 6.3.1 模型选择
选用**高斯烟团扩散模型**Gaussian Puff Model理由
- 计算简单,适合实时 20Hz Tick
- 能反映风速、大气稳定度对扩散的影响
- 工业级大气扩散的经典方法
采用**高斯烟团扩散模型**,使用**Pasquill-Gifford 稳定度分类**计算扩散系数 σ。
#### 6.3.2 核心公式
#### 6.3.2 大气稳定度映射
从天气类型和风速推导稳定度等级,无需额外参数:
| 天气 | 风速 | 稳定度 | 太阳辐射 | 湍流 |
|------|------|------|------|------|
| 晴天 | ≤ 5 m/s | A极不稳定 | 强 | 强 |
| 晴天 | > 5 m/s | B不稳定 | 中 | 中 |
| 阴天 | 任意 | D中性 | 弱 | 中 |
| 雨天 | 任意 | D中性 | 弱 | 中 |
| 雾天 | 任意 | E稳定 | 无 | 弱 |
| 夜间 | 任意 | F极稳定 | 无 | 最弱 |
#### 6.3.3 σ 扩散系数Pasquill-Gifford
扩散距离 x 米后,水平扩散系数 σ_y、垂直扩散系数 σ_z单位 m
| 稳定度 | σ_y | σ_z |
|------|------|------|
| A | 0.22x / (1+0.0001x)^0.5 | 0.20x |
| B | 0.16x / (1+0.0001x)^0.5 | 0.12x |
| C | 0.11x / (1+0.0001x)^0.5 | 0.08x / (1+0.0002x)^0.5 |
| D | 0.08x / (1+0.0001x)^0.5 | 0.06x / (1+0.0015x)^0.5 |
| E | 0.06x / (1+0.0001x)^0.5 | 0.03x / (1+0.0003x)^0.5 |
| F | 0.04x / (1+0.0001x)^0.5 | 0.016x / (1+0.0003x)^0.5 |
> 对于瞬时释放的烟团Puff扩散距离 x 取云团中心随风漂移的距离x = u × t其中 u 为风速t 为经过时间。若风速为 0取 x ≈ 1m纯湍流扩散
#### 6.3.4 浓度场公式
云团内任意点 (x, y, z) 在时刻 t 的浓度:
```
C(x, y, z, t) = ────────────── × exp[ -½( (x-ut)/σx )² ]
(2π)^(3/2) σx σy σz
C(x, y, z, t) = ────────────── × exp[ -½( (x-ut)/σy )² ]
(2π)^(3/2) σy σy σz
× exp[ -½( y/σy )² ]
× { exp[ -½( (z-H)/σz )² ] + exp[ -½( (z+H)/σz )² ] }
其中:
Q = 源强(释放总量)
Q = 源强(释放物质总量kg,来自 AmmunitionSpec
u = 风速m/s
H = 有效释放高度m
σx, σy, σz = 三轴扩散系数,随时间增大
σy, σz = 扩散系数(由 6.3.2 和 6.3.3 确定)
```
#### 6.3.3 简化方案
#### 6.3.5 可判定指标
完整三维浓度场计算量大。实时仿真中采用**简化方案**
| 指标 | 计算 | 说明 |
|------|------|------|
| 中心浓度 | C_max = Q / [(2π)^(3/2) σσz] | 云团最浓处 |
| 有效半径 | R_eff = min{ r | C(中心, r) < C_threshold } | 浓度超过阈值的范围 |
| 消散条件 | C_max < C_threshold / 10 经过时间 > MaxDuration | 浓度可忽略 |
```
简化假设:
- 云团近似为球体σx = σy = σz = σ
- 半径 R(t) = InitialRadius + DispersionRate × t
- DispersionRate = DispersionRateBase × f(风速, 大气稳定度)
- 中心随风速漂移Center(t) = Center(0) + WindVector × t
- 密度随半径衰减Density(t) = CoreDensity × (InitialRadius / R(t))³
> **简化**:实时判定时,无人机位于云团内当且仅当 C(无人机坐标) ≥ EffectiveConcentration。不需要球体假设直接用高斯公式计算。
简化判定:
- 无人机进入球体 → 暴露于云团
- 密度 ≥ EffectiveConcentration → 有效毁伤
- 半径 ≥ MaxRadius OR 时间 ≥ MaxDuration → 消散
```
#### 6.3.6 AmmunitionSpec 调整
#### 6.3.4 接口
| 字段 | 变更 | 说明 |
|------|------|------|
| `DispersionRateBase` | ❌ 删除 | 由 PG 稳定度代替 |
| `InitialRadius` | 保留 | 仍作初始 σ 参考 |
| `SourceStrength` | ✅ 新增 | 源强 Qkg取代线性模型 |
> 浓度 C 的单位为 kg/m³无人机是否受损取决于 C ≥ EffectiveConcentration。
#### 6.3.7 接口(不变)
```csharp
public interface ICloudDispersionModel
{
void Initialize(AmmunitionSpec ammo, CombatScene env, Vector3 releasePos, float releaseTime);
void Tick(float deltaTime, float windSpeed, WindDirection windDir);
Vector3 Center { get; } // 当前中心坐标(随风漂移)
float Radius { get; } // 当前半径m
float CoreDensity { get; } // 当前核心密度
float EffectiveRadius { get; } // 密度 ≥ 有效浓度的半径
ParticleParams Particles { get; } // Unity 粒子参数
bool IsDissipated { get; } // 是否已消散
Vector3 Center { get; }
float Radius { get; }
float CoreDensity { get; }
float EffectiveRadius { get; }
ParticleParams Particles { get; }
bool IsDissipated { get; }
}
```

View File

@ -6,7 +6,7 @@ namespace CounterDrone.Core.Algorithms
/// <summary>吸入式爆炸 — 指数累积型:暴露时间越长伤害越高</summary>
public class ActiveFuelDamageModel : IDamageModel
{
private const float EffectiveThreshold = 0.03f; // 有效浓度阈值
private const float EffectiveThreshold = 0.001f; // 有效浓度阈值
private const float BaseRate = 0.02f; // 基础速率
private const float ExpFactor = 0.3f; // 指数增长因子

View File

@ -6,7 +6,7 @@ namespace CounterDrone.Core.Algorithms
/// <summary>爆燃式 — 触发型:双条件满足后瞬间高伤害</summary>
public class ActiveMaterialDamageModel : IDamageModel
{
private const float TriggerThreshold = 0.1f; // 触发浓度阈值
private const float TriggerThreshold = 0.0002f; // 触发浓度阈值
private const float BurstDamage = 0.85f; // 一次爆发伤害
private const float ResidualRate = 0.05f; // 后续余伤速率
private bool _triggered;

View File

@ -29,6 +29,11 @@ namespace CounterDrone.Core.Algorithms
/// <summary>多轮次云团列表(含主云团)</summary>
public List<CloudDispersal> CloudSalvo { get; set; } = new();
/// <summary>齐射参数:弹药数,由算法填充</summary>
public int SalvoRounds { get; set; } = 1;
/// <summary>齐射参数云团间隔m由算法填充</summary>
public float SalvoSpacing { get; set; }
public List<RecommendedPlatform> Platforms { get; set; } = new();
public List<RecommendedDetection> Detections { get; set; } = new();

View File

@ -0,0 +1,59 @@
using System.Collections.Generic;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>默认弹药规格 — 基于发烟罐/烟幕弹工程数据</summary>
/// <remarks>
/// 参考10kg烟幕剂抛射药/烟幕剂质量比 5%-20%TNT当量 1.5-2kg典型值
/// 初始半径 R₀ = 3.3 × W^0.32 ≈ 3.8~4.1m
/// </remarks>
public static class DefaultAmmunition
{
public static List<AmmunitionSpec> GetAll()
{
return new List<AmmunitionSpec>
{
new AmmunitionSpec
{
Id = "default-inert", AerosolType = (int)AerosolType.InertGas,
Name = "惰性气体弹(发烟罐型)",
InitialRadius = 3.8,
CoreDensity = 1.5, EdgeDensity = 0.1,
InitialTemperature = 1800, BuoyancyFactor = 0.3,
EffectiveConcentration = 0.0001,
MaxRadius = 100.0, MaxDuration = 120.0,
SourceStrength = 10.0,
BurstChargeKg = 1.5, TurbulentExpansionK = 3.0,
},
new AmmunitionSpec
{
Id = "default-active", AerosolType = (int)AerosolType.ActiveMaterial,
Name = "活性材料弹(爆炸分散型)",
InitialRadius = 5.0,
CoreDensity = 2.0, EdgeDensity = 0.2,
InitialTemperature = 2400, BuoyancyFactor = 0.6,
EffectiveConcentration = 0.0002,
MaxRadius = 80.0, MaxDuration = 90.0,
SourceStrength = 12.0,
BurstChargeKg = 4.0, TurbulentExpansionK = 4.0,
},
new AmmunitionSpec
{
Id = "default-fuel", AerosolType = (int)AerosolType.ActiveFuel,
Name = "活性燃料弹(抛射分散型)",
InitialRadius = 3.8,
CoreDensity = 1.8, EdgeDensity = 0.15,
InitialTemperature = 1900, BuoyancyFactor = 0.4,
EffectiveConcentration = 0.0001,
MaxRadius = 90.0, MaxDuration = 100.0,
SourceStrength = 10.0,
BurstChargeKg = 1.5, TurbulentExpansionK = 3.0,
},
};
}
public static AmmunitionSpec GetByType(AerosolType type)
=> GetAll().Find(a => a.AerosolType == (int)type) ?? GetAll()[0];
}
}

View File

@ -83,26 +83,28 @@ namespace CounterDrone.Core.Algorithms
return result;
}
// 弹药参数
var initialR = (float)ammo.InitialRadius;
// 弹药参数Phase 1 爆轰 + Phase 2 膨胀后的有效半径
var r0 = 3.3f * (float)Math.Pow(Math.Max(0.01, (float)ammo.BurstChargeKg), 0.32);
var k = (float)ammo.TurbulentExpansionK;
var maxDur = (float)ammo.MaxDuration;
// 无人机到达中点时云团已膨胀了 halfTime 秒
var halfTime = totalFlightTime / 2f;
var effectiveR = r0 + k * (float)Math.Sqrt(Math.Min(halfTime, 30f)); // 不超过 Phase 2
if (halfTime > 30f) effectiveR += k * (float)Math.Sqrt(halfTime - 30f) * 0.3f; // Phase 3 增速变慢
// 无人机穿过单个云团的时间
var crossTime = (2f * initialR) / avgSpeed;
var crossTime = (2f * effectiveR) / avgSpeed;
var neededExposure = aerosolType == AerosolType.ActiveMaterial ? 2f : 6f;
var spacing = initialR * 1.5f; // 云团间隔
var spacing = effectiveR * 1.5f;
// 有效覆盖距离 = 间距×(N-1) + 直径,需 ≥ 所需暴露时间 × 速度
var requiredCoverage = neededExposure * avgSpeed;
var roundsNeeded = Math.Max(1,
(int)Math.Ceiling((requiredCoverage - 2f * initialR) / spacing) + 1);
// 实际有效暴露时间
var actualCoverage = spacing * (roundsNeeded - 1) + 2f * initialR;
(int)Math.Ceiling((requiredCoverage - 2f * effectiveR) / spacing) + 1);
var actualCoverage = spacing * (roundsNeeded - 1) + 2f * effectiveR;
var actualExposure = actualCoverage / avgSpeed;
var prob = Math.Min(0.95f, actualExposure / neededExposure);
var expansionTime = (float)Math.Pow((effectiveR - r0) / k, 2);
var recommendedTiming = halfTime - expansionTime;
// 多轮次云团:沿航路等距分布
var cloudSalvo = new List<CloudDispersal>();
for (int i = 0; i < roundsNeeded; i++)
{
@ -120,7 +122,7 @@ namespace CounterDrone.Core.Algorithms
ReleaseMode = (int)ReleaseMode.Single,
Source = "Algorithm",
PositionMode = (int)PositionMode.AlgorithmRecommended,
RecommendedTiming = totalFlightTime / 2f,
RecommendedTiming = recommendedTiming,
});
}
@ -146,6 +148,8 @@ namespace CounterDrone.Core.Algorithms
AerosolRationale = rationale,
RecommendedCloud = cloudSalvo[0],
CloudSalvo = cloudSalvo,
SalvoRounds = roundsNeeded,
SalvoSpacing = spacing,
Platforms = platforms,
Detections = new List<RecommendedDetection>
{
@ -173,7 +177,7 @@ namespace CounterDrone.Core.Algorithms
Duration = (int)maxDur,
Source = "Algorithm",
PositionMode = (int)PositionMode.AlgorithmRecommended,
RecommendedTiming = totalFlightTime * 0.25f,
RecommendedTiming = totalFlightTime * 0.25f - expansionTime,
},
Platforms = platforms,
InterceptProbability = prob * 0.3f,

View File

@ -3,19 +3,26 @@ using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>高斯烟团扩散模型 — 简化实现</summary>
/// <summary>三阶段简化扩散模型</summary>
/// <remarks>
/// Phase 1 (0时刻): R₀ = 3.3 × BurstChargeKg^0.32
/// Phase 2 (0~30s): R(t) = R₀ + TurbulentExpansionK × √t
/// Phase 3 (30s~): 高斯烟团 (Pasquill-Gifford)
/// </remarks>
public class GaussianPuffDispersion : ICloudDispersionModel
{
private AmmunitionSpec _ammo = null!;
private float _elapsed;
private float _currentRadius;
private float _currentDensity;
private Vector3 _center;
private Vector3 _windVelocity;
private bool _inPhase3;
public Vector3 Center => _center;
public float Radius => _currentRadius;
public float CoreDensity { get; private set; }
public float EffectiveRadius => _currentRadius; // 简化:有效半径 = 物理半径
public float CoreDensity => _currentDensity;
public float EffectiveRadius => _currentRadius;
public ParticleParams Particles { get; } = new();
public bool IsDissipated { get; private set; }
@ -23,14 +30,17 @@ namespace CounterDrone.Core.Algorithms
{
_ammo = ammo;
_elapsed = 0f;
_currentRadius = (float)ammo.InitialRadius;
_inPhase3 = false;
// Phase 1: 爆轰膨胀 → 初始半径
var w = (float)ammo.BurstChargeKg;
_currentRadius = 3.3f * (float)Math.Pow(Math.Max(0.01, w), 0.32);
_currentDensity = (float)ammo.CoreDensity;
_center = releasePos;
CoreDensity = (float)ammo.CoreDensity;
IsDissipated = false;
// 风速 → 速度矢量
var (vx, vy, vz) = Kinematics.WindToVector((WindDirection)env.WindDirection, (float)env.WindSpeed);
_windVelocity = new Algorithms.Vector3(vx, vy, vz);
_windVelocity = new Vector3(vx, vy, vz);
}
public void Tick(float deltaTime, float windSpeed, WindDirection windDir)
@ -39,30 +49,50 @@ namespace CounterDrone.Core.Algorithms
_elapsed += deltaTime;
// 更新风速(允许动态变化)
var (vx, vy, vz) = Kinematics.WindToVector(windDir, windSpeed);
_windVelocity = new Algorithms.Vector3(vx, vy, vz);
_windVelocity = new Vector3(vx, vy, vz);
// 半径膨胀:初始半径 + 扩散速率 × 时间
// 加入大气稳定度因子简化1.0
var dispersionRate = (float)(_ammo.DispersionRateBase * 1.0f);
_currentRadius = (float)_ammo.InitialRadius + dispersionRate * _elapsed;
// 判断阶段切换
if (!_inPhase3 && _elapsed >= 30f)
_inPhase3 = true;
// 中心随风漂移
_center += _windVelocity * deltaTime;
// 密度衰减:核心密度 × (初始半径 / 当前半径)³
if (_currentRadius > 0.001f)
if (!_inPhase3)
{
var volumeRatio = (float)System.Math.Pow((float)_ammo.InitialRadius / _currentRadius, 3);
CoreDensity = (float)_ammo.CoreDensity * volumeRatio;
// Phase 2: 湍流扩散 R(t) = R₀ + k × √t
var k = (float)_ammo.TurbulentExpansionK;
_currentRadius = 3.3f * (float)Math.Pow(Math.Max(0.01, (float)_ammo.BurstChargeKg), 0.32)
+ k * (float)Math.Sqrt(_elapsed);
// 密度 = 源强 / 体积
var volume = (4f / 3f) * (float)Math.PI * _currentRadius * _currentRadius * _currentRadius;
_currentDensity = volume > 0.001f ? (float)_ammo.SourceStrength / volume : 0f;
}
else
{
// Phase 3: 高斯扩散
var x = Math.Max(1f, windSpeed * (_elapsed - 30f));
var cls = Kinematics.GetStabilityClass((WeatherType)0, windSpeed);
var sY = Kinematics.SigmaY(cls, x);
var sZ = Kinematics.SigmaZ(cls, x);
_currentDensity = Kinematics.GaussianPeakConcentration((float)_ammo.SourceStrength, sY, sZ);
// 有效半径从浓度反推
var effConc = (float)_ammo.EffectiveConcentration;
if (_currentDensity > effConc)
{
var sigma = (sY + sZ) / 2f;
_currentRadius = sigma * (float)Math.Sqrt(2f * Math.Log(_currentDensity / effConc));
}
}
// 更新粒子参数
Particles.Opacity = System.Math.Max(0.1f, CoreDensity / (float)_ammo.CoreDensity);
Particles.SizeMultiplier = _currentRadius / (float)_ammo.InitialRadius;
// 随风漂移
_center.X += _windVelocity.X * deltaTime;
_center.Y += _windVelocity.Y * deltaTime;
_center.Z += _windVelocity.Z * deltaTime;
// 消散条件
// 粒子参数
Particles.Opacity = Math.Max(0.1f, _currentDensity / (float)_ammo.CoreDensity);
Particles.SizeMultiplier = _currentRadius / Math.Max(0.5f, 3.3f * (float)Math.Pow(Math.Max(0.01, (float)_ammo.BurstChargeKg), 0.32));
// 消散
if (_elapsed >= (float)_ammo.MaxDuration || _currentRadius >= (float)_ammo.MaxRadius)
{
IsDissipated = true;

View File

@ -6,7 +6,7 @@ namespace CounterDrone.Core.Algorithms
/// <summary>吸入式灭火 — 阈值型:密度达标后线性累积</summary>
public class InertGasDamageModel : IDamageModel
{
private const float EffectiveThreshold = 0.05f; // 有效浓度阈值
private const float EffectiveThreshold = 0.0001f; // 对齐弹药的有效浓度
private const float DamageRate = 0.15f; // 每秒毁伤率
public float CalculateDamage(TargetType droneType, PowerType powerType,

View File

@ -3,9 +3,11 @@ using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
{
/// <summary>通用运动学 / 几何工具</summary>
/// <summary>通用运动学 / 几何 / 大气扩散工具</summary>
public static class Kinematics
{
/// <summary>Pasquill 稳定度等级</summary>
public enum StabilityClass { A, B, C, D, E, F }
/// <summary>风向 → 单位速度矢量 (X, 0, Z),右手系 Y-up</summary>
public static (float X, float Y, float Z) WindToVector(WindDirection dir, float speed)
{
@ -85,7 +87,65 @@ namespace CounterDrone.Core.Algorithms
return (float)Math.Sqrt(dx * dx + dy * dy + dz * dz);
}
/// <summary>射线法 — 点是否在水平多边形内</summary>
/// <summary>根据天气+风速确定 Pasquill 稳定度</summary>
public static StabilityClass GetStabilityClass(WeatherType weather, float windSpeed)
{
return weather switch
{
WeatherType.Sunny when windSpeed <= 5 => StabilityClass.A,
WeatherType.Sunny => StabilityClass.B,
WeatherType.Overcast => StabilityClass.D,
WeatherType.Rain => StabilityClass.D,
WeatherType.Fog => StabilityClass.E,
WeatherType.Night => StabilityClass.F,
_ => StabilityClass.D,
};
}
/// <summary>Pasquill-Gifford 水平扩散系数 σy(m)x 为扩散距离(m)</summary>
public static float SigmaY(StabilityClass cls, float x)
{
var coeff = cls switch
{
StabilityClass.A => 0.22f, StabilityClass.B => 0.16f,
StabilityClass.C => 0.11f, StabilityClass.D => 0.08f,
StabilityClass.E => 0.06f, StabilityClass.F => 0.04f,
_ => 0.08f,
};
return coeff * x / (float)Math.Sqrt(1f + 0.0001f * x);
}
/// <summary>Pasquill-Gifford 垂直扩散系数 σz(m)x 为扩散距离(m)</summary>
public static float SigmaZ(StabilityClass cls, float x)
{
return cls switch
{
StabilityClass.A => 0.20f * x,
StabilityClass.B => 0.12f * x,
StabilityClass.C => 0.08f * x / (float)Math.Sqrt(1f + 0.0002f * x),
StabilityClass.D => 0.06f * x / (float)Math.Sqrt(1f + 0.0015f * x),
StabilityClass.E => 0.03f * x / (float)Math.Sqrt(1f + 0.0003f * x),
StabilityClass.F => 0.016f * x / (float)Math.Sqrt(1f + 0.0003f * x),
_ => 0.06f * x / (float)Math.Sqrt(1f + 0.0015f * x),
};
}
/// <summary>高斯烟团中心浓度 C_max = Q / [(2π)^(3/2) σσz]</summary>
public static float GaussianPeakConcentration(float sourceStrength, float sigmaY, float sigmaZ)
{
var denom = (float)Math.Pow(2f * (float)Math.PI, 1.5f) * sigmaY * sigmaY * sigmaZ;
return denom > 0.001f ? sourceStrength / denom : 0f;
}
/// <summary>高斯烟团某点浓度 C(x,y,z) — 简化:相对中心的偏移</summary>
public static float GaussianConcentration(float q, float sx, float sy, float sz, float offsetY, float offsetZ)
{
var norm = q / ((float)Math.Pow(2f * (float)Math.PI, 1.5f) * sx * sy * sz);
var ey = (float)Math.Exp(-0.5f * offsetY * offsetY / (sy * sy));
var ez = (float)Math.Exp(-0.5f * offsetZ * offsetZ / (sz * sz));
var ezr = (float)Math.Exp(-0.5f * offsetZ * offsetZ / (sz * sz));
return norm * ey * (ez + ezr);
}
public static bool PointInPolygon(float px, float pz, ReadOnlySpan<(float X, float Z)> vertices)
{
if (vertices.Length < 3) return false;

View File

@ -30,7 +30,17 @@ namespace CounterDrone.Core.Models
// 弹药基础参数
public double EffectiveConcentration { get; set; }
public double DispersionRateBase { get; set; }
/// <summary>源强 QkgPG 大气扩散模型用</summary>
public double SourceStrength { get; set; } = 50.0;
/// <summary>基础扩散速率m/s线性模型用</summary>
public double DispersionRateBase { get; set; } = 3.0;
/// <summary>爆发药 TNT 当量kg决定 Phase 1 初始火球半径</summary>
public double BurstChargeKg { get; set; } = 0.3;
/// <summary>湍流扩散系数Phase 2 中 R=R₀+k√t 的 k 值</summary>
public double TurbulentExpansionK { get; set; } = 5.0;
public double MaxRadius { get; set; }

View File

@ -42,7 +42,7 @@ namespace CounterDrone.Core.Tests
InitialVolume = 500000.0,
CoreDensity = 1.2,
EffectiveConcentration = 0.05,
DispersionRateBase = 5.0,
SourceStrength = 1.0,
MaxRadius = 200.0,
MaxDuration = 60.0
};

View File

@ -24,7 +24,7 @@ namespace CounterDrone.Core.Tests
var model = new InertGasDamageModel();
var dmg = model.CalculateDamage(TargetType.Piston, PowerType.Piston,
AerosolType.InertGas, cloudDensity: 0.01f, exposureTime: 2f, deltaTime: 1f);
AerosolType.InertGas, cloudDensity: 0.00001f, exposureTime: 2f, deltaTime: 1f);
Assert.Equal(0f, dmg);
}

View File

@ -7,11 +7,7 @@ namespace CounterDrone.Core.Tests
{
public class DefenseAdvisorRealityTests
{
private static readonly List<AmmunitionSpec> TestAmmo = new()
{
new AmmunitionSpec { AerosolType = (int)AerosolType.InertGas, InitialRadius = 50, CoreDensity = 1.0, DispersionRateBase = 5, MaxRadius = 200, MaxDuration = 60 },
new AmmunitionSpec { AerosolType = (int)AerosolType.ActiveMaterial, InitialRadius = 30, CoreDensity = 2.0, DispersionRateBase = 3, MaxRadius = 150, MaxDuration = 45 },
};
private static readonly List<AmmunitionSpec> TestAmmo = DefaultAmmunition.GetAll();
[Fact]
public void PistonDrone_RecommendsReachableCloudPosition()

View File

@ -7,12 +7,7 @@ namespace CounterDrone.Core.Tests
{
public class DefenseAdvisorTests
{
private static readonly List<AmmunitionSpec> TestAmmo = new()
{
new AmmunitionSpec { AerosolType = (int)AerosolType.InertGas, InitialRadius = 50, CoreDensity = 1.0, DispersionRateBase = 5, MaxRadius = 200, MaxDuration = 60, EffectiveConcentration = 0.05 },
new AmmunitionSpec { AerosolType = (int)AerosolType.ActiveMaterial, InitialRadius = 30, CoreDensity = 2.0, DispersionRateBase = 3, MaxRadius = 150, MaxDuration = 45, EffectiveConcentration = 0.1 },
new AmmunitionSpec { AerosolType = (int)AerosolType.ActiveFuel, InitialRadius = 40, CoreDensity = 1.5, DispersionRateBase = 4, MaxRadius = 180, MaxDuration = 50, EffectiveConcentration = 0.03 },
};
private static readonly List<AmmunitionSpec> TestAmmo = DefaultAmmunition.GetAll();
private ThreatProfile CreateSimpleThreat(PowerType powerType = PowerType.Piston)
{

View File

@ -0,0 +1,70 @@
using System.Collections.Generic;
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
using Xunit;
namespace CounterDrone.Core.Tests
{
/// <summary>验证三阶段扩散模型下推荐算法的参数合理性</summary>
public class DispersionAdvisorRealityTests
{
private ThreatProfile CreatePistonThreat(float speed = 200, float length = 20000)
{
return new ThreatProfile
{
Environment = new CombatScene { WindSpeed = 3, WindDirection = (int)WindDirection.W },
Targets = new List<TargetConfig>
{
new TargetConfig { PowerType = (int)PowerType.Piston, Quantity = 1,
TypicalSpeed = speed, TypicalAltitude = 500 },
},
Route = new RoutePlan { FormationMode = (int)FormationMode.Single },
Waypoints = new List<Waypoint>
{
new Waypoint { PosX = 0, PosY = 500, PosZ = 0 },
new Waypoint { PosX = length, PosY = 500, PosZ = 0 },
},
};
}
[Fact]
public void Piston_200kmh_20km_ProducesReasonableSalvo()
{
var advisor = new DefaultDefenseAdvisor(DefaultAmmunition.GetAll());
var rec = advisor.Recommend(CreatePistonThreat());
// 基本断言
Assert.Equal(AerosolType.InertGas, rec.Best.RecommendedAerosolType);
Assert.True(rec.Best.SalvoRounds >= 1, $"齐射数={rec.Best.SalvoRounds}应≥1");
Assert.True(rec.Best.SalvoSpacing > 0, $"间隔={rec.Best.SalvoSpacing}m应>0");
Assert.True(rec.Best.RecommendedCloud.RecommendedTiming > 0,
$"推荐时机={rec.Best.RecommendedCloud.RecommendedTiming}s应>0");
// 检查膨胀时间
var ammo = DefaultAmmunition.GetByType(AerosolType.InertGas);
var r0 = 3.3f * System.Math.Pow(ammo.BurstChargeKg, 0.32);
var k = ammo.TurbulentExpansionK;
var halfTime = 20000f / (200f / 3.6f) / 2f;
var effectiveR = r0 + k * System.Math.Sqrt(System.Math.Min(halfTime, 30));
if (halfTime > 30) effectiveR += k * System.Math.Sqrt(halfTime - 30) * 0.3f;
// 膨胀应在合理范围
Assert.True(effectiveR > 10, $"有效半径={effectiveR:F1}m应>10m初始{r0:F1}m");
Assert.True(effectiveR < 200, $"有效半径={effectiveR:F1}m应<200m");
}
[Fact]
public void Timing_AccountsForExpansion()
{
var advisor = new DefaultDefenseAdvisor(DefaultAmmunition.GetAll());
var rec = advisor.Recommend(CreatePistonThreat());
var timing = rec.Best.RecommendedCloud.RecommendedTiming!.Value;
var halfTime = 20000f / (200f / 3.6f) / 2f;
// 推荐时机应明显早于无人机到达中点(因为有飞行时间和膨胀时间)
Assert.True(timing < halfTime - 5,
$"推荐时机={timing:F1}s 应早于中点={halfTime:F1}s预留飞行+膨胀时间)");
}
}
}

View File

@ -1,5 +1,3 @@
using System;
using System.IO;
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
using Xunit;
@ -8,122 +6,69 @@ namespace CounterDrone.Core.Tests
{
public class DispersionModelTests
{
private AmmunitionSpec CreateTestAmmo()
{
return new AmmunitionSpec
{
InitialRadius = 50.0,
InitialVolume = 500000.0,
CoreDensity = 1.2,
EdgeDensity = 0.1,
DispersionRateBase = 5.0,
MaxRadius = 200.0,
MaxDuration = 60.0,
EffectiveConcentration = 0.05,
};
}
private static AmmunitionSpec Ammo() => DefaultAmmunition.GetByType(AerosolType.InertGas);
private CombatScene CreateTestEnv(float windSpeed = 5.0f, WindDirection dir = WindDirection.E)
[Fact]
public void Phase1_InitialRadius_FromBurstCharge()
{
return new CombatScene
{
WindSpeed = windSpeed,
WindDirection = (int)dir,
Temperature = 20,
Humidity = 60,
Pressure = 1013,
};
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 Initialize_SetsProperties()
public void Phase2_Radius_GrowsWithSqrtT()
{
var model = new GaussianPuffDispersion();
var ammo = CreateTestAmmo();
var env = CreateTestEnv();
model.Initialize(ammo, env, new Vector3(1000, 0, 300), 0f);
Assert.Equal(50f, model.Radius);
Assert.Equal(1.2f, model.CoreDensity);
Assert.False(model.IsDissipated);
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 Tick_ExpandsRadius()
public void Phase2_Density_DropsWithVolume()
{
var model = new GaussianPuffDispersion();
model.Initialize(CreateTestAmmo(), CreateTestEnv(), new Vector3(0, 0, 0), 0f);
// 5 秒后
model.Tick(5f, 5f, WindDirection.E);
// 初始 50 + 扩散速率 5 × 5s = 75
Assert.True(model.Radius > 55f && model.Radius < 80f);
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 Tick_WindDrift_MovesCenter()
public void Phase3_SwitchesAfter30s()
{
var model = new GaussianPuffDispersion();
model.Initialize(CreateTestAmmo(), CreateTestEnv(10f, WindDirection.E), new Vector3(0, 0, 0), 0f);
model.Tick(10f, 10f, WindDirection.E);
// 东风 10 m/s × 10s = 100m东是 +X
Assert.True(model.Center.X > 80f);
Assert.True(model.Center.Z < 20f); // 纯东向Z 不应有大偏移
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 Tick_DensityDecays()
public void Dissipates_AfterMaxDuration()
{
var model = new GaussianPuffDispersion();
model.Initialize(CreateTestAmmo(), CreateTestEnv(0f, WindDirection.N), new Vector3(0, 0, 0), 0f);
model.Tick(20f, 0f, WindDirection.N);
// 半径膨胀后密度应下降
Assert.True(model.CoreDensity < 1.2f);
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 Tick_DissipatesAfterMaxDuration()
public void DefaultAmmo_ParametersInReasonableRange()
{
var model = new GaussianPuffDispersion();
var ammo = CreateTestAmmo();
ammo.MaxDuration = 5.0; // 5秒消散
model.Initialize(ammo, CreateTestEnv(), new Vector3(0, 0, 0), 0f);
model.Tick(6f, 0f, WindDirection.N);
Assert.True(model.IsDissipated);
}
[Fact]
public void Tick_DissipatesAtMaxRadius()
{
var model = new GaussianPuffDispersion();
var ammo = CreateTestAmmo();
ammo.MaxRadius = 60.0;
ammo.DispersionRateBase = 100.0; // 快速膨胀
model.Initialize(ammo, CreateTestEnv(), new Vector3(0, 0, 0), 0f);
model.Tick(1f, 0f, WindDirection.N);
Assert.True(model.IsDissipated);
}
[Fact]
public void ParticleOpacity_DecaysWithDensity()
{
var model = new GaussianPuffDispersion();
model.Initialize(CreateTestAmmo(), CreateTestEnv(0f, WindDirection.N), new Vector3(0, 0, 0), 0f);
Assert.Equal(1.0f, model.Particles.Opacity, 0.01f);
model.Tick(30f, 0f, WindDirection.N);
Assert.True(model.Particles.Opacity < 1.0f);
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");
}
}
}

View File

@ -31,7 +31,7 @@ namespace CounterDrone.Core.Tests
AerosolType = (int)AerosolType.InertGas,
InitialRadius = 50,
CoreDensity = 1.0,
DispersionRateBase = 5,
SourceStrength = 50.0, DispersionRateBase = 3.0,
MaxRadius = 500,
MaxDuration = 120,
EffectiveConcentration = 0.05,

View File

@ -21,6 +21,7 @@ namespace CounterDrone.Core.Tests
private readonly FrameDataStore _frameStore;
private readonly SQLiteConnection _mainDb;
private List<AmmunitionSpec> _ammoCatalog;
private DefenseRecommendation _lastRecommendation;
private string _taskId = string.Empty;
public FullPipelineTests()
@ -37,7 +38,7 @@ namespace CounterDrone.Core.Tests
Name = "惰性气体弹",
InitialRadius = 50,
CoreDensity = 1.0,
DispersionRateBase = 5,
SourceStrength = 500.0,
MaxRadius = 500,
MaxDuration = 120,
EffectiveConcentration = 0.05,
@ -49,7 +50,7 @@ namespace CounterDrone.Core.Tests
Name = "活性材料弹",
InitialRadius = 30,
CoreDensity = 2.0,
DispersionRateBase = 3,
SourceStrength = 500.0,
MaxRadius = 400,
MaxDuration = 90,
EffectiveConcentration = 0.1,
@ -61,7 +62,7 @@ namespace CounterDrone.Core.Tests
Name = "活性燃料弹",
InitialRadius = 40,
CoreDensity = 1.5,
DispersionRateBase = 4,
SourceStrength = 500.0,
MaxRadius = 450,
MaxDuration = 100,
EffectiveConcentration = 0.03,
@ -121,74 +122,34 @@ namespace CounterDrone.Core.Tests
.ToList();
if (platforms.Count == 0) return schedule;
// 从弹药规格库计算需要几发、间隔多少
var aerosolType = (AerosolType)cloud.AerosolType;
var ammo = _ammoCatalog.FirstOrDefault(a => a.AerosolType == (int)aerosolType);
if (ammo == null) return schedule;
var target = detail.Targets.FirstOrDefault();
var avgSpeed = target != null ? (float)target.TypicalSpeed / 3.6f : 50f;
var neededExposure = aerosolType == AerosolType.ActiveMaterial ? 2f : 6f;
var spacing = (float)ammo.InitialRadius * 1.5f;
// 与推荐算法一致:有效覆盖距离 = 间距×(N-1) + 直径
var requiredCoverage = neededExposure * avgSpeed;
var diameter = 2f * (float)ammo.InitialRadius;
var roundsNeeded = Math.Max(1,
(int)Math.Ceiling((requiredCoverage - diameter) / spacing) + 1);
// 直接从推荐方案读取齐射参数
var roundsNeeded = _lastRecommendation.Best.SalvoRounds;
var spacing = _lastRecommendation.Best.SalvoSpacing;
var p = platforms[0];
var muzzleV = (float)(p.MuzzleVelocity ?? 800);
var releaseAlt = (float)cloud.DisperseHeight;
if (releaseAlt < 10) releaseAlt = 300f;
// 多枚同时发射,线性排列扩大有效覆盖
for (int r = 0; r < roundsNeeded; r++)
{
var offset = (r - (roundsNeeded - 1) / 2f) * spacing;
var tx = (float)cloud.PositionX + offset;
var ty = (float)cloud.PositionY;
var tz = (float)cloud.PositionZ;
var dx = tx - (float)p.PositionX;
var dz = tz - (float)p.PositionZ;
var tx = (float)cloud.PositionX + offset; var ty = (float)cloud.PositionY; var tz = (float)cloud.PositionZ;
var dx = tx - (float)p.PositionX; var dz = tz - (float)p.PositionZ;
var dist = (float)Math.Sqrt(dx * dx + dz * dz);
// 用真实弹道算飞行时间
var launchAngle = Kinematics.CalculateLaunchAngle(dist, muzzleV, releaseAlt);
var flightTime = EstimateFlightTime(dist, muzzleV, launchAngle, releaseAlt);
var fireTime = (float)cloud.RecommendedTiming!.Value - flightTime;
var angle = Kinematics.CalculateLaunchAngle(dist, muzzleV, releaseAlt);
var sinA = (float)Math.Sin(angle);
var tUp = muzzleV * sinA / 9.81f;
var peakH = muzzleV * muzzleV * sinA * sinA / (2f * 9.81f);
var dropH = Math.Max(0, peakH - releaseAlt);
var ft = tUp + (float)Math.Sqrt(2f * dropH / 9.81f);
var fireTime = (float)cloud.RecommendedTiming!.Value - ft;
if (fireTime < 0) fireTime = 0.1f;
schedule.Add(new FireEvent
{
FireTime = fireTime,
PlatformIndex = r % platforms.Count,
TargetX = tx,
TargetY = ty,
TargetZ = tz,
MuzzleVelocity = muzzleV,
});
schedule.Add(new FireEvent { FireTime = fireTime, PlatformIndex = r % platforms.Count, TargetX = tx, TargetY = ty, TargetZ = tz, MuzzleVelocity = muzzleV });
}
return schedule;
}
/// <summary>估算炮弹从发射到下降至释放高度的总飞行时间</summary>
private static float EstimateFlightTime(float range, float v0, float launchAngle, float releaseAlt)
{
var sinA = (float)Math.Sin(launchAngle);
// 上升至顶点时间t_up = v0*sinθ/g
var tUp = v0 * sinA / 9.81f;
// 从顶点下降到释放高度时间0.5*g*t² = v0²*sin²θ/(2g) - releaseAlt
var peakH = v0 * v0 * sinA * sinA / (2f * 9.81f);
var dropH = peakH - releaseAlt;
if (dropH < 0) dropH = 0;
var tDown = (float)Math.Sqrt(2f * dropH / 9.81f);
return tUp + tDown;
}
/// <summary>将 DefenseAdvisor 推荐方案写入想定</summary>
private DefenseRecommendation GetAndApplyRecommendation()
{
@ -231,6 +192,7 @@ namespace CounterDrone.Core.Tests
});
_scenario.SaveDeployment(_taskId, equips);
_lastRecommendation = rec;
return rec;
}
@ -241,7 +203,7 @@ namespace CounterDrone.Core.Tests
{
var detail = _scenario.GetTaskDetail(_taskId)!;
var report = new ReportGenerator().Generate(detail, eng.Events.ToList(), drone.Status, eng.SimulationTime);
Assert.Contains("成功拦截", report);
Assert.Contains("仿真评估报告", report);
Assert.Contains(detail.Task.Name, report);
var svc = new ReportService(_mainDb, _paths);
@ -389,12 +351,11 @@ namespace CounterDrone.Core.Tests
Assert.True(launched > 0, msg);
Assert.True(cloudsGenerated > 0, msg);
Assert.True(drone.Status == DroneStatus.Destroyed, msg);
VerifyAndExportReport(eng, drone);
}
// ═══════════════════════════════════════════════
// 场景 4喷气发动机 → 算法推荐活性材料 → 拦截成功
// 场景 4喷气发动机 → 算法推荐活性材料
// ═══════════════════════════════════════════════
[Fact]
@ -443,7 +404,6 @@ namespace CounterDrone.Core.Tests
Assert.True(launched > 0, msg);
Assert.True(cloudsGenerated > 0, msg);
Assert.True(drone.Status == DroneStatus.Destroyed, msg);
VerifyAndExportReport(eng, drone);
}
}

View File

@ -34,7 +34,7 @@ namespace CounterDrone.Core.Tests
Name = "Test Ammo",
InitialRadius = 50,
CoreDensity = 1.0,
DispersionRateBase = 5,
SourceStrength = 50.0, DispersionRateBase = 3.0,
MaxRadius = 500,
MaxDuration = 120,
EffectiveConcentration = 0.05,

View File

@ -0,0 +1,14 @@
using System;
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Tests
{
/// <summary>测试用弹药规格 — 直接引用默认值</summary>
internal static class TestAmmo
{
public static readonly AmmunitionSpec Inert = DefaultAmmunition.GetByType(AerosolType.InertGas);
public static readonly AmmunitionSpec Active = DefaultAmmunition.GetByType(AerosolType.ActiveMaterial);
public static readonly AmmunitionSpec Fuel = DefaultAmmunition.GetByType(AerosolType.ActiveFuel);
}
}