Skip to content

Modify SquaredMahalanobis __call__ to accept a 2D state_vector in state2#1272

Open
ajland wants to merge 6 commits intodstl:mainfrom
ajland:vectorize_mahalanobis
Open

Modify SquaredMahalanobis __call__ to accept a 2D state_vector in state2#1272
ajland wants to merge 6 commits intodstl:mainfrom
ajland:vectorize_mahalanobis

Conversation

@ajland
Copy link
Copy Markdown

@ajland ajland commented Mar 30, 2026

This PR is based on this exchange: #1226 (comment)

The idea is to expand SquaredMahalanobis to compute one-to-many distances instead of the original one-to-one distance.

Original interface:

  • state1.state_vector has shape (n, 1)
  • state2.state_vector has shape (n, 1)
  • Returns the floating point squared Mahalanobis distance

New interface:

  • state1.state_vector has shape (n, 1)
  • state2.state_vector has shape (n, m)
  • If m=1, returns the same floating point value as the original. If m>1, returns an np.ndarray of Mahalanobis distances of shape (m,), comparing the distance between state1 and all states in state2.

In the original exchange, @sdhiscocks said we should also consider an (n, m) + (n, m) case. This is a possibility, but I was not sure how to implement this given that a GaussianState cannot have its seconds axis >1 due to the limitations of StateVector. Please advise on this latter point.

Modify SquaredMahalanobis __call__ to accept a 2D state_vector in state2
@ajland ajland requested a review from a team as a code owner March 30, 2026 05:11
@ajland ajland requested review from jswright-dstl and timothy-glover and removed request for a team March 30, 2026 05:11
@ajland
Copy link
Copy Markdown
Author

ajland commented Mar 30, 2026

Looking at this again this morning, I see that State, and therefore GaussianState, can accept a StateVectors object, which would mean that an (n, m) + (n, m) case might be feasible. I can look into this change. Should I assume that every (n, 1) vector in state1 shares the same covar? The resulting matrix would be an (n, n) matrix, where index i, j is the distance between vectors i and j from state1 and state2, respectively. The matrix should be symmetric.

Copy link
Copy Markdown
Member

@sdhiscocks sdhiscocks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @ajland.

Maybe we wont worry about (n, m) + (n, m) case for now, as you say it's more complex.

Some test failures here, but that's because test too strictly checking floats are equal. Using pytest.approx should fix that (patch below).

It would be good to get a new test that also checks the new case of state2.state_vector being (n, m)

diff --git i/stonesoup/measures/tests/test_state.py w/stonesoup/measures/tests/test_state.py
index 8a6887e5..4af68565 100644
--- i/stonesoup/measures/tests/test_state.py
+++ w/stonesoup/measures/tests/test_state.py
@@ -48,9 +48,9 @@ def test_euclideanweighted():
 
 def test_mahalanobis():
     measure = measures.Mahalanobis()
-    assert measure(state_u, state_v) == distance.mahalanobis(u[:, 0],
-                                                             v[:, 0],
-                                                             np.linalg.inv(ui))
+    assert measure(state_u, state_v) == pytest.approx(distance.mahalanobis(u[:, 0],
+                                                                           v[:, 0],
+                                                                           np.linalg.inv(ui)))
 
 
 def test_hellinger():
@@ -141,13 +141,13 @@ def test_hellinger_partial_mapping(mapping_type):
 def test_mahalanobis_full_mapping(mapping_type):
     mapping = mapping_type(np.arange(len(u)))
     measure = measures.Mahalanobis(mapping=mapping)
-    assert measure(state_u, state_v) == distance.mahalanobis(u[:, 0],
-                                                             v[:, 0],
-                                                             np.linalg.inv(ui))
+    assert measure(state_u, state_v) == pytest.approx(distance.mahalanobis(u[:, 0],
+                                                                           v[:, 0],
+                                                                           np.linalg.inv(ui)))
     measure = measures.Mahalanobis(mapping=mapping, mapping2=mapping)
-    assert measure(state_u, state_v) == distance.mahalanobis(u[:, 0],
-                                                             v[:, 0],
-                                                             np.linalg.inv(ui))
+    assert measure(state_u, state_v) == pytest.approx(distance.mahalanobis(u[:, 0],
+                                                                           v[:, 0],
+                                                                           np.linalg.inv(ui)))
 
 
 def test_mahalanobis_partial_mapping(mapping_type):

@ajland
Copy link
Copy Markdown
Author

ajland commented Apr 1, 2026

Added the following to address the requests and errors:

  • Altered np.einsum expression to not need np.dot
  • Applied the requested pytest.approx patch
  • Added tests to ensure the (n, 1) + (n, m) case gives expected results

@ajland ajland requested a review from sdhiscocks April 1, 2026 12:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants