diff --git a/src/roboticstoolbox/mobile/ParticleFilter.py b/src/roboticstoolbox/mobile/ParticleFilter.py index bee0e69c0..9e84361f0 100644 --- a/src/roboticstoolbox/mobile/ParticleFilter.py +++ b/src/roboticstoolbox/mobile/ParticleFilter.py @@ -15,7 +15,6 @@ import matplotlib.pyplot as plt import numpy as np -import scipy as sp import spatialmath.base as smb from matplotlib import animation from matplotlib.axes import Axes @@ -696,21 +695,15 @@ def _select(self) -> None: # # particles with large weights will occupy a greater percentage of the # y axis in a cummulative plot - cdf = np.cumsum(self.weight) / self.weight.sum() + cdf = np.cumsum(self.weight) + cdf = cdf / cdf[-1] # so randomly (uniform) choosing y values is more likely to correspond to # better particles... iselect = self.random.uniform(0, 1, size=(self.nparticles,)) # find the particle that corresponds to each y value (just a look up) - interpfun = sp.interpolate.interp1d( - cdf, - np.arange(self.nparticles), - assume_sorted=True, - kind="nearest", - fill_value="extrapolate", - ) - inextgen = interpfun(iselect).astype(int) + inextgen = np.searchsorted(cdf, iselect, side="right") # copy selected particles for next generation.. self.x = self.x[inextgen, :] diff --git a/tests/test_particle_filter_api.py b/tests/test_particle_filter_api.py index f94c8ebfc..3dfa57d52 100644 --- a/tests/test_particle_filter_api.py +++ b/tests/test_particle_filter_api.py @@ -32,6 +32,24 @@ def test_map_is_the_map_of_the_sensor() -> None: assert pf.map is pf.sensor.map +@pytest.mark.parametrize("first_weight", [0.2, 0.8]) +def test_resampling_uses_weighted_cdf_intervals(first_weight: float) -> None: + pf = _filter() + particles = np.column_stack( + (np.arange(pf.nparticles), np.zeros((pf.nparticles, 2))) + ) + pf.x = particles.copy() + pf.weight = np.zeros(pf.nparticles) + pf.weight[[0, -1]] = [first_weight, 1 - first_weight] + pf._random = np.random.default_rng(47) + draws = np.random.default_rng(47).uniform(0, 1, size=pf.nparticles) + expected_indices = np.where(draws < first_weight, 0, pf.nparticles - 1) + + pf._select() + + np.testing.assert_array_equal(pf.x, particles[expected_indices]) + + @pytest.mark.parametrize("verbose", [False, True]) def test_verbose_is_what_was_asked(verbose: bool) -> None: assert _filter(verbose=verbose).verbose is verbose