From da1b94a5b46d3587b81beb858952a252ccd3932e Mon Sep 17 00:00:00 2001 From: sglvladi Date: Wed, 6 May 2020 10:24:36 +0100 Subject: [PATCH 1/5] Removed default gating from JPDA --- stonesoup/dataassociator/probability.py | 16 +++------------- .../dataassociator/tests/test_probability.py | 2 +- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/stonesoup/dataassociator/probability.py b/stonesoup/dataassociator/probability.py index cf2c5d101..de4b787c3 100644 --- a/stonesoup/dataassociator/probability.py +++ b/stonesoup/dataassociator/probability.py @@ -76,12 +76,6 @@ class JPDA(DataAssociator): hypothesiser = Property( PDAHypothesiser, doc="Generate a set of hypotheses for each prediction-detection pair") - gate_ratio = Property( - float, - doc="If probability of Detection/Track association is less than this " - "many times less than probability of MissedDetection, treat " - "probability of association as 0." - ) def associate(self, tracks, detections, time): """Associate detections with predicted states. @@ -109,7 +103,7 @@ def associate(self, tracks, detections, time): # enumerate the Joint Hypotheses of track/detection associations joint_hypotheses = \ - self.enumerate_JPDA_hypotheses(tracks, hypotheses, self.gate_ratio) + self.enumerate_JPDA_hypotheses(tracks, hypotheses) # Calculate MultiMeasurementHypothesis for each Track over all # available Detections with probabilities drawn from JointHypotheses @@ -156,7 +150,7 @@ def associate(self, tracks, detections, time): return new_hypotheses @classmethod - def enumerate_JPDA_hypotheses(cls, tracks, multihypths, gate_ratio): + def enumerate_JPDA_hypotheses(cls, tracks, multihypths): joint_hypotheses = list() @@ -171,13 +165,9 @@ def enumerate_JPDA_hypotheses(cls, tracks, multihypths, gate_ratio): for track in tracks: track_possible_assoc = list() - missed_probability = \ - multihypths[track].get_missed_detection_probability() - missed_gate = missed_probability/gate_ratio for hypothesis in multihypths[track]: # Always include missed detection (gate ratio < 1) - if not hypothesis or hypothesis.probability >= missed_gate: - track_possible_assoc.append(hypothesis) + track_possible_assoc.append(hypothesis) possible_assoc.append(track_possible_assoc) # enumerate all valid JPDA joint hypotheses diff --git a/stonesoup/dataassociator/tests/test_probability.py b/stonesoup/dataassociator/tests/test_probability.py index c12f96653..4e9dae230 100644 --- a/stonesoup/dataassociator/tests/test_probability.py +++ b/stonesoup/dataassociator/tests/test_probability.py @@ -15,7 +15,7 @@ def associator(request, probability_hypothesiser): if request.param is PDA: return request.param(probability_hypothesiser) elif request.param is JPDA: - return request.param(probability_hypothesiser, 5) + return request.param(probability_hypothesiser) def test_probability(associator): From fea3224e72253cca1790c412c262810240b08603 Mon Sep 17 00:00:00 2001 From: sglvladi Date: Wed, 6 May 2020 10:28:26 +0100 Subject: [PATCH 2/5] Added base Gater class --- stonesoup/gater/__init__.py | 4 ++++ stonesoup/gater/base.py | 14 ++++++++++++++ stonesoup/gater/tests/__init__.py | 0 3 files changed, 18 insertions(+) create mode 100644 stonesoup/gater/__init__.py create mode 100644 stonesoup/gater/base.py create mode 100644 stonesoup/gater/tests/__init__.py diff --git a/stonesoup/gater/__init__.py b/stonesoup/gater/__init__.py new file mode 100644 index 000000000..5cbee0616 --- /dev/null +++ b/stonesoup/gater/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +from .base import Gater + +__all__ = ['Gater'] diff --git a/stonesoup/gater/base.py b/stonesoup/gater/base.py new file mode 100644 index 000000000..8df97b15d --- /dev/null +++ b/stonesoup/gater/base.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +from ..base import Property +from ..hypothesiser import Hypothesiser + + +class Gater(Hypothesiser): + """Gater base class + + Gaters wrap :class:`.Hypothesiser` objects and can be used to modify (typically reduce) the + returned hypotheses. + """ + + hypothesiser = Property( + Hypothesiser, doc="Hypothesiser that is being wrapped.") diff --git a/stonesoup/gater/tests/__init__.py b/stonesoup/gater/tests/__init__.py new file mode 100644 index 000000000..e69de29bb From 49529d73af977083eba46275db3c496368105904 Mon Sep 17 00:00:00 2001 From: sglvladi Date: Wed, 6 May 2020 12:39:50 +0100 Subject: [PATCH 3/5] Changed FilteredDetectionsHypothesiser to FilteredDetectionsGater --- stonesoup/{hypothesiser => gater}/filtered.py | 6 ++--- stonesoup/gater/tests/conftest.py | 25 +++++++++++++++++++ .../tests/test_filtered.py | 15 ++++++----- 3 files changed, 34 insertions(+), 12 deletions(-) rename stonesoup/{hypothesiser => gater}/filtered.py (90%) create mode 100644 stonesoup/gater/tests/conftest.py rename stonesoup/{hypothesiser => gater}/tests/test_filtered.py (94%) diff --git a/stonesoup/hypothesiser/filtered.py b/stonesoup/gater/filtered.py similarity index 90% rename from stonesoup/hypothesiser/filtered.py rename to stonesoup/gater/filtered.py index 3ef0bdc97..6c33c0d87 100644 --- a/stonesoup/hypothesiser/filtered.py +++ b/stonesoup/gater/filtered.py @@ -1,17 +1,15 @@ # -*- coding: utf-8 -*- -from .base import Hypothesiser +from .base import Gater from ..base import Property -class FilteredDetectionsHypothesiser(Hypothesiser): +class FilteredDetectionsGater(Gater): """Wrapper for Hypothesisers - filters input data Wrapper for any type of hypothesiser - filters the 'detections' before they are fed into the hypothesiser. """ - hypothesiser = Property( - Hypothesiser, doc="Hypothesiser that is being wrapped.") metadata_filter = Property( str, doc="Metadata attribute used to filter which detections " "tracks are valid for association.") diff --git a/stonesoup/gater/tests/conftest.py b/stonesoup/gater/tests/conftest.py new file mode 100644 index 000000000..83bdef05a --- /dev/null +++ b/stonesoup/gater/tests/conftest.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +import pytest + +from ...types.prediction import ( + GaussianMeasurementPrediction, GaussianStatePrediction) + + +@pytest.fixture() +def predictor(): + class TestGaussianPredictor: + def predict(self, prior, control_input=None, timestamp=None, **kwargs): + return GaussianStatePrediction(prior.state_vector + 1, + prior.covar * 2, timestamp) + return TestGaussianPredictor() + + +@pytest.fixture() +def updater(): + class TestGaussianUpdater: + def predict_measurement(self, state_prediction, + measurement_model=None, **kwargs): + return GaussianMeasurementPrediction(state_prediction.state_vector, + state_prediction.covar, + state_prediction.timestamp) + return TestGaussianUpdater() diff --git a/stonesoup/hypothesiser/tests/test_filtered.py b/stonesoup/gater/tests/test_filtered.py similarity index 94% rename from stonesoup/hypothesiser/tests/test_filtered.py rename to stonesoup/gater/tests/test_filtered.py index 2fd7e6f5d..d62cb135e 100644 --- a/stonesoup/hypothesiser/tests/test_filtered.py +++ b/stonesoup/gater/tests/test_filtered.py @@ -1,10 +1,9 @@ import datetime -from operator import attrgetter - import numpy as np +from operator import attrgetter -from ..distance import DistanceHypothesiser -from ..filtered import FilteredDetectionsHypothesiser +from ..filtered import FilteredDetectionsGater +from ...hypothesiser.distance import DistanceHypothesiser from ...types.detection import Detection from ...types.hypothesis import SingleHypothesis from ...types.track import Track @@ -24,7 +23,7 @@ def test_filtereddetections(predictor, updater): hypothesiser = DistanceHypothesiser( predictor, updater, measure=measure, missed_distance=0.2, include_all=True) - hypothesiser_wrapper = FilteredDetectionsHypothesiser( + hypothesiser_wrapper = FilteredDetectionsGater( hypothesiser, "MMSI", match_missing=True) track = Track([GaussianStateUpdate( @@ -63,7 +62,7 @@ def test_filtereddetections_empty_detections(predictor, updater): timestamp = datetime.datetime.now() hypothesiser = DistanceHypothesiser(predictor, updater, measure=measure, missed_distance=0.2) - hypothesiser_wrapper = FilteredDetectionsHypothesiser( + hypothesiser_wrapper = FilteredDetectionsGater( hypothesiser, "MMSI", match_missing=False) track = Track([GaussianStateUpdate( @@ -94,7 +93,7 @@ def test_filtereddetections_no_track_metadata(predictor, updater): hypothesiser = DistanceHypothesiser( predictor, updater, measure=measure, missed_distance=0.2, include_all=True) - hypothesiser_wrapper = FilteredDetectionsHypothesiser( + hypothesiser_wrapper = FilteredDetectionsGater( hypothesiser, "MMSI", match_missing=True) track = Track([GaussianStateUpdate( @@ -135,7 +134,7 @@ def test_filtereddetections_no_matching_metadata(predictor, updater): timestamp = datetime.datetime.now() hypothesiser = DistanceHypothesiser(predictor, updater, measure=measure, missed_distance=0.2) - hypothesiser_wrapper = FilteredDetectionsHypothesiser( + hypothesiser_wrapper = FilteredDetectionsGater( hypothesiser, "MMSI", match_missing=True) track = Track([GaussianStateUpdate( From 5f250e115e5178116555d0619aa976a935dea090 Mon Sep 17 00:00:00 2001 From: sglvladi Date: Wed, 6 May 2020 12:40:08 +0100 Subject: [PATCH 4/5] Added DistanceGater --- stonesoup/gater/distance.py | 31 ++++++++++++ stonesoup/gater/tests/test_distance.py | 67 ++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 stonesoup/gater/distance.py create mode 100644 stonesoup/gater/tests/test_distance.py diff --git a/stonesoup/gater/distance.py b/stonesoup/gater/distance.py new file mode 100644 index 000000000..783665c64 --- /dev/null +++ b/stonesoup/gater/distance.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +from ..base import Property +from ..measures import Measure +from ..types.multihypothesis import MultipleHypothesis +from .base import Gater + + +class DistanceGater(Gater): + """ Distance based gater + + Uses a measure to calculate the distance between a hypothesis' measurement prediction and the + hypothised measurement, then removes any hypotheses whose calculated distance exceeds the + specified gate threshold. + """ + measure = Property(Measure, + doc="Measure class used to calculate the distance between the measurement " + "prediction and the hypothesised measurement.") + gate_threshold = Property(float, + doc="The gate threshold. Hypotheses whose calculated distance " + "exceeds this threshold will be filtered out.") + + def hypothesise(self, track, detections, *args, **kwargs): + + hypotheses = self.hypothesiser.hypothesise(track, detections, *args, **kwargs) + + gated_hypotheses = [hypothesis for hypothesis in hypotheses + if (not hypothesis + or self.measure(hypothesis.measurement_prediction, + hypothesis.measurement) < self.gate_threshold)] + + return MultipleHypothesis(sorted(gated_hypotheses, reverse=True)) diff --git a/stonesoup/gater/tests/test_distance.py b/stonesoup/gater/tests/test_distance.py new file mode 100644 index 000000000..a38ff19dc --- /dev/null +++ b/stonesoup/gater/tests/test_distance.py @@ -0,0 +1,67 @@ +import datetime +import pytest +import numpy as np + +from ..distance import DistanceGater +from ...hypothesiser.probability import PDAHypothesiser +from ...types.detection import Detection +from ...types.hypothesis import SingleHypothesis +from ...types.track import Track +from ...types.update import GaussianStateUpdate +from ... import measures as measures + +measure = measures.Mahalanobis() + + +@pytest.mark.parametrize( + "detections, gate_threshold, num_gated", + [ + ( # Test 1 + {Detection(np.array([[2]])), Detection(np.array([[3]])), Detection(np.array([[6]])), + Detection(np.array([[0]])), Detection(np.array([[-1]])), Detection(np.array([[-4]]))}, + 1, + 3 + ), + ( # Test 2 + {Detection(np.array([[2]])), Detection(np.array([[3]])), Detection(np.array([[6]])), + Detection(np.array([[0]])), Detection(np.array([[-1]])), Detection(np.array([[-4]]))}, + 2, + 5 + ), + ( # Test 3 + {Detection(np.array([[2]])), Detection(np.array([[3]])), Detection(np.array([[6]])), + Detection(np.array([[0]])), Detection(np.array([[-1]])), Detection(np.array([[-4]]))}, + 4, + 7 + ) + ], + ids=["test1", "test2", "test3"] +) +def test_distance(predictor, updater, detections, gate_threshold, num_gated): + + timestamp = datetime.datetime.now() + + hypothesiser = PDAHypothesiser(predictor, updater, clutter_spatial_density=0.000001) + gater = DistanceGater(hypothesiser, measure=measure, gate_threshold=gate_threshold) + + track = Track([GaussianStateUpdate( + np.array([[0]]), + np.array([[1]]), + SingleHypothesis( + None, + Detection(np.array([[0]]), metadata={"MMSI": 12345})), + timestamp=timestamp)]) + + hypotheses = gater.hypothesise(track, detections, timestamp) + + # The number of gated hypotheses matches the expected + assert len(hypotheses) == num_gated + + # The gated hypotheses are either the null hypothesis or their distance is less than the set + # gate threshold + assert all(not hypothesis.measurement or + measure(hypothesis.measurement_prediction, hypothesis.measurement) < gate_threshold + for hypothesis in hypotheses) + + # There is a SINGLE missed detection hypothesis + assert len([hypothesis for hypothesis in hypotheses if not hypothesis]) == 1 From 0210713e4b62f664cd30c262e3fcaa895bfd3203 Mon Sep 17 00:00:00 2001 From: sglvladi Date: Wed, 6 May 2020 15:13:10 +0100 Subject: [PATCH 5/5] Added documentation --- docs/source/stonesoup.gater.rst | 20 ++++++++++++++++++++ docs/source/stonesoup.rst | 1 + 2 files changed, 21 insertions(+) create mode 100644 docs/source/stonesoup.gater.rst diff --git a/docs/source/stonesoup.gater.rst b/docs/source/stonesoup.gater.rst new file mode 100644 index 000000000..f1b24924c --- /dev/null +++ b/docs/source/stonesoup.gater.rst @@ -0,0 +1,20 @@ +Gater +===== + +.. automodule:: stonesoup.gater + :no-members: + +.. automodule:: stonesoup.gater.base + :show-inheritance: + +Distance +-------- + +.. automodule:: stonesoup.gater.distance + :show-inheritance: + +Filtered +-------- + +.. automodule:: stonesoup.gater.filtered + :show-inheritance: diff --git a/docs/source/stonesoup.rst b/docs/source/stonesoup.rst index 8fb8eb0af..bec56ea91 100644 --- a/docs/source/stonesoup.rst +++ b/docs/source/stonesoup.rst @@ -29,6 +29,7 @@ Tracker Components stonesoup.dataassociator stonesoup.deleter + stonesoup.gater stonesoup.hypothesiser stonesoup.initiator stonesoup.mixturereducer