1414import importlib
1515import importlib .util
1616import pathlib
17- import pickle # nosec B403 - only used to round-trip graphs made in this file
17+ import pickle # nosec B403 nosemgrep - only used to round-trip graphs made in this file
1818import unittest
1919import unittest .mock
2020
2626HAVE_SCIPY = importlib .util .find_spec ("scipy" ) is not None
2727
2828
29+ def roundtrip (obj ):
30+ """Pickle and unpickle: these tests only read what they have just written"""
31+ return pickle .loads (pickle .dumps (obj )) # nosec B301 nosemgrep
32+
33+
2934def points_graph (cls = UGraph , n = 200 , dim = 2 , seed = 0 , ** kwargs ):
3035 rng = np .random .default_rng (seed )
3136 g = cls (dim = dim , ** kwargs )
@@ -228,7 +233,7 @@ def test_a_coordinate_changed_in_place_is_not_seen(self):
228233
229234 def test_not_in_copies_or_pickles (self ):
230235 g = self .graph ()
231- for h in (g .copy (), copy .deepcopy (g ), pickle . loads ( pickle . dumps ( g )) , g .subgraph (g )): # nosec B301
236+ for h in (g .copy (), copy .deepcopy (g ), roundtrip ( g ) , g .subgraph (g )):
232237 self .assertIsNone (h ._kdtree )
233238 self .assertEqual (h .closest ([9.0 , 0.0 ])[0 ].name , "b" )
234239
@@ -252,6 +257,67 @@ def test_no_coordinates(self):
252257 g .build_kdtree ()
253258
254259
260+ class TestVectorizedAndLoopPaths (unittest .TestCase ):
261+ """near() without an index: built-in metrics in one operation, others in a loop"""
262+
263+ def test_SE2_without_an_index (self ):
264+ for weight in (0.0 , 0.5 , 2.0 ):
265+ with self .subTest (weight = weight ):
266+ g , P = points_graph (UGraph , dim = 3 , metric = SE2Metric (weight ))
267+ for q in ([0.0 , 0.0 , 3.0 ], [4.0 , - 2.0 , - 3.0 ]):
268+ d = se2 (P - np .asarray (q ), weight )
269+ found = g .near (q , 5.0 )
270+ self .assertEqual (
271+ sorted (int (v .name [1 :]) for v , _ in found ),
272+ sorted (np .flatnonzero (d <= 5.0 ).tolist ()),
273+ )
274+ np .testing .assert_allclose ([x for _ , x in found ], np .sort (d [d <= 5.0 ]), atol = 1e-9 )
275+
276+ def test_metric_of_your_own (self ):
277+ g = UGraph (dim = 2 , metric = lambda v : float (np .abs (v ).max ())) # Chebyshev
278+ for name , c in [("a" , (0 , 0 )), ("b" , (2 , 2 )), ("c" , (3 , 0 )), ("d" , (0 , 5 ))]:
279+ g .add_vertex (list (c ), name = name )
280+ g .add_vertex (name = "nowhere" )
281+ found = g .near ([0.0 , 0.0 ], 3.0 )
282+ self .assertEqual ([(v .name , d ) for v , d in found ], [("a" , 0.0 ), ("b" , 2.0 ), ("c" , 3.0 )])
283+
284+ def test_vertices_without_coordinates (self ):
285+ g = UGraph ()
286+ g .add_vertex (name = "nowhere" )
287+ g .add_vertex (name = "nowhere else" )
288+ self .assertEqual (g .within ([[0 , 0 ], [1 , 1 ]]), [])
289+ self .assertEqual (g .near ([0 , 0 ], 5 ), [])
290+
291+ def test_closest_skips_vertices_without_coordinates (self ):
292+ g = UGraph (dim = 2 )
293+ g .add_vertex (name = "nowhere" )
294+ g .add_vertex ([3.0 , 4.0 ], name = "a" )
295+ vertex , d = g .closest ([0.0 , 0.0 ]) # no index: the loop
296+ self .assertEqual ((vertex .name , d ), ("a" , 5.0 ))
297+
298+
299+ @unittest .skipUnless (HAVE_SCIPY , "scipy is not installed" )
300+ class TestIndexGuards (unittest .TestCase ):
301+ def test_a_query_that_is_not_finite_falls_back (self ):
302+ g , _ = points_graph (UGraph , n = 20 )
303+ g .build_kdtree ()
304+ vertex , d = g .closest ([np .nan , 0.0 ]) # the loop: nothing is nearer than nan
305+ self .assertIsNone (vertex )
306+ self .assertEqual (d , np .inf )
307+
308+ def test_a_query_of_the_wrong_length_with_SE2 (self ):
309+ g , _ = points_graph (UGraph , n = 20 , dim = 3 , metric = "SE2" )
310+ g .build_kdtree ()
311+ with self .assertRaises (ValueError ): # the metric itself says so
312+ g .closest ([0.0 , 0.0 ])
313+
314+ def test_coordinates_that_are_none_finite (self ):
315+ g = UGraph (dim = 2 )
316+ g .add_vertex ([np .nan , np .nan ])
317+ with self .assertRaises (ValueError ):
318+ g .build_kdtree ()
319+
320+
255321class TestScipyIsOnlyImportedOnDemand (unittest .TestCase ):
256322 def test_no_module_of_pgraph_imports_scipy_at_import_time (self ):
257323 # spatialmath, which pgraph depends on, imports scipy itself, so
0 commit comments