Repository: dstl/Stone-Soup

Issue:
GuassianModel rvs method generates noise from identity if covariance is None-type
Would it be appropriate to handle `covar = None` as 'noiseless'?
Currently, using the scipy.stats.multivariate_normal.rvs method will default a covariance to the identity matrix. Therefore, when passing in None, noise is generated.

Discussion:

Patch:
diff --git a/stonesoup/models/base.py b/stonesoup/models/base.py
index 85d048d5..7ed4634d 100644
--- a/stonesoup/models/base.py
+++ b/stonesoup/models/base.py
@@ -188,8 +188,14 @@ class GaussianModel(Model):
             distribution.
         """
 
+        covar = self.covar(**kwargs)
+
+        # If model has None-type covariance or contains None, it does not represent a Gaussian
+        if covar is None or None in covar:
+            raise ValueError("Cannot generate rvs from None-type covariance")
+
         noise = multivariate_normal.rvs(
-            np.zeros(self.ndim), self.covar(**kwargs), num_samples)
+            np.zeros(self.ndim), covar, num_samples)
 
         noise = np.atleast_2d(noise)
 
@@ -227,11 +233,17 @@ class GaussianModel(Model):
             The likelihood of ``state1``, given ``state2``
         """
 
+        covar = self.covar(**kwargs)
+
+        # If model has None-type covariance or contains None, it does not represent a Gaussian
+        if covar is None or None in covar:
+            raise ValueError("Cannot generate pdf from None-type covariance")
+
         # Calculate difference before to handle custom types (mean defaults to zero)
         # This is required as log pdf coverts arrays to floats
         likelihood = multivariate_normal.logpdf(
             (state1.state_vector - self.function(state2, **kwargs)).ravel(),
-            cov=self.covar(**kwargs)
+            cov=covar
         )
         return Probability(likelihood, log_value=True)
 


Test patch:
diff --git a/stonesoup/models/measurement/tests/test_combined.py b/stonesoup/models/measurement/tests/test_combined.py
index eb73837c..e27bd8ee 100644
--- a/stonesoup/models/measurement/tests/test_combined.py
+++ b/stonesoup/models/measurement/tests/test_combined.py
@@ -107,3 +107,15 @@ def test_mismatch_ndim_state():
             CartesianToBearingRange(3, [0, 1], np.diag([1, 10])),
             CartesianToBearingRange(4, [0, 1], np.diag([1, 10])),
         ])
+
+
+def test_none_covar():
+    new_model = CombinedReversibleGaussianMeasurementModel([
+        CartesianToBearingRange(4, [0, 1], None),
+        CartesianToBearingRange(4, [0, 1], np.diag([1, 10]))
+    ])
+
+    with pytest.raises(ValueError, match="Cannot generate rvs from None-type covariance"):
+        new_model.rvs()
+    with pytest.raises(ValueError, match="Cannot generate pdf from None-type covariance"):
+        new_model.pdf(State([0, 0, 0, 0]), State([0, 0, 0, 0]))
diff --git a/stonesoup/models/measurement/tests/test_models.py b/stonesoup/models/measurement/tests/test_models.py
index e3defa13..8fc8ae28 100644
--- a/stonesoup/models/measurement/tests/test_models.py
+++ b/stonesoup/models/measurement/tests/test_models.py
@@ -9,6 +9,7 @@ from ..nonlinear import (
     CartesianToElevationBearing, Cartesian2DToBearing, CartesianToBearingRangeRate,
     CartesianToElevationBearingRangeRate)
 from ...base import ReversibleModel
+from ...measurement.linear import LinearGaussian
 from ....functions import jacobian as compute_jac
 from ....functions import pol2cart
 from ....functions import rotz, rotx, roty, cart2sphere
@@ -79,6 +80,22 @@ def hbearing(state_vector, pos_map, translation_offset, rotation_offset):
     return StateVector([Elevation(theta), Bearing(phi)])
 
 
+@pytest.mark.parametrize(
+    "model_class",
+    [LinearGaussian,
+     CartesianToElevationBearingRange,
+     CartesianToBearingRange,
+     CartesianToElevationBearing,
+     Cartesian2DToBearing,
+     CartesianToBearingRangeRate,
+     CartesianToElevationBearingRangeRate]
+)
+def test_none_covar(model_class):
+    model = model_class(ndim_state=0, mapping=None, noise_covar=None)
+    with pytest.raises(ValueError, match="Cannot generate pdf from None-type covariance"):
+        model.pdf(State([0]), State([0]))
+
+
 @pytest.mark.parametrize(
     "h, ModelClass, state_vec, R , mapping,\
      translation_offset, rotation_offset",
diff --git a/stonesoup/models/transition/tests/test_time_invariant.py b/stonesoup/models/transition/tests/test_time_invariant.py
index 48cd6b70..be4870c6 100644
--- a/stonesoup/models/transition/tests/test_time_invariant.py
+++ b/stonesoup/models/transition/tests/test_time_invariant.py
@@ -5,6 +5,7 @@ import numpy as np
 
 from ..linear import LinearGaussianTimeInvariantTransitionModel
 from ....types.state import State
+import pytest
 
 
 def test_linear_gaussian():
@@ -23,3 +24,9 @@ def test_linear_gaussian():
                                               noise=np.zeros([3, 1])))
     assert isinstance(model.rvs(), np.ndarray)
     assert isinstance(model.pdf(State(x_2), State(x_1)), Real)
+
+    model = LinearGaussianTimeInvariantTransitionModel(transition_matrix=F, covariance_matrix=None)
+    with pytest.raises(ValueError, match="Cannot generate rvs from None-type covariance"):
+        model.rvs()
+    with pytest.raises(ValueError, match="Cannot generate pdf from None-type covariance"):
+        model.pdf(State([0]), State([0]))