Repository: dstl/Stone-Soup

Issue:
self.metric_manager.generate_metrics() : ZeroDivisionError: division by zero
Probably my data, however FYI.

```
  File "lib\_mot\mot.py", line 302, in run
    self.metrics_generate()
  File "lib\_mot\mot.py", line 253, in metrics_generate
    self.metrics = self.metric_manager.generate_metrics()
  File "C:\g\testapp\lib\lib\site-packages\stonesoup\metricgenerator\manager.py", line 87, in generate_metrics
    metric = generator.compute_metric(self)
  File "C:\g\testapp\lib\lib\site-packages\stonesoup\metricgenerator\tracktotruthmetrics.py", line 45, in compute_metric
    self.A_time_range(manager),
  File "C:\g\testapp\lib\lib\site-packages\stonesoup\metricgenerator\tracktotruthmetrics.py", line 169, in A_time_range
    manager, timestamps)
ZeroDivisionError: division by zero
```

Discussion:

Patch:
diff --git a/stonesoup/metricgenerator/tracktotruthmetrics.py b/stonesoup/metricgenerator/tracktotruthmetrics.py
index d076e4d2..0ac1edca 100644
--- a/stonesoup/metricgenerator/tracktotruthmetrics.py
+++ b/stonesoup/metricgenerator/tracktotruthmetrics.py
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 import datetime
+import warnings
 from operator import attrgetter
 
 import numpy as np
@@ -48,6 +49,16 @@ class SIAPMetrics(MetricGenerator):
                    self.LS(manager)]
         return metrics
 
+    @staticmethod
+    def _warn_no_truth(manager):
+        if len(manager.groundtruth_paths) == 0:
+            warnings.warn("No truth to generate SIAP Metric", stacklevel=2)
+
+    @staticmethod
+    def _warn_no_tracks(manager):
+        if len(manager.tracks) == 0:
+            warnings.warn("No tracks to generate SIAP Metric", stacklevel=2)
+
     def C_single_time(self, manager, timestamp):
         r"""SIAP metric C at a specific time
 
@@ -126,8 +137,12 @@ class SIAPMetrics(MetricGenerator):
         """
 
         timestamps = manager.list_timestamps()
-        C = self._jt_sum(manager, timestamps) / self._j_sum(
-            manager, timestamps)
+        try:
+            C = self._jt_sum(manager, timestamps) / self._j_sum(
+                manager, timestamps)
+        except ZeroDivisionError:
+            self._warn_no_truth(manager)
+            C = 0
         return TimeRangeMetric(
             title="SIAP C",
             value=C,
@@ -165,8 +180,12 @@ class SIAPMetrics(MetricGenerator):
         """
 
         timestamps = manager.list_timestamps()
-        A = self._na_sum(manager, timestamps) / self._jt_sum(
-            manager, timestamps)
+        try:
+            A = self._na_sum(manager, timestamps) / self._jt_sum(manager, timestamps)
+        except ZeroDivisionError:
+            self._warn_no_truth(manager)
+            self._warn_no_tracks(manager)
+            A = 0
         return TimeRangeMetric(
             title="SIAP A",
             value=A,
@@ -207,7 +226,11 @@ class SIAPMetrics(MetricGenerator):
             self._n_t(manager, timestamp) - self._na_t(manager, timestamp)
             for timestamp in timestamps)
 
-        S = numerator / self._n_sum(manager, timestamps)
+        try:
+            S = numerator / self._n_sum(manager, timestamps)
+        except ZeroDivisionError:
+            self._warn_no_tracks(manager)
+            S = 0
         return TimeRangeMetric(
             title="SIAP S",
             value=S,
@@ -234,6 +257,8 @@ class SIAPMetrics(MetricGenerator):
         r = self._r(manager)
         if r == 0:
             value = np.inf
+            self._warn_no_truth(manager)
+            self._warn_no_tracks(manager)
         else:
             value = 1 / r
 
@@ -277,9 +302,14 @@ class SIAPMetrics(MetricGenerator):
                           for truth in manager.groundtruth_paths)
 
         timestamps = manager.list_timestamps()
+        try:
+            LS = numerator / denominator
+        except ZeroDivisionError:
+            self._warn_no_truth(manager)
+            LS = 0
         return TimeRangeMetric(
             title="SIAP LS",
-            value=numerator / denominator,
+            value=LS,
             time_range=TimeRange(min(timestamps), max(timestamps)),
             generator=self)
 
@@ -571,7 +601,11 @@ class SIAPMetrics(MetricGenerator):
                         for truth in manager.groundtruth_paths)
         denominator = sum(self._tt_j(manager, truth).total_seconds()
                           for truth in manager.groundtruth_paths)
-        return numerator / denominator
+        try:
+            return numerator / denominator
+        except ZeroDivisionError:
+            # No truth or tracks
+            return 0
 
     def _t_j(self, truth):
         """Total time truth exists for


Test patch:
diff --git a/stonesoup/metricgenerator/tests/test_tracktotruthmetrics.py b/stonesoup/metricgenerator/tests/test_tracktotruthmetrics.py
index e7a3df90..6fc0c9ea 100644
--- a/stonesoup/metricgenerator/tests/test_tracktotruthmetrics.py
+++ b/stonesoup/metricgenerator/tests/test_tracktotruthmetrics.py
@@ -1,5 +1,7 @@
 import datetime
 
+import pytest
+
 from ..tracktotruthmetrics import SIAPMetrics
 from ...types.association import TimeRangeAssociation, AssociationSet
 from ...types.track import Track
@@ -271,7 +273,6 @@ def test_compute_metric():
     track9 = Track(
         states=[State([[9]], timestamp=tstart + datetime.timedelta(seconds=i))
                 for i in range(35, 40)])
-    manager.groundtruth_paths = truth
     manager.tracks = {track1, track2, track3, track4, track5, track6, track7,
                       track8, track9}
     manager.groundtruth_paths = {truth}
@@ -330,3 +331,77 @@ def test_compute_metric():
     assert ls.time_range.start_timestamp == tstart
     assert ls.time_range.end_timestamp == tend
     assert ls.generator == generator
+
+
+def test_no_truth_divide_by_zero():
+    manager = SimpleManager()
+    generator = SIAPMetrics()
+    # Create truth, tracks and associations, same as test_nu_j
+    tstart = datetime.datetime.now()
+    track1 = Track(
+        states=[State([[1]], timestamp=tstart + datetime.timedelta(seconds=i))
+                for i in range(3)])
+    track2 = Track(
+        states=[State([[2]], timestamp=tstart + datetime.timedelta(seconds=i))
+                for i in range(5, 10)])
+    track3 = Track(
+        states=[State([[3]], timestamp=tstart + datetime.timedelta(seconds=i))
+                for i in range(7, 15)])
+    track4 = Track(
+        states=[State([[4]], timestamp=tstart + datetime.timedelta(seconds=i))
+                for i in range(13, 20)])
+    track5 = Track(
+        states=[State([[5]], timestamp=tstart + datetime.timedelta(seconds=i))
+                for i in range(18, 28)])
+    track6 = Track(
+        states=[State([[6]], timestamp=tstart + datetime.timedelta(seconds=i))
+                for i in range(22, 26)])
+    track7 = Track(
+        states=[State([[7]], timestamp=tstart + datetime.timedelta(seconds=i))
+                for i in range(30, 40)])
+    track8 = Track(
+        states=[State([[8]], timestamp=tstart + datetime.timedelta(seconds=i))
+                for i in range(30, 35)])
+    track9 = Track(
+        states=[State([[9]], timestamp=tstart + datetime.timedelta(seconds=i))
+                for i in range(35, 40)])
+    manager.tracks = {track1, track2, track3, track4, track5, track6, track7,
+                      track8, track9}
+    manager.groundtruth_paths = set()
+    associations = {TimeRangeAssociation({track}, time_range=TimeRange(
+        start_timestamp=min([state.timestamp for state in track.states]),
+        end_timestamp=max([state.timestamp for state in track.states])))
+                    for track in manager.tracks}
+    manager.association_set = AssociationSet(associations)
+
+    with pytest.warns(UserWarning) as warning:
+        metrics = generator.compute_metric(manager)
+
+    assert warning[0].message.args[0] == "No truth to generate SIAP Metric"
+
+    assert len(metrics) == 5
+
+
+def test_no_track_divide_by_zero():
+    manager = SimpleManager()
+    generator = SIAPMetrics()
+    # Create truth, tracks and associations, same as test_nu_j
+    tstart = datetime.datetime.now()
+    truth = GroundTruthPath(states=[
+        GroundTruthState([[1]], timestamp=tstart + datetime.timedelta(
+            seconds=i))
+        for i in range(40)])
+    manager.tracks = set()
+    manager.groundtruth_paths = {truth}
+    associations = {TimeRangeAssociation({truth}, time_range=TimeRange(
+        start_timestamp=min([state.timestamp for state in track.states]),
+        end_timestamp=max([state.timestamp for state in track.states])))
+                    for track in manager.tracks}
+    manager.association_set = AssociationSet(associations)
+
+    with pytest.warns(UserWarning) as warning:
+        metrics = generator.compute_metric(manager)
+
+    assert warning[0].message.args[0] == "No tracks to generate SIAP Metric"
+
+    assert len(metrics) == 5