Repository: dstl/Stone-Soup

Issue:
Possible issue with lru cache,  _pickle.PicklingError: 
This must be my setup, The first time I'm seeing lru_cache issues, I use multiprocessing for a lot of different projects, this is a first time error.

`Can't pickle <functools._lru_cache_wrapper object at 0xxxxx: it's not the same object as stonesoup.measures.SquaredMahalanobis._inv_cov`

==
```
  File "C:\ss\lib\python\lib\multiprocessing\process.py", line 121, in start
    self._popen = self._Popen(self)
  File "C:\ss\lib\python\lib\multiprocessing\context.py", line 224, in _Popen
    return _default_context.get_context().Process._Popen(process_obj)
  File "C:\ss\lib\python\lib\multiprocessing\context.py", line 327, in _Popen
    return Popen(process_obj)
  File "C:\ss\lib\python\lib\multiprocessing\popen_spawn_win32.py", line 93, in __init__
    reduction.dump(process_obj, to_child)
  File "C:\ss\lib\python\lib\multiprocessing\reduction.py", line 60, in dump
    ForkingPickler(file, protocol).dump(obj)
_pickle.PicklingError: Can't pickle <functools._lru_cache_wrapper object at 0x000001D130788040>: it's not the same object as stonesoup.measures.SquaredMahalanobis._inv_cov
```

Discussion:

Patch:
diff --git a/stonesoup/measures.py b/stonesoup/measures.py
index 5f910e48..c13395ca 100644
--- a/stonesoup/measures.py
+++ b/stonesoup/measures.py
@@ -1,3 +1,4 @@
+import copy
 from abc import abstractmethod
 from functools import lru_cache
 
@@ -169,6 +170,18 @@ class SquaredMahalanobis(Measure):
         if self.state_covar_inv_cache_size is None or self.state_covar_inv_cache_size > 0:
             self._inv_cov = lru_cache(maxsize=self.state_covar_inv_cache_size)(self._inv_cov)
 
+    def __getstate__(self):
+        result = copy.copy(self.__dict__)
+        result["_inv_cov"] = None
+        return result
+
+    def __setstate__(self, state):
+        self.__dict__ = state
+        if self.state_covar_inv_cache_size is None or self.state_covar_inv_cache_size > 0:
+            self._inv_cov = lru_cache(maxsize=self.state_covar_inv_cache_size)(type(self)._inv_cov)
+        else:
+            self._inv_cov = type(self)._inv_cov
+
     def __call__(self, state1, state2):
         r"""Calculate the Squared Mahalanobis distance between a pair of state objects
 


Test patch:
diff --git a/stonesoup/tests/test_measures.py b/stonesoup/tests/test_measures.py
index 981a1d83..c1cb221f 100644
--- a/stonesoup/tests/test_measures.py
+++ b/stonesoup/tests/test_measures.py
@@ -1,4 +1,5 @@
 import datetime
+import pickle
 
 import numpy as np
 import pytest
@@ -250,3 +251,26 @@ def test_euclideanweighted_partial_mapping(mapping_type):
     measure = measures.EuclideanWeighted(weight, mapping=mapping, mapping2=mapping2)
     assert measure(state_u, state_v) == \
         distance.euclidean([10, 1], [11, 2], weight)
+
+
+@pytest.mark.parametrize(
+    'measure,result',
+    [
+        (measures.Mahalanobis(), distance.mahalanobis(u[:, 0], v[:, 0], np.linalg.inv(ui))),
+        (measures.Mahalanobis(state_covar_inv_cache_size=0),
+         distance.mahalanobis(u[:, 0], v[:, 0], np.linalg.inv(ui))),
+        (measures.SquaredMahalanobis(),
+         distance.mahalanobis(u[:, 0], v[:, 0], np.linalg.inv(ui))**2),
+    ],
+    ids=['Mahalanobis', 'Mahalanobis-no-cache', 'SquaredMahalanobis'],
+)
+def test_mahalanobis_pickle(measure, result):
+    assert measure(state_u, state_v) == pytest.approx(result)
+    if measure.state_covar_inv_cache_size > 0:
+        assert measure._inv_cov.cache_info().currsize == 1
+
+    measure = pickle.loads(pickle.dumps(measure))
+    assert measure(state_u, state_v) == pytest.approx(result)
+    if measure.state_covar_inv_cache_size > 0:
+        assert measure._inv_cov.cache_info().hits == 0  # Cache not pickled currently
+        assert measure._inv_cov.cache_info().currsize == 1