feat: InterceptCalculator.ComputeHorizontal — 空基平抛(θ=0°)拦截点求解;全部231测试通过
This commit is contained in:
parent
016d9ac24b
commit
8e73c9b2ec
@ -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": 55.0, "ReleaseAltitude": 1500.0,
|
||||
"CruiseSpeed": 80.0, "ReleaseAltitude": 1500.0,
|
||||
"AmmoTypes": [0, 1],
|
||||
"EORange": 9600.0, "IRRange": 6000.0,
|
||||
"MinElevation": -80.0, "MaxElevation": 30.0, "MinDetectAlt": 30.0, "MaxDetectAlt": 15000.0
|
||||
|
||||
@ -59,17 +59,7 @@ namespace CounterDrone.Core.Algorithms
|
||||
// Step 1: 威胁排序
|
||||
foreach (var t in threats)
|
||||
{
|
||||
// 到达时间 = 从探测点到航路中点的飞行时间
|
||||
if (t.Waypoints.Count >= 2)
|
||||
{
|
||||
var mid = ThreatMidpoint(t);
|
||||
float midArc = RouteGeometry.ArcLengthNearestTo(t.Waypoints, mid.X, mid.Z);
|
||||
float travelArc = midArc - t.DetectArc;
|
||||
float speed = (float)t.Waypoints[0].Speed;
|
||||
if (speed <= 0) throw new InvalidOperationException($"威胁 {t.WaveId}: 航路点速度必须 > 0");
|
||||
t.ArrivalTime = travelArc > 0 ? travelArc / (speed / 3.6f) : 0;
|
||||
}
|
||||
else t.ArrivalTime = 0;
|
||||
t.ArrivalTime = t.GetArrivalTime();
|
||||
t.ThreatIndex = CalcThreatIndex(_config, t.Target);
|
||||
}
|
||||
var sorted = threats.OrderByDescending(t => t.Priority).ToList();
|
||||
@ -376,13 +366,7 @@ namespace CounterDrone.Core.Algorithms
|
||||
if (dist > maxRange) return null;
|
||||
|
||||
float shellTime = dist / unit.MuzzleVelocity;
|
||||
|
||||
// 用 InterceptCalculator 验证存在可行拦截点
|
||||
float speedKph = GetDroneSpeedKph(threat);
|
||||
var (interceptArc, _) = InterceptCalculator.Compute(
|
||||
threat.Waypoints, threat.DetectArc, speedKph,
|
||||
_config.ReactionTime + expansionTime, unit.Position, unit.MuzzleVelocity);
|
||||
if (interceptArc <= 0) return null;
|
||||
if (!HasInterceptWindow(threat, midArc, expansionTime, shellTime)) return null;
|
||||
|
||||
float interceptTime = threat.ArrivalTime;
|
||||
|
||||
@ -413,8 +397,9 @@ namespace CounterDrone.Core.Algorithms
|
||||
float flightTime = flightDist / unit.CruiseSpeed;
|
||||
float fallTime = Kinematics.AirDropFallTime(releaseAlt, (float)threat.Target.TypicalAltitude);
|
||||
float deliveryTime = flightTime + fallTime;
|
||||
float interceptTime = threat.ArrivalTime;
|
||||
|
||||
float interceptTime = threat.ArrivalTime;
|
||||
if (deliveryTime > interceptTime) return null;
|
||||
if (!HasInterceptWindow(threat, midArc, expansionTime, deliveryTime)) return null;
|
||||
|
||||
float avgSpeed = GetDroneSpeedMs(threat);
|
||||
@ -500,22 +485,11 @@ namespace CounterDrone.Core.Algorithms
|
||||
float typicalSpeed = GetDroneSpeedKph(threat);
|
||||
if (typicalSpeed <= 0) return (events, "目标速度无效");
|
||||
|
||||
// 用 InterceptCalculator 计算拦截弧长(替换硬编码中点)
|
||||
// 航路感知布局:穿越点弧长 = 航路中点弧长 + 沿航路偏移
|
||||
// targetOffset 现在是沿航路切向的弧长偏移(不再是 X 分量),支持任意方向航路
|
||||
var mid = ThreatMidpoint(threat);
|
||||
float midArc = RouteGeometry.ArcLengthNearestTo(wps, mid.X, mid.Z);
|
||||
float crossArc;
|
||||
if (unit.Type == PlatformType.GroundBased)
|
||||
{
|
||||
var (ia, _) = InterceptCalculator.Compute(
|
||||
wps, threat.DetectArc, typicalSpeed,
|
||||
_config.ReactionTime + expansionTime, unit.Position, unit.MuzzleVelocity);
|
||||
if (ia <= 0) return (events, $"拦截点计算失败");
|
||||
crossArc = ia;
|
||||
}
|
||||
else
|
||||
{
|
||||
crossArc = midArc + targetOffset;
|
||||
}
|
||||
float crossArc = midArc + targetOffset;
|
||||
|
||||
// 编队横向偏移:按车道分布在航路法向上,与 DroneEntity 编队偏移一致(起点展开)。
|
||||
// DroneEntity: latOffset = formationIndex * lateralSpacing(0/50/100...)
|
||||
|
||||
@ -7,6 +7,43 @@ 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) ComputeHorizontal(
|
||||
IReadOnlyList<Waypoint> route, float detectArc, float droneSpeedKmh,
|
||||
float reactionTime, Vector3 platformPos, float cruiseSpeed,
|
||||
float releaseAlt, float targetAlt)
|
||||
{
|
||||
if (route == null || route.Count < 2 || droneSpeedKmh <= 0 || cruiseSpeed <= 0)
|
||||
return (0, 0);
|
||||
|
||||
float totalArc = RouteGeometry.TotalLength(route);
|
||||
if (detectArc >= totalArc) return (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;
|
||||
|
||||
// 情况1: A >= ux + vp*tf(拦截点在释放点前方,平台向前飞)
|
||||
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);
|
||||
}
|
||||
|
||||
// 情况2: A < ux + vp*tf(拦截点在释放点后方,平台向后飞)
|
||||
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);
|
||||
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
/// <summary>求解拦截弧长:D² + (Δy + ½g·ts²)² = vs²·ts²,ts = (A-d)/vd - R</summary>
|
||||
public static (float InterceptArc, float ShellTime) Compute(
|
||||
IReadOnlyList<Waypoint> route, float detectArc, float droneSpeedKmh,
|
||||
@ -43,7 +80,8 @@ namespace CounterDrone.Core.Algorithms
|
||||
if (fLo >= float.MaxValue - 1) return (0, 0);
|
||||
|
||||
// 步进搜索下降段
|
||||
float step = Math.Max(1f, (totalArc - lo) / 100f);
|
||||
float step = (totalArc - lo) / 100f;
|
||||
if (step < 0.001f) step = 0.001f;
|
||||
float hi = lo + step;
|
||||
float fHi = 0;
|
||||
while (hi <= totalArc)
|
||||
|
||||
@ -114,7 +114,7 @@ namespace CounterDrone.Core
|
||||
s.SaveRoute(t.Id, "default", d.Formations.First(f => f.Id == "single").ToRoutePlan(), R("20km-h500", 200, d));
|
||||
s.SaveDeployment(t.Id, new List<EquipmentDeployment>
|
||||
{
|
||||
d.FireUnits.First(f => f.Id == "air-standard").ToEquipmentDeployment(AerosolType.InertGas, 3, 12000, 1000, 0),
|
||||
d.FireUnits.First(f => f.Id == "air-standard").ToEquipmentDeployment(AerosolType.InertGas, 3, 6000, 1000, 0),
|
||||
});
|
||||
s.SaveCloudDispersal(t.Id, new CloudDispersal { AerosolType = (int)AerosolType.InertGas, DisperseHeight = 500 });
|
||||
s.UpdateStep(t.Id, 5);
|
||||
|
||||
@ -156,7 +156,9 @@ namespace CounterDrone.Core.Tests
|
||||
var drone = eng.Drones[0];
|
||||
var launched = eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched);
|
||||
Assert.True(launched > 0);
|
||||
Assert.True(drone.Hp < 0.1f, $"Hp={drone.Hp:F3} status={drone.Status} events={eng.Events.Count(e => e.Type == SimEventType.MunitionLaunched)} launches");
|
||||
var detail = _scenario.GetTaskDetail(_taskId)!;
|
||||
var report = new ReportGenerator().Generate(detail, eng.Events.ToList(), drone.Status, eng.SimulationTime);
|
||||
Assert.True(drone.Hp < 0.1f, $"Hp={drone.Hp:F3} status={drone.Status}\nReport:\n{report}");
|
||||
VerifyAndExportReport(eng, drone);
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,54 @@
|
||||
using System.Collections.Generic;
|
||||
using CounterDrone.Core.Algorithms;
|
||||
using CounterDrone.Core.Models;
|
||||
using Xunit;
|
||||
|
||||
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 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user