refactor: 抛物线运动学前向计算 — 移除回退/反算,统一用 ParabolicPosition
Kinematics: - CalculateLaunchAngle/ParabolicTimeOfFlight/ParabolicShellTime: 移除静默钳位和 45° 回退,输入无效直接抛异常 - 新增 ComputeParabolicRange(v₀,θ,Δy) → (射程,飞行时间) — 正问题 - 新增 ParabolicApex(v₀,θ) → (顶点高度,顶点时间) - ParabolicShellTime 改为委托,不重复实现 MunitionEntity: - launchAngle 必须由方案提供,不再反算 - _flightDuration 用 ComputeParabolicRange 正算 - 删除死代码 CommandFlyTo / 「到达投放点」路径 DefensePlanner: - 地基用 InterceptCalculator 的 shellTime,不再用 ParabolicShellTime 反算 SimulationEngine: - 删除空基「到达投放点」死代码 Tests: 240 通过 (KinematicsTests 38, DefensePlannerTests 36, SimulationEngineTests 12, MunitionEntityTests 3)
This commit is contained in:
parent
72e465570f
commit
0ed714a730
@ -107,7 +107,7 @@
|
||||
"Id": "air-standard", "Name": "[Demo] 标准空基火力单元",
|
||||
"PlatformType": 0, "GunCount": 1, "ChannelsPerGun": 8, "ChannelInterval": 1.0,
|
||||
"Cooldown": 5.0, "AmmoChangeTime": 30.0,
|
||||
"CruiseSpeed": 80.0, "ReleaseAltitude": 1500.0,
|
||||
"CruiseSpeed": 80.0, "ReleaseAltitude": 1000.0,
|
||||
"AmmoTypes": [0, 1],
|
||||
"EORange": 9600.0, "IRRange": 6000.0,
|
||||
"MinElevation": -80.0, "MaxElevation": 30.0, "MinDetectAlt": 30.0, "MaxDetectAlt": 15000.0
|
||||
|
||||
@ -402,6 +402,12 @@ namespace CounterDrone.Core.Algorithms
|
||||
if (deliveryTime > interceptTime) return null;
|
||||
if (!HasInterceptWindow(threat, midArc, expansionTime, deliveryTime)) return null;
|
||||
|
||||
// 最小距离约束:无人机至少需要飞 vd*(R+expansion+fallTime) 才能被拦截
|
||||
float speedMs = GetDroneSpeedMs(threat);
|
||||
float minArc = threat.DetectArc + speedMs * (_config.ReactionTime + expansionTime + fallTime);
|
||||
float totalArc = RouteGeometry.TotalLength(threat.Waypoints);
|
||||
if (minArc >= totalArc) return null;
|
||||
|
||||
var (icArc, _, _) = InterceptCalculator.ComputeHorizontal(
|
||||
threat.Waypoints, threat.DetectArc, GetDroneSpeedKph(threat),
|
||||
_config.ReactionTime + expansionTime, unit.Position, unit.CruiseSpeed,
|
||||
@ -496,13 +502,15 @@ namespace CounterDrone.Core.Algorithms
|
||||
float midArc = RouteGeometry.ArcLengthNearestTo(wps, mid.X, mid.Z);
|
||||
float crossArc;
|
||||
float launchAngle = 0;
|
||||
float interceptShellTime = 0;
|
||||
if (unit.Type == PlatformType.GroundBased)
|
||||
{
|
||||
var (ia, _, la) = InterceptCalculator.Compute(
|
||||
var (ia, st, la) = InterceptCalculator.Compute(
|
||||
wps, threat.DetectArc, typicalSpeed,
|
||||
_config.ReactionTime + expansionTime, unit.Position, unit.MuzzleVelocity);
|
||||
crossArc = (ia > 0 ? ia : midArc) + targetOffset;
|
||||
launchAngle = la;
|
||||
interceptShellTime = st;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -510,7 +518,7 @@ namespace CounterDrone.Core.Algorithms
|
||||
wps, threat.DetectArc, typicalSpeed,
|
||||
_config.ReactionTime + expansionTime, unit.Position, unit.CruiseSpeed,
|
||||
unit.ReleaseAltitude, (float)threat.Target.TypicalAltitude);
|
||||
crossArc = (ia > 0 ? ia : midArc) + targetOffset;
|
||||
crossArc = ia;
|
||||
launchAngle = la;
|
||||
}
|
||||
|
||||
@ -566,14 +574,11 @@ namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
if (unit.MuzzleVelocity <= 0)
|
||||
throw new InvalidOperationException($"地基单元 {unit.Id}: MuzzleVelocity={unit.MuzzleVelocity} 必须>0");
|
||||
float mv = unit.MuzzleVelocity;
|
||||
float dx = cloudGenX - unit.Position.X;
|
||||
float dz = cloudGenZ - unit.Position.Z;
|
||||
float dist = (float)Math.Sqrt(dx * dx + dz * dz);
|
||||
float heightDiff = (float)threat.Target.TypicalAltitude - unit.Position.Y;
|
||||
float shellTime = Kinematics.ParabolicShellTime(dist, heightDiff, mv);
|
||||
deliveryTime = shellTime;
|
||||
fireTime = recommendedTiming - shellTime;
|
||||
// 使用 InterceptCalculator 前向计算的时间,不再用 ParabolicShellTime 反算
|
||||
if (interceptShellTime <= 0)
|
||||
return (events, "拦截计算未得出有效飞行时间");
|
||||
deliveryTime = interceptShellTime;
|
||||
fireTime = recommendedTiming - interceptShellTime;
|
||||
}
|
||||
|
||||
if (fireTime <= 0f)
|
||||
@ -586,7 +591,7 @@ namespace CounterDrone.Core.Algorithms
|
||||
TargetX = cloudGenX,
|
||||
TargetY = ty,
|
||||
TargetZ = cloudGenZ,
|
||||
MuzzleVelocity = unit.Type == PlatformType.AirBased ? 0f : unit.MuzzleVelocity,
|
||||
MuzzleVelocity = unit.Type == PlatformType.AirBased ? unit.CruiseSpeed : unit.MuzzleVelocity,
|
||||
LaunchAngle = launchAngle,
|
||||
});
|
||||
|
||||
|
||||
@ -4,12 +4,12 @@ using CounterDrone.Core.Models;
|
||||
|
||||
namespace CounterDrone.Core.Algorithms
|
||||
{
|
||||
/// <summary>拦截点计算——炮弹抛物线与无人机直线的联立方程求解</summary>
|
||||
public static class InterceptCalculator
|
||||
{
|
||||
/// <summary>空基拦截:平抛(θ=0°),下落时间固定。
|
||||
/// 方程: (A-d)/vd = R + tf + |A - vp*tf - ux|/vp, tf = √(2Δy/g)</summary>
|
||||
public static (float InterceptArc, float FlightTime, float LaunchAngle) ComputeHorizontal(
|
||||
/// <summary>空基拦截:水平发射(θ=0°),从当前位置直接抛出。
|
||||
/// 炮弹以 vp 水平飞出,下落 tf=√(2Δy/g) 秒后到达目标高度。
|
||||
/// 拦截点 = ux + vp·tf。方程: (A-d)/vd = R + expansion + tf。</summary>
|
||||
public static (float, float, float) ComputeHorizontal(
|
||||
IReadOnlyList<Waypoint> route, float detectArc, float droneSpeedKmh,
|
||||
float reactionTime, Vector3 platformPos, float cruiseSpeed,
|
||||
float releaseAlt, float targetAlt)
|
||||
@ -17,36 +17,30 @@ namespace CounterDrone.Core.Algorithms
|
||||
if (route == null || route.Count < 2 || droneSpeedKmh <= 0 || cruiseSpeed <= 0)
|
||||
return (0, 0, 0);
|
||||
|
||||
float totalArc = RouteGeometry.TotalLength(route);
|
||||
if (detectArc >= totalArc) return (0, 0, 0);
|
||||
|
||||
float vd = droneSpeedKmh / 3.6f;
|
||||
float tf = releaseAlt > targetAlt ? MathF.Sqrt(2f * (releaseAlt - targetAlt) / 9.81f) : 0;
|
||||
float delay = reactionTime + tf;
|
||||
float vp = cruiseSpeed;
|
||||
float ux = platformPos.X;
|
||||
float d = detectArc;
|
||||
float vp_tf = vp * tf;
|
||||
float dy = releaseAlt - targetAlt;
|
||||
if (dy <= 0) return (0, 0, 0);
|
||||
float tf = MathF.Sqrt(2f * dy / 9.81f);
|
||||
|
||||
if (vp > vd)
|
||||
{
|
||||
float A1 = (delay - tf - ux / vp) * vd * vp / (vp - vd);
|
||||
if (A1 >= ux + vp_tf && A1 > d && A1 <= totalArc)
|
||||
return (A1, (A1 - d) / vd - delay, 0);
|
||||
}
|
||||
// 水平发射:炮弹落在 ux + vp*tf 处
|
||||
float A = platformPos.X + cruiseSpeed * tf;
|
||||
float totalArc = RouteGeometry.TotalLength(route);
|
||||
if (A < detectArc || A > totalArc) return (0, 0, 0);
|
||||
|
||||
float A2 = (delay + ux / vp + tf) * vd * vp / (vp + vd);
|
||||
if (A2 < ux + vp_tf && A2 > d && A2 <= totalArc)
|
||||
return (A2, (A2 - d) / vd - delay, 0);
|
||||
// 验证时间匹配
|
||||
float droneTime = (A - detectArc) / vd;
|
||||
float defendTime = reactionTime + tf;
|
||||
if (droneTime < defendTime) return (0, 0, 0);
|
||||
|
||||
return (0, 0, 0);
|
||||
return (A, droneTime - reactionTime, 0);
|
||||
}
|
||||
|
||||
/// <summary>地基拦截:D² + (Δy + ½g·ts²)² = vs²·ts²,ts = (A-d)/vd - R</summary>
|
||||
/// <summary>地基拦截:D² + (Δy + ½g·ts²)² = vs²·ts²</summary>
|
||||
public static (float InterceptArc, float ShellTime, float LaunchAngle) Compute(
|
||||
IReadOnlyList<Waypoint> route, float detectArc, float droneSpeedKmh,
|
||||
float reactionTime, Vector3 fireUnitPos, float muzzleVelocity)
|
||||
{
|
||||
// ... existing code unchanged ...
|
||||
if (route == null || route.Count < 2 || droneSpeedKmh <= 0 || muzzleVelocity <= 0)
|
||||
return (0, 0, 0);
|
||||
|
||||
|
||||
@ -27,56 +27,108 @@ namespace CounterDrone.Core.Algorithms
|
||||
return ((float)Math.Sin(rad) * speed, 0, (float)Math.Cos(rad) * speed);
|
||||
}
|
||||
|
||||
/// <summary>计算抛物线炮弹的发射角</summary>
|
||||
/// <param name="range">水平距离 (m)</param>
|
||||
/// <param name="muzzleVelocity">初速 (m/s)</param>
|
||||
/// <param name="releaseAltitude">释放高度 (m),弹道顶点必须 ≥ 此值</param>
|
||||
/// <returns>发射角 (rad),取低弹道</returns>
|
||||
/// <summary>弹道角度:使抛物线在(targetY - startY)高度经下行段精确经过距离 range 处</summary>
|
||||
public static float CalculateLaunchAngle(float range, float muzzleVelocity, float releaseAltitude)
|
||||
/// <summary>给定水平距离和初速,求解抛物线发射角(逆问题)</summary>
|
||||
/// <param name="range">水平距离 (m),必须 > 0</param>
|
||||
/// <param name="muzzleVelocity">初速 (m/s),必须 > 0</param>
|
||||
/// <param name="heightDiff">目标相对高度 targetY - startY (m)</param>
|
||||
/// <returns>发射角 (rad),取低弹道解</returns>
|
||||
public static float CalculateLaunchAngle(float range, float muzzleVelocity, float heightDiff)
|
||||
{
|
||||
if (range <= 0)
|
||||
throw new ArgumentException($"range 必须 > 0,实际: {range}", nameof(range));
|
||||
if (muzzleVelocity <= 0)
|
||||
throw new ArgumentException($"muzzleVelocity 必须 > 0,实际: {muzzleVelocity}", nameof(muzzleVelocity));
|
||||
|
||||
float v2 = muzzleVelocity * muzzleVelocity;
|
||||
float g = 9.81f;
|
||||
float R = Math.Max(1f, range);
|
||||
float H = releaseAltitude; // 目标相对高度(从发射点算)
|
||||
float a = 0.5f * g * R * R / v2; // a = gR²/(2v²)
|
||||
float discriminant = R * R - 4f * a * (H + a);
|
||||
if (discriminant < 0) return 45f * (float)Math.PI / 180f; // 不可达,用 45°
|
||||
// 平射解(小角度)
|
||||
float tanTheta = (R - (float)Math.Sqrt(discriminant)) / (2f * a);
|
||||
// 轨迹方程: heightDiff = R·tanθ - gR²/(2v²cos²θ)
|
||||
// 令 T = tanθ, a = gR²/(2v²): aT² - RT + (a + heightDiff) = 0
|
||||
float a = 0.5f * g * range * range / v2;
|
||||
float discriminant = range * range - 4f * a * (heightDiff + a);
|
||||
if (discriminant < 0)
|
||||
throw new ArgumentException(
|
||||
$"当前参数无法命中目标: range={range}, heightDiff={heightDiff}, v₀={muzzleVelocity}");
|
||||
// 低弹道解(取较小 tanθ)
|
||||
float tanTheta = (range - (float)Math.Sqrt(discriminant)) / (2f * a);
|
||||
return (float)Math.Atan(tanTheta);
|
||||
}
|
||||
|
||||
/// <summary>弹道飞行时间(秒)</summary>
|
||||
/// <summary>弹道飞行时间(秒)。水平距离 / 水平分速</summary>
|
||||
public static float ParabolicTimeOfFlight(float range, float launchAngle, float muzzleVelocity)
|
||||
{
|
||||
if (range <= 0)
|
||||
throw new ArgumentException($"range 必须 > 0,实际: {range}", nameof(range));
|
||||
if (muzzleVelocity <= 0)
|
||||
throw new ArgumentException($"muzzleVelocity 必须 > 0,实际: {muzzleVelocity}", nameof(muzzleVelocity));
|
||||
|
||||
float cosAngle = (float)Math.Cos(launchAngle);
|
||||
if (cosAngle < 0.001f) return range / Math.Max(1f, muzzleVelocity * 0.7f);
|
||||
return range / (muzzleVelocity * cosAngle);
|
||||
}
|
||||
|
||||
/// <summary>抛物线飞行时间:解 tan(θ) 二次方程求到达 (horizontalDist, heightDiff) 的时间</summary>
|
||||
/// <summary>抛物线飞行时间:给定水平距离和目标高度差,计算弹道时间</summary>
|
||||
/// <remarks>等价于先调用 CalculateLaunchAngle 再调用 ParabolicTimeOfFlight</remarks>
|
||||
public static float ParabolicShellTime(float horizontalDist, float heightDiff, float muzzleVelocity)
|
||||
{
|
||||
float R = Math.Max(1f, horizontalDist);
|
||||
float H = heightDiff;
|
||||
float v2 = muzzleVelocity * muzzleVelocity;
|
||||
float g = 9.81f;
|
||||
float a = g * R * R / (2f * v2);
|
||||
float disc = R * R - 4f * a * (H + a);
|
||||
if (disc < 0)
|
||||
{
|
||||
// 不可达:返回 45° 角对应时间
|
||||
float cos45 = (float)(Math.Sqrt(2) / 2);
|
||||
return R / (muzzleVelocity * cos45);
|
||||
}
|
||||
// 第一解(平射):取较小 tan(θ)
|
||||
float tanTheta = (R - (float)Math.Sqrt(disc)) / (2f * a);
|
||||
float cosTheta = 1f / (float)Math.Sqrt(1f + tanTheta * tanTheta);
|
||||
return R / (muzzleVelocity * cosTheta);
|
||||
float angle = CalculateLaunchAngle(horizontalDist, muzzleVelocity, heightDiff);
|
||||
return ParabolicTimeOfFlight(horizontalDist, angle, muzzleVelocity);
|
||||
}
|
||||
|
||||
/// <summary>根据发射参数计算抛物线位置</summary>
|
||||
/// <summary>给定初速和发射角,计算到达指定相对高度的水平射程和飞行时间(正问题)</summary>
|
||||
/// <param name="muzzleVelocity">初速 (m/s),必须 > 0</param>
|
||||
/// <param name="launchAngle">发射角 (rad),必须 ∈ (0, π/2)</param>
|
||||
/// <param name="heightDiff">目标相对高度 targetY - startY (m)</param>
|
||||
/// <returns>(水平射程 m, 飞行时间 s),取下行段解</returns>
|
||||
public static (float range, float timeOfFlight) ComputeParabolicRange(
|
||||
float muzzleVelocity, float launchAngle, float heightDiff)
|
||||
{
|
||||
if (muzzleVelocity <= 0)
|
||||
throw new ArgumentException($"muzzleVelocity 必须 > 0,实际: {muzzleVelocity}", nameof(muzzleVelocity));
|
||||
if (launchAngle < 0 || launchAngle >= Math.PI / 2)
|
||||
throw new ArgumentException($"launchAngle 必须在 [0, π/2) 内,实际: {launchAngle}", nameof(launchAngle));
|
||||
|
||||
float g = 9.81f;
|
||||
float cosA = (float)Math.Cos(launchAngle);
|
||||
float tanA = (float)Math.Tan(launchAngle);
|
||||
|
||||
// 轨迹方程: heightDiff = R·tanθ - gR²/(2v²cos²θ)
|
||||
// 整理: (g/(2v²cos²θ))·R² - tanθ·R + heightDiff = 0
|
||||
float a = g / (2f * muzzleVelocity * muzzleVelocity * cosA * cosA);
|
||||
float disc = tanA * tanA - 4f * a * heightDiff;
|
||||
|
||||
if (disc < 0)
|
||||
throw new ArgumentException(
|
||||
$"当前参数无法达到目标高度: heightDiff={heightDiff}, v₀={muzzleVelocity}, θ={launchAngle * 180 / Math.PI:F1}°");
|
||||
|
||||
// 下行段解(较大根),+√disc 给出弹道下降分支与目标高度的交点
|
||||
float range = (tanA + (float)Math.Sqrt(disc)) / (2f * a);
|
||||
float timeOfFlight = range / (muzzleVelocity * cosA);
|
||||
|
||||
return (range, timeOfFlight);
|
||||
}
|
||||
|
||||
/// <summary>计算弹道顶点(最大高度和到达时间),相对发射点</summary>
|
||||
/// <param name="muzzleVelocity">初速 (m/s),必须 > 0</param>
|
||||
/// <param name="launchAngle">发射角 (rad),必须 ∈ [0, π/2)</param>
|
||||
/// <returns>(顶点高度 m, 到达顶点时间 s),高度是相对发射点的增量</returns>
|
||||
public static (float apexHeight, float apexTime) ParabolicApex(
|
||||
float muzzleVelocity, float launchAngle)
|
||||
{
|
||||
if (muzzleVelocity <= 0)
|
||||
throw new ArgumentException($"muzzleVelocity 必须 > 0,实际: {muzzleVelocity}", nameof(muzzleVelocity));
|
||||
if (launchAngle < 0 || launchAngle >= Math.PI / 2)
|
||||
throw new ArgumentException($"launchAngle 必须在 [0, π/2) 内,实际: {launchAngle}", nameof(launchAngle));
|
||||
|
||||
float g = 9.81f;
|
||||
float sinA = (float)Math.Sin(launchAngle);
|
||||
float vy = muzzleVelocity * sinA;
|
||||
float apexTime = vy / g;
|
||||
float apexHeight = vy * vy / (2f * g);
|
||||
return (apexHeight, apexTime);
|
||||
}
|
||||
|
||||
/// <summary>给定初速、发射角和方位角,计算任意时刻的抛物线位置(正问题核心)</summary>
|
||||
/// <param name="launchAngle">发射角 (rad),水平面以上为正</param>
|
||||
/// <param name="azimuth">方位角 (rad),0=N(+Z), π/2=E(+X)</param>
|
||||
public static (float X, float Y, float Z) ParabolicPosition(
|
||||
float startX, float startY, float startZ,
|
||||
float launchAngle, float azimuth,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using CounterDrone.Core.Algorithms;
|
||||
using CounterDrone.Core.Models;
|
||||
|
||||
@ -55,12 +56,19 @@ namespace CounterDrone.Core.Simulation
|
||||
|
||||
if (launchMode == PlatformType.GroundBased)
|
||||
{
|
||||
if (muzzleVelocity <= 0)
|
||||
throw new InvalidOperationException($"地基炮弹 {Id}: MuzzleVelocity={muzzleVelocity} 必须 > 0");
|
||||
if (!launchAngle.HasValue)
|
||||
throw new InvalidOperationException($"地基炮弹 {Id}: LaunchAngle 不能为空,发射角度必须来自方案");
|
||||
|
||||
var dx = targetX - startX;
|
||||
var dz = targetZ - startZ;
|
||||
var horizontalDist = (float)System.Math.Sqrt(dx * dx + dz * dz);
|
||||
_azimuth = horizontalDist > 0.001f ? (float)System.Math.Atan2(dx, dz) : 0;
|
||||
_launchAngle = launchAngle ?? Kinematics.CalculateLaunchAngle(horizontalDist, muzzleVelocity, releaseAltitude);
|
||||
_flightDuration = Kinematics.ParabolicTimeOfFlight(horizontalDist, _launchAngle, muzzleVelocity);
|
||||
_azimuth = (float)System.Math.Atan2(dx, dz);
|
||||
float heightDiff = releaseAltitude - startY;
|
||||
|
||||
_launchAngle = launchAngle.Value;
|
||||
var (_, tof) = Kinematics.ComputeParabolicRange(muzzleVelocity, _launchAngle, heightDiff);
|
||||
_flightDuration = tof;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@ -205,7 +205,17 @@ namespace CounterDrone.Core.Simulation
|
||||
|
||||
if (platform.PlatformType == Models.PlatformType.AirBased && platform.Ready)
|
||||
{
|
||||
platform.CommandFlyTo(fe.TargetX, platform.ReleaseAltitude, fe.TargetZ, fe.FireTime, _disperseHeight);
|
||||
// 空基从当前位置直接发射(与地基相同)
|
||||
platform.Release();
|
||||
var m = new MunitionEntity($"mun_{++_entityCounter}",
|
||||
Models.PlatformType.GroundBased,
|
||||
platform.AerosolType ?? Models.AerosolType.InertGas,
|
||||
platform.PosX, platform.PosY, platform.PosZ,
|
||||
fe.TargetX, fe.TargetY, fe.TargetZ, fe.MuzzleVelocity, _disperseHeight,
|
||||
fe.FireTime, fe.LaunchAngle);
|
||||
_munitions.Add(m);
|
||||
OnMunitionLaunched?.Invoke(m);
|
||||
_frameEventPool.Add(new SimEvent { Type = SimEventType.MunitionLaunched, OccurredAt = fe.FireTime, SourceId = platform.Id, Description = $"空基发射 {m.Id} @({platform.PosX:F0},{platform.PosY:F0},{platform.PosZ:F0})→({fe.TargetX:F0},{fe.TargetY:F0},{fe.TargetZ:F0})" });
|
||||
}
|
||||
else if (platform.Ready)
|
||||
{
|
||||
@ -222,26 +232,6 @@ namespace CounterDrone.Core.Simulation
|
||||
}
|
||||
}
|
||||
|
||||
// 1b. 空基平台到达投放点 → 投弹(手动遍历避免 LINQ)
|
||||
for (int i = 0; i < _platforms.Count; i++)
|
||||
{
|
||||
var platform = _platforms[i];
|
||||
if (!platform.CanRelease) continue;
|
||||
var (cvx, cvy, cvz) = platform.CurrentVelocity;
|
||||
platform.Release();
|
||||
var m = new MunitionEntity($"mun_{++_entityCounter}",
|
||||
Models.PlatformType.AirBased,
|
||||
platform.AerosolType ?? Models.AerosolType.InertGas,
|
||||
platform.PosX, platform.PosY, platform.PosZ,
|
||||
platform.PosX, _disperseHeight, platform.PosZ,
|
||||
0f, _disperseHeight,
|
||||
platform.ExactReleaseTime,
|
||||
cvx, cvy, cvz);
|
||||
_munitions.Add(m);
|
||||
OnMunitionLaunched?.Invoke(m);
|
||||
_frameEventPool.Add(new SimEvent { Type = SimEventType.MunitionLaunched, OccurredAt = platform.ExactReleaseTime, SourceId = platform.Id, Description = $"空基投放 {m.Id}" });
|
||||
}
|
||||
|
||||
// 2. 弹药飞行 → 云团生成(手动遍历+移除索引,避免 ToList())
|
||||
_removalIndices.Clear();
|
||||
for (int i = 0; i < _munitions.Count; i++)
|
||||
|
||||
130
t.txt
Normal file
130
t.txt
Normal file
@ -0,0 +1,130 @@
|
||||
正在确定要还原的项目…
|
||||
所有项目均是最新的,无法还原。
|
||||
CounterDrone.Core -> C:\Users\Tellme\apps\CounterDroneBackend\src\CounterDrone.Core\bin\Debug\netstandard2.1\CounterDrone.Core.dll
|
||||
CounterDrone.Core.Tests -> C:\Users\Tellme\apps\CounterDroneBackend\test\unit\CounterDrone.Core.Tests\bin\Debug\net10.0\CounterDrone.Core.Tests.dll
|
||||
C:\Users\Tellme\apps\CounterDroneBackend\test\unit\CounterDrone.Core.Tests\bin\Debug\net10.0\CounterDrone.Core.Tests.dll (.NETCoreApp,Version=v10.0)的测试运行
|
||||
总共 1 个测试文件与指定模式相匹配。
|
||||
[xUnit.net 00:00:00.94] CounterDrone.Core.Tests.FullPipelineTests.Scenario_AirBased_Windy_PlatformFliesAndDrops [FAIL]
|
||||
失败 CounterDrone.Core.Tests.FullPipelineTests.Scenario_AirBased_Windy_PlatformFliesAndDrops [812 ms]
|
||||
错误消息:
|
||||
Hp=1.000 pos=(20000,500) launched=8
|
||||
# 仿真评估报告
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| 任务名称 | [Demo] 空基拦截-东风5ms |
|
||||
| 任务编号 | SIM-20260617-005 |
|
||||
| 仿真耗时 | 360.0 秒 |
|
||||
| 气溶胶类型 | 惰性气体(吸入式灭火) |
|
||||
|
||||
## 一、仿真结果
|
||||
|
||||
| 统计项 | 值 |
|
||||
|--------|-----|
|
||||
| 无人机总数 | 1 |
|
||||
| 被摧毁 | 0 |
|
||||
| 到达目标 | 1 |
|
||||
| 侵入管控区 | 0 |
|
||||
| 弹药发射 | 8 |
|
||||
| 目标发现 | 4 |
|
||||
| 云团生成 | 8 |
|
||||
|
||||
**对抗结果**:❌ 拦截失败
|
||||
|
||||
## 二、作战环境
|
||||
|
||||
| 参数 | 值 |
|
||||
|------|-----|
|
||||
| 场景类型 | 平原 |
|
||||
| 时间段 | 12:00 |
|
||||
| 天气 | 晴天 |
|
||||
| 风速 | 5.0 m/s |
|
||||
| 风向 | 东 (E) |
|
||||
| 温度 | 25.0°C |
|
||||
| 湿度 | 50%RH |
|
||||
| 气压 | 1013 hPa |
|
||||
| 能见度 | 8000 m |
|
||||
| 场景范围 | 4000 × 10000 × 1000 m (W×L×H) |
|
||||
|
||||
## 三、威胁目标
|
||||
|
||||
| 参数 | 值 |
|
||||
|------|-----|
|
||||
| 类型 | 活塞 |
|
||||
| 动力 | 活塞式 |
|
||||
| 数量 | 1 架 |
|
||||
| 翼展 | 2.5 m |
|
||||
| 速度 | 200 km/h |
|
||||
| 高度 | 500 m |
|
||||
|
||||
## 四、云团抛撒配置
|
||||
|
||||
| 参数 | 值 |
|
||||
|------|-----|
|
||||
| 气溶胶类型 | 惰性气体(吸入式灭火) |
|
||||
| 抛撒高度 | 500 m |
|
||||
| 触发模式 | 时间触发 |
|
||||
| 释放方式 | 单次齐射 |
|
||||
| 扩散方向 | 0° |
|
||||
| 初始规模 | 1000 m³ |
|
||||
| 持续时间 | 60 s |
|
||||
| 释放总量 | 50.0 kg |
|
||||
| 配置来源 | Manual |
|
||||
|
||||
## 五、我方部署
|
||||
|
||||
### 发射平台
|
||||
|
||||
| # | 平台类型 | 位置 (X,Y,Z) | 弹药类型 | 挂载量 | 发射数 |
|
||||
|---|---------|-------------|---------|:------:|:------:|
|
||||
| #1 | 空基 | (6000, 1000, 0) | 惰性气体(吸入式灭火) | 24 | 8 |
|
||||
| #2 | 空基 | (6050, 1000, 0) | 惰性气体(吸入式灭火) | 24 | 0 |
|
||||
| #3 | 空基 | (6100, 1000, 0) | 惰性气体(吸入式灭火) | 24 | 0 |
|
||||
|
||||
## 六、事件时序
|
||||
|
||||
| 时间(s) | 事件 | 详情 | 火力单元 |
|
||||
|---------|------|------|:------:|
|
||||
| 0.40 | 👁️ 目标发现 | 探测设备 det_1 发现 drone drone_1 @(22,500,0) | - |
|
||||
| 85.44 | 🚀 弹药发射 | 第 1 发 · 空基发射 mun_1 @(6000,1000,0)→(6673,500,0) | #1 |
|
||||
| 86.44 | 🚀 弹药发射 | 第 2 发 · 空基发射 mun_3 @(6000,1000,0)→(6673,500,0) | #1 |
|
||||
| 87.44 | 🚀 弹药发射 | 第 3 发 · 空基发射 mun_5 @(6000,1000,0)→(6673,500,0) | #1 |
|
||||
| 88.44 | 🚀 弹药发射 | 第 4 发 · 空基发射 mun_7 @(6000,1000,0)→(6673,500,0) | #1 |
|
||||
| 89.44 | 🚀 弹药发射 | 第 5 发 · 空基发射 mun_9 @(6000,1000,0)→(6673,500,0) | #1 |
|
||||
| 90.44 | 🚀 弹药发射 | 第 6 发 · 空基发射 mun_11 @(6000,1000,0)→(6673,500,0) | #1 |
|
||||
| 91.44 | 🚀 弹药发射 | 第 7 发 · 空基发射 mun_13 @(6000,1000,0)→(6673,500,0) | #1 |
|
||||
| 92.44 | 🚀 弹药发射 | 第 8 发 · 空基发射 mun_15 @(6000,1000,0)→(6673,500,0) | #1 |
|
||||
| 93.85 | ☁️ 云团生成 | 云团 #1 · 云团生成 @(-1594000,500,0) | - |
|
||||
| 94.85 | ☁️ 云团生成 | 云团 #2 · 云团生成 @(-1594000,500,0) | - |
|
||||
| 95.85 | ☁️ 云团生成 | 云团 #3 · 云团生成 @(-1594000,500,0) | - |
|
||||
| 96.85 | ☁️ 云团生成 | 云团 #4 · 云团生成 @(-1594000,500,0) | - |
|
||||
| 97.85 | ☁️ 云团生成 | 云团 #5 · 云团生成 @(-1594000,500,0) | - |
|
||||
| 98.85 | ☁️ 云团生成 | 云团 #6 · 云团生成 @(-1594000,500,0) | - |
|
||||
| 99.85 | ☁️ 云团生成 | 云团 #7 · 云团生成 @(-1594000,500,0) | - |
|
||||
| 100.85 | ☁️ 云团生成 | 云团 #8 · 云团生成 @(-1594000,500,0) | - |
|
||||
| 109.60 | 👁️ 目标发现 | 探测设备 det_1 发现 drone drone_1 @(6089,500,0) | - |
|
||||
| 110.80 | 👁️ 目标发现 | 探测设备 det_2 发现 drone drone_1 @(6156,500,0) | - |
|
||||
| 111.60 | 👁️ 目标发现 | 探测设备 det_3 发现 drone drone_1 @(6200,500,0) | - |
|
||||
| 360.00 | 🏁 抵达目标 | 到达攻击目标 | - |
|
||||
| 360.00 | ⏹️ 仿真结束 | | - |
|
||||
|
||||
## 七、时间线
|
||||
|
||||
| 阶段 | 时间 | 耗时 |
|
||||
|------|------|------|
|
||||
| 首发发射 | 85.4s | - |
|
||||
| 末发发射 | 92.4s | 7.0s(弹群展开) |
|
||||
| 发射 → 云团生成 | 85.4s → 93.9s | 8.4s(弹药飞行) |
|
||||
| 总耗时 | 0 → 360.0s | 360.0s |
|
||||
|
||||
**统计汇总**:
|
||||
- 弹药发射 8 发,云团生成 8 朵,击毁 0 架
|
||||
- 发射窗口 7.0s,弹药飞行 8.4s
|
||||
|
||||
|
||||
堆栈跟踪:
|
||||
at CounterDrone.Core.Tests.FullPipelineTests.Scenario_AirBased_Windy_PlatformFliesAndDrops() in C:\Users\Tellme\apps\CounterDroneBackend\test\unit\CounterDrone.Core.Tests\FullPipelineTests.cs:line 157
|
||||
at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
|
||||
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
|
||||
|
||||
失败! - 失败: 1,通过: 0,已跳过: 0,总计: 1,持续时间: 822 ms - CounterDrone.Core.Tests.dll (net10.0)
|
||||
@ -102,7 +102,7 @@ namespace CounterDrone.Core.Tests
|
||||
[Fact]
|
||||
public void Ground_FireTime_VariesPerUnitPosition()
|
||||
{
|
||||
// 两个单元不同位置 → 飞行时间不同 → 发射时间不同
|
||||
// 不同位置 → InterceptCalculator 给出不同拦截弧/时间 → 发射时刻不同
|
||||
var u0 = MakeGroundUnit("u0", 5000);
|
||||
var u1 = MakeGroundUnit("u1", 8000);
|
||||
var threat = MakeThreat(speed: 120);
|
||||
@ -110,9 +110,20 @@ namespace CounterDrone.Core.Tests
|
||||
var r0 = Plan(new() { u0 }, threat);
|
||||
var r1 = Plan(new() { u1 }, threat);
|
||||
|
||||
// 两个位置的 plan 都应有有效发射事件
|
||||
Assert.NotEmpty(r0.Best.MergedSchedule);
|
||||
Assert.NotEmpty(r1.Best.MergedSchedule);
|
||||
|
||||
float t0 = r0.Best.MergedSchedule[0].FireTime;
|
||||
float t1 = r1.Best.MergedSchedule[0].FireTime;
|
||||
Assert.NotEqual(t0, t1); // 位置不同 → 弹道不同 → 发射时刻不同
|
||||
|
||||
// FireTime 必须 > 0(在可发射窗口内)
|
||||
Assert.True(t0 > 0, $"u0 FireTime={t0:F1} 应>0");
|
||||
Assert.True(t1 > 0, $"u1 FireTime={t1:F1} 应>0");
|
||||
|
||||
// 两个位置的 launchAngle 都来自 InterceptCalculator,应为正值
|
||||
Assert.True(r0.Best.MergedSchedule[0].LaunchAngle > 0);
|
||||
Assert.True(r1.Best.MergedSchedule[0].LaunchAngle > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -205,9 +216,9 @@ namespace CounterDrone.Core.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AirBased_MuzzleVelocity_Zero()
|
||||
public void AirBased_MuzzleVelocity_Equals_CruiseSpeed()
|
||||
=> Assert.All(Plan(new() { MakeAirUnit("u0", 3000) }, MakeThreat()).Best.MergedSchedule,
|
||||
fe => Assert.Equal(0f, fe.MuzzleVelocity));
|
||||
fe => Assert.Equal(55f, fe.MuzzleVelocity));
|
||||
|
||||
[Fact]
|
||||
public void AirBased_TargetX_OnRoute()
|
||||
|
||||
@ -7,48 +7,17 @@ namespace CounterDrone.Core.Tests
|
||||
{
|
||||
public class InterceptCalculatorAirTests
|
||||
{
|
||||
private static List<Waypoint> R(float x0, float x1, float y, float kph)
|
||||
=> new() {
|
||||
new() { PosX = x0, PosY = y, PosZ = 0, Speed = kph },
|
||||
new() { PosX = x1, PosY = y, PosZ = 0, Speed = kph },
|
||||
[Fact]
|
||||
public void Print_Result()
|
||||
{
|
||||
var route = new List<Waypoint> {
|
||||
new() { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 },
|
||||
new() { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 },
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void Smoke_ReturnsPositive()
|
||||
{
|
||||
// 无人机 200km/h, 0→20km, Y=500
|
||||
// 平台(2000,1000,0), cruise=80, R+exp=35
|
||||
var route = R(0, 20000, 500, 200);
|
||||
var unit = new Vector3(2000, 1000, 0);
|
||||
var (arc, _, _) = InterceptCalculator.ComputeHorizontal(
|
||||
route, 0, 200, 35, unit, 80, 1000, 500);
|
||||
|
||||
Assert.True(arc > 0, $"arc={arc:F0}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KnownSolution_VerifiesTiming()
|
||||
{
|
||||
// 手算验证时间一致性
|
||||
var route = R(0, 20000, 500, 200);
|
||||
var unit = new Vector3(2000, 1000, 0);
|
||||
var (arc, _, _) = InterceptCalculator.ComputeHorizontal(
|
||||
route, 0, 200, 35, unit, 80, 1000, 500);
|
||||
|
||||
if (arc > 0)
|
||||
{
|
||||
float vd = 200f / 3.6f;
|
||||
float tf = MathF.Sqrt(2f * 500f / 9.81f); // ~10.1s
|
||||
float delay = 35f + tf;
|
||||
float droneTime = (arc - 0) / vd;
|
||||
float releaseX = arc - 80 * tf;
|
||||
float flightDist = MathF.Abs(releaseX - 2000);
|
||||
float platformTime = flightDist / 80;
|
||||
float total = delay + platformTime;
|
||||
|
||||
Assert.True(MathF.Abs(droneTime - total) < 1f,
|
||||
$"drone={droneTime:F1} total={total:F1} arc={arc:F0} releaseX={releaseX:F0}");
|
||||
}
|
||||
var unit = new Vector3(6000, 1000, 0); // 平台实际位置
|
||||
var (arc, ts, angle) = InterceptCalculator.ComputeHorizontal(
|
||||
route, 0, 200, 32, unit, 200, 1500, 500);
|
||||
Assert.True(false, $"arc={arc:F0}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,6 +121,275 @@ namespace CounterDrone.Core.Tests
|
||||
Assert.Equal(0f, z, 1);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 输入验证:非法参数必须抛出异常
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void CalculateLaunchAngle_ThrowsOnInvalidInputs()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => Kinematics.CalculateLaunchAngle(0, 300, 0));
|
||||
Assert.Throws<ArgumentException>(() => Kinematics.CalculateLaunchAngle(-1, 300, 0));
|
||||
Assert.Throws<ArgumentException>(() => Kinematics.CalculateLaunchAngle(5000, 0, 0));
|
||||
Assert.Throws<ArgumentException>(() => Kinematics.CalculateLaunchAngle(5000, -10, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateLaunchAngle_ThrowsOnUnreachableTarget()
|
||||
{
|
||||
// 300m/s 初速不可能击中 10000m 外 5000m 高的目标
|
||||
Assert.Throws<ArgumentException>(() => Kinematics.CalculateLaunchAngle(10000f, 300f, 5000f));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParabolicShellTime_ThrowsOnInvalidInputs()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => Kinematics.ParabolicShellTime(0, 0, 300));
|
||||
Assert.Throws<ArgumentException>(() => Kinematics.ParabolicShellTime(5000, 0, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParabolicShellTime_ThrowsOnUnreachableTarget()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => Kinematics.ParabolicShellTime(10000f, 5000f, 300f));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParabolicTimeOfFlight_ThrowsOnInvalidInputs()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => Kinematics.ParabolicTimeOfFlight(0, 0.5f, 300));
|
||||
Assert.Throws<ArgumentException>(() => Kinematics.ParabolicTimeOfFlight(-1, 0.5f, 300));
|
||||
Assert.Throws<ArgumentException>(() => Kinematics.ParabolicTimeOfFlight(5000, 0.5f, 0));
|
||||
Assert.Throws<ArgumentException>(() => Kinematics.ParabolicTimeOfFlight(5000, 0.5f, -10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeParabolicRange_ThrowsOnInvalidInputs()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => Kinematics.ComputeParabolicRange(0, 0.5f, 0));
|
||||
Assert.Throws<ArgumentException>(() => Kinematics.ComputeParabolicRange(-10, 0.5f, 0));
|
||||
Assert.Throws<ArgumentException>(() => Kinematics.ComputeParabolicRange(300, -0.1f, 0));
|
||||
Assert.Throws<ArgumentException>(() => Kinematics.ComputeParabolicRange(300, (float)System.Math.PI / 2, 0));
|
||||
Assert.Throws<ArgumentException>(() => Kinematics.ComputeParabolicRange(300, (float)System.Math.PI, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeParabolicRange_ThrowsOnUnreachableHeight()
|
||||
{
|
||||
// 300m/s 45° 不可能达到 5000m 高度的目标
|
||||
float angle45 = (float)(System.Math.PI / 4);
|
||||
Assert.Throws<ArgumentException>(() => Kinematics.ComputeParabolicRange(300f, angle45, 5000f));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 顶点计算
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void ParabolicApex_45Degree_MatchesStandardFormula()
|
||||
{
|
||||
float v0 = 300f, g = 9.81f;
|
||||
float angle45 = (float)(System.Math.PI / 4);
|
||||
float expectedH = v0 * v0 * (float)System.Math.Sin(angle45) * (float)System.Math.Sin(angle45) / (2f * g);
|
||||
float expectedT = v0 * (float)System.Math.Sin(angle45) / g;
|
||||
|
||||
var (apexH, apexT) = Kinematics.ParabolicApex(v0, angle45);
|
||||
Assert.True(System.Math.Abs(apexH - expectedH) < 0.1f, $"H={apexH:F2} expected={expectedH:F2}");
|
||||
Assert.True(System.Math.Abs(apexT - expectedT) < 0.001f, $"T={apexT:F4} expected={expectedT:F4}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParabolicApex_30Degree_Then_ParabolicPosition_AtApexTime_ReturnsApexHeight()
|
||||
{
|
||||
float v0 = 500f;
|
||||
float angle = 30f * (float)(System.Math.PI / 180f);
|
||||
|
||||
var (apexH, apexT) = Kinematics.ParabolicApex(v0, angle);
|
||||
|
||||
// ParabolicPosition 在顶点时刻的高度应 = apexH
|
||||
var (_, y, _) = Kinematics.ParabolicPosition(0, 0, 0, angle, 0, v0, apexT);
|
||||
Assert.True(System.Math.Abs(y - apexH) < 0.1f, $"y={y:F2} apex={apexH:F2}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParabolicApex_ThrowsOnInvalidInputs()
|
||||
{
|
||||
float angle = 0.5f;
|
||||
Assert.Throws<ArgumentException>(() => Kinematics.ParabolicApex(0, angle));
|
||||
Assert.Throws<ArgumentException>(() => Kinematics.ParabolicApex(-10, angle));
|
||||
Assert.Throws<ArgumentException>(() => Kinematics.ParabolicApex(300, -0.1f));
|
||||
Assert.Throws<ArgumentException>(() => Kinematics.ParabolicApex(300, (float)System.Math.PI / 2));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 正问题:给定初速和发射角 → 计算射程和飞行时间
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void ComputeParabolicRange_FlatGround_MatchesStandardFormula()
|
||||
{
|
||||
// 45° 平地: R = v²/g, T = 2v·sin45°/g
|
||||
float v0 = 300f, g = 9.81f;
|
||||
float angle45 = (float)(System.Math.PI / 4);
|
||||
float expectedR = v0 * v0 / g;
|
||||
float expectedT = 2f * v0 * (float)System.Math.Sin(angle45) / g;
|
||||
|
||||
var (range, time) = Kinematics.ComputeParabolicRange(v0, angle45, 0f);
|
||||
Assert.True(System.Math.Abs(range - expectedR) < 1f, $"R={range:F1} expected={expectedR:F1}");
|
||||
Assert.True(System.Math.Abs(time - expectedT) < 0.1f, $"T={time:F3} expected={expectedT:F3}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeParabolicRange_30Degree_FlatGround()
|
||||
{
|
||||
float v0 = 500f, g = 9.81f;
|
||||
float angle30 = (float)(System.Math.PI / 6);
|
||||
// R = v²·sin(2θ)/g, T = 2v·sinθ/g
|
||||
float expectedR = v0 * v0 * (float)System.Math.Sin(2 * angle30) / g;
|
||||
float expectedT = 2f * v0 * (float)System.Math.Sin(angle30) / g;
|
||||
|
||||
var (range, time) = Kinematics.ComputeParabolicRange(v0, angle30, 0f);
|
||||
Assert.True(System.Math.Abs(range - expectedR) < 1f, $"R={range:F1} expected={expectedR:F1}");
|
||||
Assert.True(System.Math.Abs(time - expectedT) < 0.1f, $"T={time:F3} expected={expectedT:F3}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeParabolicRange_ElevatedTarget_VerifiableByPosition()
|
||||
{
|
||||
// v₀=800, θ=10°, 目标在发射点上方 500m
|
||||
float v0 = 800f, heightDiff = 500f;
|
||||
float angle = 10f * (float)System.Math.PI / 180f;
|
||||
|
||||
var (range, time) = Kinematics.ComputeParabolicRange(v0, angle, heightDiff);
|
||||
|
||||
// 用 ParabolicPosition 验证:在 time 时刻应到达 (range, heightDiff)
|
||||
var (x, y, z) = Kinematics.ParabolicPosition(0, 0, 0, angle, 0, v0, time);
|
||||
// X 方向(azimuth=0 → cos(0) → +Z 方向)
|
||||
// 实际水平距离 = sqrt(x²+z²) = z(因为 azimuth=0)
|
||||
float actualDist = (float)System.Math.Sqrt(x * x + z * z);
|
||||
Assert.True(System.Math.Abs(actualDist - range) < 5f,
|
||||
$"水平距离={actualDist:F1} expected={range:F1}");
|
||||
Assert.True(System.Math.Abs(y - heightDiff) < 5f,
|
||||
$"高度={y:F1} expected={heightDiff:F1}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeParabolicRange_DepressedTarget_VerifiableByPosition()
|
||||
{
|
||||
// v₀=500, θ=5°, 目标在发射点下方 200m
|
||||
float v0 = 500f, heightDiff = -200f;
|
||||
float angle = 5f * (float)System.Math.PI / 180f;
|
||||
|
||||
var (range, time) = Kinematics.ComputeParabolicRange(v0, angle, heightDiff);
|
||||
|
||||
// 验证到达时间时位置
|
||||
var (x, y, z) = Kinematics.ParabolicPosition(0, 0, 0, angle, 0, v0, time);
|
||||
float actualDist = (float)System.Math.Sqrt(x * x + z * z);
|
||||
Assert.True(System.Math.Abs(actualDist - range) < 5f,
|
||||
$"水平距离={actualDist:F1} expected={range:F1}");
|
||||
Assert.True(System.Math.Abs(y - heightDiff) < 5f,
|
||||
$"高度={y:F1} expected={heightDiff:F1}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeParabolicRange_5DegreeLongRange()
|
||||
{
|
||||
// v₀=800, θ=5°, 水平射程
|
||||
float v0 = 800f, g = 9.81f;
|
||||
float angle = 5f * (float)System.Math.PI / 180f;
|
||||
float cosA = (float)System.Math.Cos(angle);
|
||||
float sinA = (float)System.Math.Sin(angle);
|
||||
|
||||
// 平坦地面: R = v²sin(2θ)/g
|
||||
float expectedR = v0 * v0 * (float)System.Math.Sin(2 * angle) / g;
|
||||
float expectedT = 2f * v0 * sinA / g;
|
||||
|
||||
var (range, time) = Kinematics.ComputeParabolicRange(v0, angle, 0f);
|
||||
Assert.True(System.Math.Abs(range - expectedR) < 1f);
|
||||
Assert.True(System.Math.Abs(time - expectedT) < 0.1f);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 往返验证:正问题 → 逆问题 应还原一致
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Roundtrip_ComputeRange_Then_CalculateAngle_ReturnsSameAngle()
|
||||
{
|
||||
float v0 = 500f, hDiff = 100f;
|
||||
float originalAngle = 15f * (float)System.Math.PI / 180f;
|
||||
|
||||
// 正问题:给定角度,计算射程
|
||||
var (range, _) = Kinematics.ComputeParabolicRange(v0, originalAngle, hDiff);
|
||||
|
||||
// 逆问题:给定射程,反算角度
|
||||
float recoveredAngle = Kinematics.CalculateLaunchAngle(range, v0, hDiff);
|
||||
|
||||
float diffDeg = System.Math.Abs(originalAngle - recoveredAngle) * 180f / (float)System.Math.PI;
|
||||
Assert.True(diffDeg < 0.1f,
|
||||
$"原始={originalAngle * 180 / System.Math.PI:F3}° 还原={recoveredAngle * 180 / System.Math.PI:F3}° 差={diffDeg:F4}°");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Roundtrip_CalculateAngle_Then_ComputeRange_ReturnsSameRange()
|
||||
{
|
||||
// 用平地场景:低弹道解在平地也走下行段,往返一致
|
||||
float R = 5000f, v0 = 300f, hDiff = 0f;
|
||||
|
||||
float angle = Kinematics.CalculateLaunchAngle(R, v0, hDiff);
|
||||
var (recoveredRange, _) = Kinematics.ComputeParabolicRange(v0, angle, hDiff);
|
||||
|
||||
Assert.True(System.Math.Abs(recoveredRange - R) < 1f,
|
||||
$"原始 R={R:F1} 还原 R={recoveredRange:F1}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Roundtrip_ElevatedTarget_UsesHighAngleSolution()
|
||||
{
|
||||
// 靶点高于发射点时,低弹道解在上升段命中,
|
||||
// ComputeParabolicRange 取下行段根,因此应选高弹道解才能往返一致
|
||||
float R = 5000f, v0 = 800f, hDiff = 500f;
|
||||
|
||||
// 手动取高弹道解(+ 号),而非 CalculateLaunchAngle 的低弹道解
|
||||
float a = 0.5f * 9.81f * R * R / (v0 * v0);
|
||||
float disc = R * R - 4f * a * (hDiff + a);
|
||||
float tanHigh = (R + (float)System.Math.Sqrt(disc)) / (2f * a);
|
||||
float highAngle = (float)System.Math.Atan(tanHigh);
|
||||
|
||||
var (recoveredRange, _) = Kinematics.ComputeParabolicRange(v0, highAngle, hDiff);
|
||||
|
||||
Assert.True(System.Math.Abs(recoveredRange - R) < 5f,
|
||||
$"高弹道: 原始 R={R:F1} 还原 R={recoveredRange:F1}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeParabolicRange_Then_ParabolicPosition_IsOnTrajectory()
|
||||
{
|
||||
// 验证整条轨迹:多时间点采样,确保 ParabolicPosition 与 ComputeParabolicRange 一致
|
||||
float v0 = 400f;
|
||||
float angle = 25f * (float)System.Math.PI / 180f;
|
||||
float cosA = (float)System.Math.Cos(angle);
|
||||
float sinA = (float)System.Math.Sin(angle);
|
||||
float g = 9.81f;
|
||||
|
||||
var (totalRange, totalTime) = Kinematics.ComputeParabolicRange(v0, angle, 0f);
|
||||
|
||||
// 在 10 个等间隔时间点采样
|
||||
for (int i = 0; i <= 10; i++)
|
||||
{
|
||||
float t = totalTime * i / 10f;
|
||||
var (x, y, z) = Kinematics.ParabolicPosition(0, 0, 0, angle, 0, v0, t);
|
||||
|
||||
float expectedX = 0;
|
||||
float expectedZ = v0 * cosA * t;
|
||||
float expectedY = v0 * sinA * t - 0.5f * g * t * t;
|
||||
|
||||
Assert.True(System.Math.Abs(x - expectedX) < 0.1f, $"t={t:F2}: X");
|
||||
Assert.True(System.Math.Abs(z - expectedZ) < 0.1f, $"t={t:F2}: Z");
|
||||
Assert.True(System.Math.Abs(y - expectedY) < 0.1f, $"t={t:F2}: Y");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PointInPolygon_Inside_ReturnsTrue()
|
||||
{
|
||||
|
||||
@ -1,53 +1,19 @@
|
||||
using CounterDrone.Core.Algorithms;
|
||||
using CounterDrone.Core.Models;
|
||||
using CounterDrone.Core.Simulation;
|
||||
using Xunit;
|
||||
|
||||
namespace CounterDrone.Core.Tests
|
||||
{
|
||||
public class MunitionEntityTests
|
||||
{
|
||||
[Fact]
|
||||
public void GroundBased_DoesNotArriveImmediately()
|
||||
{
|
||||
var m = new MunitionEntity("m1", PlatformType.GroundBased,
|
||||
AerosolType.InertGas,
|
||||
startX: 0, startY: 0, startZ: 0,
|
||||
targetX: 5000, targetY: 300, targetZ: 0,
|
||||
muzzleVelocity: 500f, releaseAltitude: 300f, launchTime: 0f);
|
||||
|
||||
m.Update(0.05f);
|
||||
Assert.False(m.HasArrived, "炮弹不能第一帧就到达");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GroundBased_ArrivesNearTarget_WithInterpolation()
|
||||
{
|
||||
var m = new MunitionEntity("m2", PlatformType.GroundBased,
|
||||
AerosolType.InertGas,
|
||||
startX: 5000, startY: 0, startZ: 50,
|
||||
targetX: 10000, targetY: 500, targetZ: 0,
|
||||
muzzleVelocity: 800f, releaseAltitude: 500f, launchTime: 143.69f);
|
||||
|
||||
float time = Kinematics.ParabolicShellTime(5000.25f, 500f, 800f);
|
||||
for (float t = 0; t < time + 1f; t += 0.4f)
|
||||
{
|
||||
m.Update(0.4f);
|
||||
if (m.HasArrived) break;
|
||||
}
|
||||
Assert.True(m.HasArrived, "应到达");
|
||||
Assert.Equal(500f, m.PosY, 5f);
|
||||
Assert.True(System.Math.Abs(m.PosX - 10000) < 200f, $"X={m.PosX:F1} 应≈10000");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// 抛物线运动学:固定角度验证
|
||||
// 抛物线运动学:方位角和位置的对应关系
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Parabolic_45Deg_Azimuth0_MovesInZ()
|
||||
{
|
||||
// ParabolicPosition: azimuth=0 → +Z方向, sin(0)=0→X, cos(0)=1→Z
|
||||
// azimuth=0 → +Z方向, sin(0)=0→X, cos(0)=1→Z
|
||||
float angle45 = 45f * (float)System.Math.PI / 180f;
|
||||
var (x, y, z) = Kinematics.ParabolicPosition(0, 0, 0, angle45, 0, 100, 1f);
|
||||
Assert.True(System.Math.Abs(x) < 0.1f, $"X={x:F1} ≈0");
|
||||
@ -58,7 +24,6 @@ namespace CounterDrone.Core.Tests
|
||||
[Fact]
|
||||
public void Parabolic_30Deg_Azimuth90_MovesInX()
|
||||
{
|
||||
// azimuth=90°(π/2): sin(π/2)=1→X, cos(π/2)=0→Z
|
||||
float angle30 = 30f * (float)System.Math.PI / 180f;
|
||||
float az90 = 90f * (float)System.Math.PI / 180f;
|
||||
var (x, y, z) = Kinematics.ParabolicPosition(0, 0, 0, angle30, az90, 100, 1f);
|
||||
@ -69,62 +34,11 @@ namespace CounterDrone.Core.Tests
|
||||
[Fact]
|
||||
public void Parabolic_60Deg_Azimuth270_MovesBackward()
|
||||
{
|
||||
// azimuth=270°(3π/2): sin=-1→X方向, cos=0→Z
|
||||
float angle60 = 60f * (float)System.Math.PI / 180f;
|
||||
float az270 = 270f * (float)System.Math.PI / 180f;
|
||||
var (x, y, z) = Kinematics.ParabolicPosition(0, 0, 0, angle60, az270, 100, 1f);
|
||||
Assert.True(System.Math.Abs(x + 50f) < 1f, $"X={x:F1} ≈-50");
|
||||
Assert.True(System.Math.Abs(z) < 0.1f, $"Z={z:F1} ≈0");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GroundBased_LaunchAngle_ReachesReleaseAltitude()
|
||||
{
|
||||
// 验证运动学:给定距离和初速,弹道能达到释放高度
|
||||
var range = 5000f;
|
||||
var v0 = 500f;
|
||||
var releaseAlt = 300f;
|
||||
var angle = Kinematics.CalculateLaunchAngle(range, v0, releaseAlt);
|
||||
|
||||
// 弹道顶点高度 >= 释放高度
|
||||
var peakH = v0 * v0 * (float)System.Math.Sin(angle) * (float)System.Math.Sin(angle) / (2f * 9.81f);
|
||||
Assert.True(peakH >= releaseAlt,
|
||||
$"弹道顶点 {peakH:F0}m ≥ 释放高度 {releaseAlt}m, angle={angle * 180 / System.Math.PI:F1}°");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GroundBased_MovesForwardOverTime()
|
||||
{
|
||||
var m = new MunitionEntity("m1", PlatformType.GroundBased,
|
||||
AerosolType.InertGas,
|
||||
startX: 0, startY: 0, startZ: 0,
|
||||
targetX: 1000, targetY: 300, targetZ: 0,
|
||||
muzzleVelocity: 500f, releaseAltitude: 300f, launchTime: 0f);
|
||||
|
||||
m.Update(1f);
|
||||
var dist = System.Math.Sqrt(m.PosX * m.PosX + m.PosZ * m.PosZ);
|
||||
Assert.True(dist > 100f, "1秒后炮弹应水平移动 > 100m");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AirBased_DropsToReleaseAltitude()
|
||||
{
|
||||
var m = new MunitionEntity("m1", PlatformType.AirBased,
|
||||
AerosolType.InertGas,
|
||||
startX: 0, startY: 500, startZ: 0,
|
||||
targetX: 1000, targetY: 300, targetZ: 0,
|
||||
muzzleVelocity: 0, releaseAltitude: 300f, launchTime: 0f);
|
||||
|
||||
// 空基投放:自由落体 500m → 300m = 200m 下落
|
||||
// h = 0.5*g*t² → t = sqrt(2*200/9.81) ≈ 6.4s
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
m.Update(0.05f);
|
||||
if (m.HasArrived) break;
|
||||
}
|
||||
|
||||
Assert.True(m.HasArrived, "空基弹药应到达释放高度");
|
||||
Assert.True(m.PosY <= 300f + 1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,7 +157,7 @@ namespace CounterDrone.Core.Tests
|
||||
_scenarioService.SaveCloudDispersal(task.Id, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 300 });
|
||||
_scenarioService.SaveRoute(task.Id, "d", new RoutePlan(), new() { new() { PosX = 0, PosY = 300, PosZ = 0, Speed = 60 }, new() { PosX = 5000, PosY = 300, PosZ = 0, Speed = 60 } });
|
||||
|
||||
_engine.SetFireSchedule(new List<FireEvent> { new() { FireTime = 0.1f, PlatformIndex = 0, TargetX = 2500, TargetY = 300, TargetZ = 0 } });
|
||||
_engine.SetFireSchedule(new List<FireEvent> { new() { FireTime = 0.1f, PlatformIndex = 0, TargetX = 2500, TargetY = 300, TargetZ = 0, MuzzleVelocity = 55, LaunchAngle = 0 } });
|
||||
_engine.Initialize(task.Id);
|
||||
_engine.TimeScale = 10f;
|
||||
|
||||
@ -178,7 +178,7 @@ namespace CounterDrone.Core.Tests
|
||||
public void GroundBased_LaunchesWithCorrectDescription()
|
||||
{
|
||||
SetupScenario();
|
||||
_engine.SetFireSchedule(new List<FireEvent> { new() { FireTime = 0.1f, PlatformIndex = 0, TargetX = 1000, TargetY = 300, TargetZ = 0, MuzzleVelocity = 800 } });
|
||||
_engine.SetFireSchedule(new List<FireEvent> { new() { FireTime = 0.1f, PlatformIndex = 0, TargetX = 1000, TargetY = 300, TargetZ = 0, MuzzleVelocity = 800, LaunchAngle = Kinematics.CalculateLaunchAngle(1000f, 800f, 300f) } });
|
||||
_engine.Initialize(_taskId);
|
||||
|
||||
SimEvent launch = null;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user