RecommendMultiGroup: 多编队火力分配 + 专一弹药优先 + 130测试通过

This commit is contained in:
tian 2026-06-12 15:46:05 +08:00
parent 12cb6576fe
commit 6030b8de07
5 changed files with 156 additions and 1 deletions

View File

@ -33,8 +33,52 @@ namespace CounterDrone.Core.Algorithms
public float InterceptProbability { get; set; }
public string SummaryRationale { get; set; } = string.Empty;
/// <summary>由推荐方案生成的发射计划,仿真器直接执行</summary>
public List<FireEvent> FireSchedule { get; set; } = new();
/// <summary>此方案占用的平台起始索引(多编队合并用)</summary>
public int PlatformOffset { get; set; }
}
/// <summary>多编队推荐结果</summary>
public class MultiGroupRecommendation
{
/// <summary>每组的独立方案</summary>
public List<DefenseSolution> GroupSolutions { get; set; } = new();
/// <summary>合并后的总发射计划</summary>
public List<FireEvent> MergedFireSchedule { get; set; } = new();
}
/// <summary>火力单元(装备编组)</summary>
public class FireUnit
{
public string GroupId { get; set; } = string.Empty;
public AerosolType LoadedAmmo { get; set; }
public int PlatformCount { get; set; }
public float PositionX, PositionY, PositionZ;
public float MuzzleVelocity { get; set; } = 800f;
public float Cooldown { get; set; } = 5f;
public float AmmoChangeTime { get; set; } = 300f;
// 弹药库:可以装填的弹药类型
public List<AerosolType> AvailableAmmo { get; set; } = new();
}
/// <summary>无人机编队(多编队推荐输入)</summary>
public class DroneGroup
{
public string GroupId { get; set; } = string.Empty;
public TargetConfig Target { get; set; } = new();
public RoutePlan Route { get; set; } = new();
public List<Waypoint> Waypoints { get; set; } = new();
/// <summary>预计到达航路中点的时间(秒)</summary>
public float GetArrivalTime()
{
if (Waypoints.Count < 2) return 0;
var start = new Vector3((float)Waypoints[0].PosX, (float)Waypoints[0].PosY, (float)Waypoints[0].PosZ);
var end = new Vector3((float)Waypoints[^1].PosX, (float)Waypoints[^1].PosY, (float)Waypoints[^1].PosZ);
var speed = (float)Target.TypicalSpeed / 3.6f;
return start.DistanceTo(end) / speed / 2f;
}
}
public class RecommendedPlatform

View File

@ -219,5 +219,62 @@ namespace CounterDrone.Core.Algorithms
var sigma = (sY + sZ) / 2f;
return rPhase2 + sigma * (float)Math.Sqrt(2f * Math.Log(peakC / threshold));
}
/// <summary>多编队推荐 — 每组独立方案,按时间分配火力单元,合并发射计划</summary>
public MultiGroupRecommendation RecommendMultiGroup(
List<DroneGroup> droneGroups, List<FireUnit> fireUnits, CombatScene environment)
{
var result = new MultiGroupRecommendation();
if (droneGroups.Count == 0 || fireUnits.Count == 0) return result;
var sorted = droneGroups.OrderBy(g => g.GetArrivalTime()).ToList();
var usedUntil = new Dictionary<string, float>();
int offset = 0;
foreach (var group in sorted)
{
var powerType = (PowerType)group.Target.PowerType;
var neededAmmo = MatchTable.GetValueOrDefault(powerType, AerosolType.InertGas);
FireUnit assigned = null;
// 优先匹配专一弹药的火力单元,避免多用途单元被抢占
var candidates = fireUnits
.Where(u => u.AvailableAmmo.Contains(neededAmmo))
.OrderBy(u => u.AvailableAmmo.Count) // 少弹种的优先
.ThenBy(u => usedUntil.ContainsKey(u.GroupId) ? 1 : 0); // 不忙的优先
foreach (var unit in candidates)
{
if (!unit.AvailableAmmo.Contains(neededAmmo)) continue;
if (!usedUntil.TryGetValue(unit.GroupId, out var busyUntil))
{ assigned = unit; break; }
if (unit.LoadedAmmo == neededAmmo && busyUntil <= group.GetArrivalTime())
{ assigned = unit; break; }
if (busyUntil + unit.AmmoChangeTime <= group.GetArrivalTime())
{ assigned = unit; break; }
}
if (assigned == null) continue;
var threat = new ThreatProfile
{
Environment = environment,
Targets = new List<TargetConfig> { group.Target },
Route = group.Route,
Waypoints = group.Waypoints,
};
var sol = Recommend(threat).Best;
foreach (var fe in sol.FireSchedule) fe.PlatformIndex += offset;
result.GroupSolutions.Add(sol);
result.MergedFireSchedule.AddRange(sol.FireSchedule);
assigned.LoadedAmmo = sol.RecommendedAerosolType;
usedUntil[assigned.GroupId] = (sol.FireSchedule.Count > 0
? sol.FireSchedule.Max(f => f.FireTime) : group.GetArrivalTime()) + assigned.Cooldown;
offset += sol.Platforms.Count;
}
return result;
}
}
}

View File

@ -1,3 +1,4 @@
using System.Collections.Generic;
using CounterDrone.Core.Models;
namespace CounterDrone.Core.Algorithms
@ -6,5 +7,11 @@ namespace CounterDrone.Core.Algorithms
public interface IDefenseAdvisor
{
DefenseRecommendation Recommend(ThreatProfile threat);
/// <summary>多编队推荐 — 为每组分配火力单元,合并发射计划</summary>
MultiGroupRecommendation RecommendMultiGroup(
List<DroneGroup> droneGroups,
List<FireUnit> fireUnits,
CombatScene environment);
}
}

View File

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using CounterDrone.Core.Algorithms;
using CounterDrone.Core.Models;
using Xunit;
namespace CounterDrone.Core.Tests
{
public class MultiGroupAdvisorTests
{
[Fact]
public void TwoSeparateUnits_TwoGroups_BothGetSolutions()
{
var ammoCatalog = DefaultAmmunition.GetAll();
Console.WriteLine($"Ammo: inert={ammoCatalog[0].BurstChargeKg}kg, active={ammoCatalog[1].BurstChargeKg}kg");
var advisor = new DefaultDefenseAdvisor(ammoCatalog);
var g1 = new DroneGroup
{
GroupId = "g1", Target = new TargetConfig { PowerType = (int)PowerType.Piston, Quantity = 1, TypicalSpeed = 200, TypicalAltitude = 500 },
Route = new RoutePlan { FormationMode = (int)FormationMode.Single },
Waypoints = new List<Waypoint> { new() { PosX = 0, PosY = 500, PosZ = 0 }, new() { PosX = 20000, PosY = 500, PosZ = 0 } },
};
var g2 = new DroneGroup
{
GroupId = "g2", Target = new TargetConfig { PowerType = (int)PowerType.Jet, Quantity = 1, TypicalSpeed = 500, TypicalAltitude = 800 },
Route = new RoutePlan { FormationMode = (int)FormationMode.Single },
Waypoints = new List<Waypoint> { new() { PosX = 0, PosY = 800, PosZ = 0 }, new() { PosX = 30000, PosY = 800, PosZ = 0 } },
};
var u1 = new FireUnit { GroupId = "u1", AvailableAmmo = new() { AerosolType.InertGas, AerosolType.ActiveMaterial }, PlatformCount = 5, MuzzleVelocity = 800 };
var u2 = new FireUnit { GroupId = "u2", AvailableAmmo = new() { AerosolType.ActiveMaterial }, PlatformCount = 4, MuzzleVelocity = 800 };
var rec = advisor.RecommendMultiGroup(
new List<DroneGroup> { g1, g2 },
new List<FireUnit> { u1, u2 },
new CombatScene { WindSpeed = 3 });
var msg = $"Solutions={rec.GroupSolutions.Count}, Merged={rec.MergedFireSchedule.Count}";
foreach (var s in rec.GroupSolutions)
msg += $" | {s.RecommendedAerosolType}: plat={s.Platforms.Count} fire={s.FireSchedule.Count}";
Assert.True(rec.GroupSolutions.Count == 2, msg);
Assert.NotEmpty(rec.MergedFireSchedule);
}
}
}