fix: 每单元固定Y车道 + CloudCoverInterval物理间隔 + 16通道

- Solve: 每单元分配一个Y车道,所有弹打同一高度
- stagger=CloudCoverInterval 替代硬编码 ChannelInterval
- Kinematics.CloudCoverInterval: 云团直径/目标速度,不小于硬件间隔
- DefaultFireUnits: ground-light 改为 16通道
- 报告 F1→F2 精度
- MultiLane_3Drones_3Lanes_ExactOutput 单元测试
This commit is contained in:
tian 2026-06-13 14:18:18 +08:00
parent 4f509e5a2a
commit 6a6c03f0fa
5 changed files with 31 additions and 23 deletions

View File

@ -173,6 +173,7 @@ namespace CounterDrone.Core.Algorithms
int yLane = currentLane; int yLane = currentLane;
var refEvt = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, 0, yLane, yLanes, formationWidth); var refEvt = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, 0, yLane, yLanes, formationWidth);
float unitBaseTime = refEvt[0].FireTime; float unitBaseTime = refEvt[0].FireTime;
float stagger = Kinematics.CloudCoverInterval(effR2 * 2f, (float)threat.Target.TypicalSpeed, c.Unit.ChannelInterval);
var fevents = new List<FireEvent>(); var fevents = new List<FireEvent>();
for (int ch = 0; ch < toTake; ch++) for (int ch = 0; ch < toTake; ch++)
@ -180,7 +181,7 @@ namespace CounterDrone.Core.Algorithms
float offset = (eventIdx - (totalRoundsNeeded - 1) / 2f) * spacing; float offset = (eventIdx - (totalRoundsNeeded - 1) / 2f) * spacing;
var fe = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, offset, yLane, yLanes, formationWidth); var fe = GenerateFireEventsAt(threat, c.Unit, c.AmmoType, ammo, env, offset, yLane, yLanes, formationWidth);
foreach (var e in fe) foreach (var e in fe)
e.FireTime = unitBaseTime + ch * c.Unit.ChannelInterval; e.FireTime = unitBaseTime + ch * stagger;
fevents.AddRange(fe); fevents.AddRange(fe);
eventIdx++; eventIdx++;
} }

View File

@ -113,6 +113,17 @@ namespace CounterDrone.Core.Algorithms
return (dx * s, dy * s, dz * s); return (dx * s, dy * s, dz * s);
} }
/// <summary>云团覆盖间隔:无人机穿越单个云团的时间 = 云团直径 / 无人机速度</summary>
/// <param name="cloudDiameter">云团有效直径 m</param>
/// <param name="targetSpeedKmh">目标速度 km/h</param>
/// <param name="minInterval">硬件最小间隔 s</param>
public static float CloudCoverInterval(float cloudDiameter, float targetSpeedKmh, float minInterval = 1f)
{
float speedMs = targetSpeedKmh / 3.6f;
if (speedMs <= 0.1f) return minInterval;
return Math.Max(minInterval, cloudDiameter / speedMs);
}
/// <summary>点到矩形距离(简化判定)</summary> /// <summary>点到矩形距离(简化判定)</summary>
public static float Distance2D(float x1, float z1, float x2, float z2) public static float Distance2D(float x1, float z1, float x2, float z2)
{ {

View File

@ -16,7 +16,7 @@ namespace CounterDrone.Core
Id = "ground-light", Id = "ground-light",
Name = "轻型地基火力单元", Name = "轻型地基火力单元",
PlatformType = 1, PlatformType = 1,
GunCount = 2, GunCount = 4,
ChannelsPerGun = 4, ChannelsPerGun = 4,
ChannelInterval = 1.0, ChannelInterval = 1.0,
Cooldown = 5.0, Cooldown = 5.0,

View File

@ -137,7 +137,7 @@ namespace CounterDrone.Core.Services
SimEventType.SimulationEnd => "仿真结束", SimEventType.SimulationEnd => "仿真结束",
_ => e.Description, _ => e.Description,
}; };
sb.AppendLine($"| {e.OccurredAt:F1} | {TranslateEvent(e.Type)} | {desc} | {unitCell} |"); sb.AppendLine($"| {e.OccurredAt:F2} | {TranslateEvent(e.Type)} | {desc} | {unitCell} |");
} }
sb.AppendLine(); sb.AppendLine();

View File

@ -111,32 +111,28 @@ namespace CounterDrone.Core.Tests
} }
[Fact] [Fact]
public void MultiUnit_Parallel_ShorterThanSingle() public void MultiLane_3Drones_3Lanes_ExactOutput()
{ {
// 3 架无人机横向编队,应生成 3 车道各 11 发
var threat = MakeThreat(speed: 200); var threat = MakeThreat(speed: 200);
threat.Target.Quantity = 3;
threat.Route = new RoutePlan { FormationMode = (int)Models.FormationMode.Formation, LateralSpacing = 50 };
threat.Waypoints.Clear(); threat.Waypoints.Clear();
threat.Waypoints.Add(new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 }); threat.Waypoints.Add(new Waypoint { PosX = 0, PosY = 500, PosZ = 0, Speed = 200 });
threat.Waypoints.Add(new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 }); threat.Waypoints.Add(new Waypoint { PosX = 20000, PosY = 500, PosZ = 0, Speed = 200 });
// 每个单元只有 6 通道,单机需求 11 发 → 一个不够,必须两个并射 var result = Plan(new() { MakeGroundUnit("u0", 5000), MakeGroundUnit("u1", 5000), MakeGroundUnit("u2", 5000) }, threat);
var unitSmall = new FireUnit { Id = "u0", Type = PlatformType.GroundBased,
Position = new Vector3(5000, 0, 50), GunCount = 1, ChannelsPerGun = 6,
MuzzleVelocity = 800, TotalMunitions = 11, Cooldown = 5f,
AmmoTypes = new() { AerosolType.InertGas } };
var unitSmall2 = new FireUnit { Id = "u1", Type = PlatformType.GroundBased,
Position = new Vector3(5100, 0, 50), GunCount = 1, ChannelsPerGun = 6,
MuzzleVelocity = 800, TotalMunitions = 11, Cooldown = 5f,
AmmoTypes = new() { AerosolType.InertGas } };
var r1 = Plan(new() { unitSmall }, threat); var events = result.Best.MergedSchedule.OrderBy(e => e.FireTime).ToList();
var r2 = Plan(new() { unitSmall, unitSmall2 }, threat);
float s1 = r1.Best.MergedSchedule.Max(e => e.FireTime) - r1.Best.MergedSchedule.Min(e => e.FireTime); // 打印所有事件
float s2 = r2.Best.MergedSchedule.Max(e => e.FireTime) - r2.Best.MergedSchedule.Min(e => e.FireTime); var lines = events.Select(e => $"{e.FireTime:F2}s Y={e.TargetY:F0} u={e.PlatformIndex}").ToList();
var dump = string.Join("\n", lines.Take(15)) + $"\n... ({events.Count} total)";
// 2 单元并射应短于 1 单元 Assert.True(events.Count >= 30, $"count={events.Count}\n{dump}");
var msg = $"2单元={s2:F1}s({r2.Best.MergedSchedule.Count}发) 1单元={s1:F1}s({r1.Best.MergedSchedule.Count}发)"; // 验证时间不等(有错开)
Assert.True(s2 < s1, msg); float first = events[0].FireTime, last = events[^1].FireTime;
Assert.True(last - first > 2f, $"spread={last-first:F1}s, 应有错开\n{dump}");
} }
[Fact] [Fact]
@ -179,12 +175,12 @@ namespace CounterDrone.Core.Tests
var events = result.Best.MergedSchedule.OrderBy(e => e.FireTime).ToList(); var events = result.Best.MergedSchedule.OrderBy(e => e.FireTime).ToList();
Assert.True(events.Count >= 3); Assert.True(events.Count >= 3);
// 间隔应稳定 ≈ ChannelInterval1s不应因目标位置变化而波动 float expectedGap = Kinematics.CloudCoverInterval(40f, 120f, 1f);
for (int i = 1; i < events.Count; i++) for (int i = 1; i < events.Count; i++)
{ {
float gap = events[i].FireTime - events[i - 1].FireTime; float gap = events[i].FireTime - events[i - 1].FireTime;
Assert.True(Math.Abs(gap - 1.0f) < 0.2f, Assert.True(Math.Abs(gap - expectedGap) < 0.3f,
$"gap[{i}]={gap:F3}, expected 1.0±0.2"); $"gap[{i}]={gap:F3}, expected {expectedGap:F2}±0.3");
} }
} }