< Summary

Class:IADS
Assembly:bamlab.micromissiles
File(s):/github/workspace/Assets/Scripts/IADS/IADS.cs
Covered lines:80
Uncovered lines:201
Coverable lines:281
Total lines:466
Line coverage:28.4% (80 of 281)
Covered branches:0
Total branches:0
Covered methods:12
Total methods:31
Method coverage:38.7% (12 of 31)

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
IADS()0%110100%
Awake()0%6200%
OnDestroy()0%20400%
Start()0%110100%
LateUpdate()0%330100%
RegisterSimulationStarted()0%2100%
LaunchInterceptorsManager()0%12300%
CheckAndLaunchInterceptors()0%72800%
ShouldLaunchSubmunitions(...)0%25.956017.86%
HasClusterAssignment(...)0%110100%
CleanupDestroyedClusters()0%5.155081.82%
ReleaseInterceptorFromCluster(...)0%110100%
AssignSubmunitionsToThreats(...)0%30500%
RequestAssignInterceptorToThreat(...)0%110100%
AssignInterceptorToThreat(...)0%13.127050%
RequestClusterThreat(...)0%110100%
CheckForEscapingThreatsManager()0%12300%
CheckForEscapingThreats()0%42600%
ClusterThreatsManager()0%12300%
ClusterThreats()0%30500%
RegisterNewThreat(...)0%110100%
RegisterNewInterceptor(...)0%110100%
RegisterInterceptorHit(...)0%30500%
RegisterInterceptorMiss(...)0%20400%
RegisterThreatHit(...)0%12300%
RegisterThreatMiss(...)0%12300%
GetThreatTracks()0%2100%
GetInterceptorTracks()0%2100%
RegisterSimulationEnded()0%2100%

File(s)

/github/workspace/Assets/Scripts/IADS/IADS.cs

#LineLine coverage
 1using System;
 2using System.Collections;
 3using System.Collections.Generic;
 4using System.IO;
 5using System.Linq;
 6using UnityEngine;
 7
 8// Integrated Air Defense System.
 9public class IADS : MonoBehaviour {
 010  public static IADS Instance { get; private set; }
 11
 12  private const float LaunchInterceptorsPeriod = 0.4f;
 13  private const float CheckForEscapingThreatsPeriod = 5.0f;
 14  private const float ClusterThreatsPeriod = 2.0f;
 15
 16  // TODO(titan): Choose the CSV file based on the interceptor type.
 117  private ILaunchAnglePlanner _launchAnglePlanner =
 18      new LaunchAngleCsvInterpolator(Path.Combine("Planning", "hydra70_launch_angle.csv"));
 119  private IAssignment _assignmentScheme = new MaxSpeedAssignment();
 20  private Coroutine _launchInterceptorsCoroutine;
 21
 22  [SerializeField]
 123  private List<TrackFileData> _trackFiles = new List<TrackFileData>();
 124  private Dictionary<Agent, TrackFileData> _trackFileMap = new Dictionary<Agent, TrackFileData>();
 25
 126  private List<Interceptor> _assignmentQueue = new List<Interceptor>();
 127  private List<Cluster> _threatClusters = new List<Cluster>();
 128  private Dictionary<Cluster, ThreatClusterData> _threatClusterMap =
 29      new Dictionary<Cluster, ThreatClusterData>();
 130  private Dictionary<Interceptor, Cluster> _interceptorClusterMap =
 31      new Dictionary<Interceptor, Cluster>();
 132  private HashSet<Interceptor> _assignableInterceptors = new HashSet<Interceptor>();
 33
 134  private HashSet<Threat> _threatsToCluster = new HashSet<Threat>();
 35  private Coroutine _checkForEscapingThreatsCoroutine;
 36  private Coroutine _clusterThreatsCoroutine;
 37
 138  private int _trackFileIdTicker = 0;
 39
 040  private void Awake() {
 041    if (Instance == null) {
 042      Instance = this;
 043    } else {
 044      Destroy(gameObject);
 045    }
 046  }
 47
 048  private void OnDestroy() {
 049    if (_launchInterceptorsCoroutine != null) {
 050      StopCoroutine(_launchInterceptorsCoroutine);
 051    }
 052    if (_checkForEscapingThreatsCoroutine != null) {
 053      StopCoroutine(_checkForEscapingThreatsCoroutine);
 054    }
 055    if (_clusterThreatsCoroutine != null) {
 056      StopCoroutine(_clusterThreatsCoroutine);
 057    }
 058  }
 59
 160  public void Start() {
 161    SimManager.Instance.OnSimulationStarted += RegisterSimulationStarted;
 162    SimManager.Instance.OnSimulationEnded += RegisterSimulationEnded;
 163    SimManager.Instance.OnNewThreat += RegisterNewThreat;
 164    SimManager.Instance.OnNewInterceptor += RegisterNewInterceptor;
 165  }
 66
 167  public void LateUpdate() {
 68    // Detach interceptors assigned to fully-destroyed clusters.
 169    CleanupDestroyedClusters();
 70
 71    // Update the cluster centroids.
 672    foreach (var cluster in _threatClusters) {
 173      cluster.Recenter();
 174      _threatClusterMap[cluster].UpdateCentroid();
 175    }
 76
 77    // Assign any interceptors that are no longer assigned to any threat.
 178    AssignInterceptorToThreat(
 179        _assignableInterceptors.Where(interceptor => !interceptor.IsTerminated()).ToList());
 180  }
 81
 082  private void RegisterSimulationStarted() {
 083    _launchInterceptorsCoroutine =
 84        StartCoroutine(LaunchInterceptorsManager(LaunchInterceptorsPeriod));
 085    _checkForEscapingThreatsCoroutine =
 86        StartCoroutine(CheckForEscapingThreatsManager(CheckForEscapingThreatsPeriod));
 087    _clusterThreatsCoroutine = StartCoroutine(ClusterThreatsManager(ClusterThreatsPeriod));
 088  }
 89
 090  private IEnumerator LaunchInterceptorsManager(float period) {
 091    while (true) {
 92      // Check whether an interceptor should be launched at a cluster and launch it.
 093      CheckAndLaunchInterceptors();
 094      yield return new WaitForSeconds(period);
 095    }
 96  }
 97
 098  private void CheckAndLaunchInterceptors() {
 099    foreach (var cluster in _threatClusters) {
 100      // Check whether an interceptor has already been assigned to the cluster.
 0101      if (_threatClusterMap[cluster].Status != ThreatClusterStatus.UNASSIGNED) {
 0102        continue;
 103      }
 104
 105      // Check whether all threats in the cluster have terminated.
 0106      bool allTerminated = cluster.Threats.All(threat => threat.IsTerminated());
 0107      if (allTerminated) {
 0108        continue;
 109      }
 110
 111      // Create a predictor to track the cluster's centroid.
 0112      IPredictor predictor = new LinearExtrapolator(_threatClusterMap[cluster].Centroid);
 113
 114      // Create a launch planner.
 0115      ILaunchPlanner planner = new IterativeLaunchPlanner(_launchAnglePlanner, predictor);
 0116      LaunchPlan plan = planner.Plan();
 117
 118      // Check whether an interceptor should be launched.
 0119      if (plan.ShouldLaunch) {
 0120        Debug.Log(
 121            $"Launching a carrier interceptor at an elevation of {plan.LaunchAngle} degrees to position {plan.InterceptP
 0122        UIManager.Instance.LogActionMessage(
 123            $"[IADS] Launching a carrier interceptor at an elevation of {plan.LaunchAngle} degrees to position {plan.Int
 124
 125        // Create a new interceptor.
 0126        Configs.AgentConfig config =
 127            SimManager.Instance.SimulationConfig.InterceptorSwarmConfigs.Count > 0
 128                ? SimManager.Instance.SimulationConfig.InterceptorSwarmConfigs[0].AgentConfig
 129                : null;
 0130        Simulation.State initialState = new Simulation.State();
 131
 132        // Set the initial position, which defaults to the origin.
 0133        initialState.Position = Coordinates3.ToProto(Vector3.zero);
 134
 135        // Set the initial velocity to point along the launch vector.
 0136        initialState.Velocity = Coordinates3.ToProto(plan.GetNormalizedLaunchVector() * 1e-3f);
 0137        Interceptor interceptor = SimManager.Instance.CreateInterceptor(config, initialState);
 138
 139        // Assign the interceptor to the cluster.
 0140        _interceptorClusterMap[interceptor] = cluster;
 0141        interceptor.AssignTarget(_threatClusterMap[cluster].Centroid);
 0142        _threatClusterMap[cluster].AssignInterceptor(interceptor);
 143
 144        // Create an interceptor swarm.
 0145        SimManager.Instance.AddInterceptorSwarm(new List<Agent> { interceptor as Agent });
 0146      }
 0147    }
 0148  }
 149
 1150  public bool ShouldLaunchSubmunitions(Interceptor carrier) {
 2151    if (!HasClusterAssignment(carrier)) {
 1152      return false;
 153    }
 154    // The carrier interceptor will spawn submunitions when any target is greater than 30 degrees
 155    // away from the carrier interceptor's current velocity or when any threat is within 500 meters
 156    // of the interceptor.
 157    const float SubmunitionSpawnMaxAngularDeviation = 30.0f;
 158    const float SubmunitionSpawnMinDistanceToThreat = 500.0f;
 159    const float SubmunitionSpawnMaxDistanceToThreat = 2000.0f;
 160    // TODO(titan): The prediction time should be a function of the submunition characteristic, such
 161    // as the boost time.
 162    const float SubmunitionSpawnPredictionTime = 0.6f;
 163
 0164    Cluster cluster = _interceptorClusterMap[carrier];
 0165    List<Threat> threats = cluster.Threats.ToList();
 0166    Vector3 carrierPosition = carrier.GetPosition();
 0167    Vector3 carrierVelocity = carrier.GetVelocity();
 0168    foreach (var threat in threats) {
 0169      IPredictor predictor = new LinearExtrapolator(threat);
 0170      PredictorState predictedState = predictor.Predict(SubmunitionSpawnPredictionTime);
 0171      Vector3 positionToPredictedThreat = predictedState.Position - carrierPosition;
 0172      float predictedDistanceToThreat = positionToPredictedThreat.magnitude;
 173
 174      // Check whether the distance to the threat is less than the minimum distance.
 0175      if (predictedDistanceToThreat < SubmunitionSpawnMinDistanceToThreat) {
 0176        return true;
 177      }
 178
 179      // Check whether the angular deviation exceeds the maximum angular deviation.
 0180      float distanceDeviation =
 181          (Vector3.ProjectOnPlane(positionToPredictedThreat, carrierVelocity)).magnitude;
 0182      float angularDeviation =
 183          Mathf.Asin(distanceDeviation / predictedDistanceToThreat) * Mathf.Rad2Deg;
 0184      if (angularDeviation > SubmunitionSpawnMaxAngularDeviation &&
 0185          predictedDistanceToThreat < SubmunitionSpawnMaxDistanceToThreat) {
 0186        return true;
 187      }
 0188    }
 0189    return false;
 1190  }
 191
 192  // True if an interceptor still has a valid cluster assignment.
 1193  private bool HasClusterAssignment(Interceptor interceptor) {
 1194    return _interceptorClusterMap.ContainsKey(interceptor);
 1195  }
 196
 197  // Releases interceptors from clusters that only contain destroyed threats.
 1198  private void CleanupDestroyedClusters() {
 6199    foreach (Cluster cluster in _threatClusters) {
 1200      if (!cluster.IsFullyTerminated()) {
 0201        continue;
 202      }
 203
 1204      IReadOnlyList<Interceptor> assigned = _threatClusterMap[cluster].AssignedInterceptors;
 1205      if (assigned.Count == 0) {
 0206        continue;
 207      }
 208
 6209      foreach (Interceptor interceptor in assigned.ToList()) {
 1210        ReleaseInterceptorFromCluster(cluster, interceptor);
 1211      }
 1212    }
 1213  }
 214
 215  // Detach a single interceptor from a cluster and decide its next action.
 216  // All interceptors are queued for reassignment in the hierarchical architecture,
 217  // including carrier interceptors.
 1218  private void ReleaseInterceptorFromCluster(Cluster cluster, Interceptor interceptor) {
 1219    _threatClusterMap[cluster].RemoveInterceptor(interceptor);
 1220    _interceptorClusterMap.Remove(interceptor);
 221    // TODO(titan): During the hierarchical architecture overhaul/refactor,
 222    // we need to properly handle the assignment of carrier interceptors at this point.
 223    // Since they have been unassigned from their cluster, they will need to be queued for
 224    // a new cluster assignment. Right now, they will go for re-assignment but fail
 225    // since they return false for IsAssignable().
 1226    RequestAssignInterceptorToThreat(interceptor);
 1227  }
 228
 0229  public void AssignSubmunitionsToThreats(Interceptor carrier, List<Interceptor> interceptors) {
 230    // Assign threats to the submunitions.
 0231    Cluster cluster = _interceptorClusterMap[carrier];
 0232    List<Threat> threats = cluster.Threats.ToList();
 0233    IEnumerable<IAssignment.AssignmentItem> assignments =
 234        _assignmentScheme.Assign(interceptors, threats);
 235
 236    // Mark the cluster as delegated to submunitions.
 0237    _threatClusterMap[cluster].RemoveInterceptor(carrier, delegated: true);
 238
 239    // Apply the assignments to the submunitions.
 0240    foreach (var assignment in assignments) {
 0241      assignment.Interceptor.AssignTarget(assignment.Threat);
 0242    }
 243
 244    // Check whether any submunitions were not assigned to a threat.
 0245    foreach (var interceptor in interceptors) {
 0246      if (!interceptor.HasAssignedTarget()) {
 0247        RequestAssignInterceptorToThreat(interceptor);
 0248      }
 0249    }
 0250  }
 251
 1252  public void RequestAssignInterceptorToThreat(Interceptor interceptor) {
 1253    interceptor.UnassignTarget();
 1254    _assignableInterceptors.Add(interceptor);
 1255  }
 256
 1257  private void AssignInterceptorToThreat(in IReadOnlyList<Interceptor> interceptors) {
 1258    if (interceptors.Count == 0) {
 0259      return;
 260    }
 261
 262    // The threat originally assigned to the interceptor has been terminated, so assign another
 263    // threat to the interceptor.
 264
 265    // This pulls from all available track files, not from our previously assigned cluster.
 3266    List<Threat> threats = _trackFiles.Where(trackFile => trackFile.Agent is Threat)
 1267                               .Select(trackFile => trackFile.Agent as Threat)
 268                               .ToList();
 1269    if (threats.Count == 0) {
 0270      return;
 271    }
 272
 1273    IEnumerable<IAssignment.AssignmentItem> assignments =
 274        _assignmentScheme.Assign(interceptors, threats);
 275
 276    // Apply the assignments to the submunitions.
 3277    foreach (var assignment in assignments) {
 0278      assignment.Interceptor.AssignTarget(assignment.Threat);
 0279      _assignableInterceptors.Remove(assignment.Interceptor);
 0280    }
 1281  }
 282
 1283  public void RequestClusterThreat(Threat threat) {
 1284    _threatsToCluster.Add(threat);
 1285  }
 286
 0287  private IEnumerator CheckForEscapingThreatsManager(float period) {
 0288    while (true) {
 0289      yield return new WaitForSeconds(period);
 0290      CheckForEscapingThreats();
 0291    }
 292  }
 293
 0294  private void CheckForEscapingThreats() {
 0295    List<Threat> threats = _trackFiles
 0296                               .Where(trackFile => trackFile.Status == TrackStatus.ASSIGNED &&
 297                                                   trackFile.Agent is Threat)
 0298                               .Select(trackFile => trackFile.Agent as Threat)
 299                               .ToList();
 0300    if (threats.Count == 0) {
 0301      return;
 302    }
 303
 304    // Check whether the threats are escaping the pursuing interceptors.
 0305    foreach (var threat in threats) {
 0306      bool isEscaping = threat.AssignedInterceptors.All(interceptor => {
 0307        Vector3 interceptorPosition = interceptor.GetPosition();
 0308        Vector3 threatPosition = threat.GetPosition();
 309
 0310        float threatTimeToHit = (float)(threatPosition.magnitude / threat.GetSpeed());
 0311        float interceptorTimeToHit =
 312            (float)((threatPosition - interceptorPosition).magnitude / interceptor.GetSpeed());
 0313        return interceptorPosition.magnitude > threatPosition.magnitude ||
 314               threatTimeToHit < interceptorTimeToHit;
 0315      });
 0316      if (isEscaping) {
 0317        RequestClusterThreat(threat);
 0318      }
 0319    }
 0320  }
 321
 0322  private IEnumerator ClusterThreatsManager(float period) {
 0323    while (true) {
 0324      ClusterThreats();
 0325      yield return new WaitForSeconds(period);
 0326    }
 327  }
 328
 0329  private void ClusterThreats() {
 330    // Maximum number of threats per cluster.
 331    const int MaxSize = 7;
 332    // Maximum cluster radius in meters.
 333    const float MaxRadius = 500;
 334
 335    // Filter the threats.
 0336    List<Threat> threats =
 337        _threatsToCluster
 0338            .Where(threat => !threat.IsTerminated() && threat.AssignedInterceptors.Count == 0)
 339            .ToList();
 0340    if (threats.Count == 0) {
 0341      return;
 342    }
 343
 344    // Cluster threats.
 0345    IClusterer clusterer = new AgglomerativeClusterer(new List<Agent>(threats), MaxSize, MaxRadius);
 0346    clusterer.Cluster();
 0347    var clusters = clusterer.Clusters;
 0348    Debug.Log($"Clustered {threats.Count} threats into {clusters.Count} clusters.");
 0349    UIManager.Instance.LogActionMessage(
 350        $"[IADS] Clustered {threats.Count} threats into {clusters.Count} clusters.");
 351
 0352    _threatClusters = clusters.ToList();
 0353    foreach (var cluster in clusters) {
 0354      _threatClusterMap.Add(cluster, new ThreatClusterData(cluster));
 0355    }
 356
 0357    _threatsToCluster.Clear();
 0358  }
 359
 1360  public void RegisterNewThreat(Threat threat) {
 1361    string trackID = $"T{1000 + ++_trackFileIdTicker}";
 1362    ThreatData trackFile = new ThreatData(threat, trackID);
 1363    _trackFiles.Add(trackFile);
 1364    _trackFileMap.Add(threat, trackFile);
 1365    RequestClusterThreat(threat);
 366
 1367    threat.OnThreatHit += RegisterThreatHit;
 1368    threat.OnThreatMiss += RegisterThreatMiss;
 1369  }
 370
 1371  public void RegisterNewInterceptor(Interceptor interceptor) {
 1372    string trackID = $"I{2000 + ++_trackFileIdTicker}";
 1373    InterceptorData trackFile = new InterceptorData(interceptor, trackID);
 1374    _trackFiles.Add(trackFile);
 1375    _trackFileMap.Add(interceptor, trackFile);
 376
 1377    interceptor.OnInterceptMiss += RegisterInterceptorMiss;
 1378    interceptor.OnInterceptHit += RegisterInterceptorHit;
 1379  }
 380
 0381  private void RegisterInterceptorHit(Interceptor interceptor, Threat threat) {
 0382    var threatTrack = _trackFileMap[threat] as ThreatData;
 0383    var interceptorTrack = _trackFileMap[interceptor] as InterceptorData;
 384
 0385    if (threatTrack != null) {
 0386      threatTrack.RemoveInterceptor(interceptor);
 0387      threatTrack.MarkDestroyed();
 0388    }
 389
 0390    if (interceptorTrack != null) {
 0391      interceptorTrack.RemoveThreat(threat);
 0392      interceptorTrack.MarkDestroyed();
 0393    }
 394
 395    // Assign the interceptors to other threats.
 0396    foreach (var assignedInterceptor in threat.AssignedInterceptors.ToList()) {
 0397      if (assignedInterceptor is not CarrierInterceptor) {
 0398        RequestAssignInterceptorToThreat(assignedInterceptor as Interceptor);
 0399      }
 0400    }
 0401  }
 402
 0403  private void RegisterInterceptorMiss(Interceptor interceptor, Threat threat) {
 404    // Assign the interceptor to another threat.
 0405    RequestAssignInterceptorToThreat(interceptor);
 406
 0407    var threatTrack = _trackFileMap[threat] as ThreatData;
 0408    var interceptorTrack = _trackFileMap[interceptor] as InterceptorData;
 409
 0410    if (threatTrack != null) {
 0411      threatTrack.RemoveInterceptor(interceptor);
 412
 413      // Check if the threat is being targeted by at least one interceptor.
 0414      if (threatTrack.AssignedInterceptorCount == 0) {
 0415        RequestClusterThreat(threat);
 0416      }
 0417    }
 418
 0419    if (interceptorTrack != null) {
 0420      interceptorTrack.RemoveThreat(threat);
 0421      interceptorTrack.MarkDestroyed();
 0422    }
 0423  }
 424
 0425  private void RegisterThreatHit(Threat threat) {
 0426    var threatTrack = _trackFileMap[threat] as ThreatData;
 0427    if (threatTrack != null) {
 0428      threatTrack.MarkDestroyed();
 0429    }
 430
 431    // Re-assign the assigned interceptors to other threats.
 0432    foreach (var interceptor in threat.AssignedInterceptors.ToList()) {
 0433      RequestAssignInterceptorToThreat(interceptor as Interceptor);
 0434    }
 0435  }
 436
 0437  private void RegisterThreatMiss(Threat threat) {
 438    // The threat missed (e.g., it hit the floor).
 439    // Re-assign the assigned interceptors to other threats.
 0440    foreach (var interceptor in threat.AssignedInterceptors.ToList()) {
 0441      RequestAssignInterceptorToThreat(interceptor as Interceptor);
 0442    }
 443
 0444    var threatTrack = _trackFileMap[threat] as ThreatData;
 0445    if (threatTrack != null) {
 0446      threatTrack.MarkDestroyed();
 0447    }
 0448  }
 449
 0450  public List<ThreatData> GetThreatTracks() => _trackFiles.OfType<ThreatData>().ToList();
 451
 452  public List<InterceptorData> GetInterceptorTracks() =>
 0453      _trackFiles.OfType<InterceptorData>().ToList();
 454
 0455  private void RegisterSimulationEnded() {
 0456    _trackFiles.Clear();
 0457    _trackFileMap.Clear();
 0458    _assignmentQueue.Clear();
 0459    _threatClusters.Clear();
 0460    _threatClusterMap.Clear();
 0461    _interceptorClusterMap.Clear();
 0462    _assignableInterceptors.Clear();
 0463    _threatsToCluster.Clear();
 0464    _trackFileIdTicker = 0;
 0465  }
 466}