From 496280c2c736c406fab6288799ac939d2c0253f4 Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Sat, 7 Dec 2024 15:53:01 +0000 Subject: [PATCH 1/4] Scale points for Voronoi plots --- pybop/plot/voronoi.py | 78 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/pybop/plot/voronoi.py b/pybop/plot/voronoi.py index 4dad41223..042cf5379 100644 --- a/pybop/plot/voronoi.py +++ b/pybop/plot/voronoi.py @@ -194,9 +194,41 @@ def interpolate_point(p, q, axis, boundary_val): return np.array([boundary_val, s]) if axis == 0 else np.array([s, boundary_val]) +def assign_nearest_value(x, y, f, xi, yi): + """ + Computes an array of values given by the score of the nearest point. + + Parameters + ---------- + x : array-like + The x coordinates of points with known scores. + y : array-like + The y coordinates of points with known scores. + f : array-like + The score function at the given x and y coordinates. + xi : array-like + The x coordinates of grid points. + yi : array-like + The y coordinates of grid points. + + Returns + ------- + A numpy array containing the scores corresponding to the grid points. + """ + # Create a KD-tree for efficient nearest neighbor search + tree = cKDTree(np.column_stack((x, y))) + + # Find the nearest point for each grid point + _, indices = tree.query(np.column_stack((xi.ravel(), yi.ravel()))) + zi = f[indices].reshape(xi.shape) + + return zi + + def surface( optim: Union[BaseOptimiser, Optimisation], bounds=None, + normalised_distance_metric=True, resolution=250, show=True, **layout_kwargs, @@ -211,6 +243,9 @@ def surface( bounds : numpy.ndarray, optional A 2x2 array specifying the [min, max] bounds for each parameter. If None, uses `cost.parameters.get_bounds_for_plotly`. + normalised_distance_metric : bool, optional + If True, the voronoi regions are computed using the Euclidean distance between + points normalised with respect to the bounds (default: True). resolution : int, optional Resolution of the plot. Default is 500. show : bool, optional @@ -235,20 +270,47 @@ def surface( bounds if bounds is not None else [param.bounds for param in optim.parameters] )[:2] - # Compute regions - x, y, f, regions = _voronoi_regions(x_optim, y_optim, f, xlim, ylim) - # Create a grid for plot xi = np.linspace(xlim[0], xlim[1], resolution) yi = np.linspace(ylim[0], ylim[1], resolution) xi, yi = np.meshgrid(xi, yi) - # Create a KD-tree for efficient nearest neighbor search - tree = cKDTree(np.column_stack((x, y))) + if normalised_distance_metric: + # Normalise the region + x_range: float = xlim[1] - xlim[0] + y_range: float = ylim[1] - ylim[0] - # Find the nearest point for each grid point - _, indices = tree.query(np.column_stack((xi.ravel(), yi.ravel()))) - zi = f[indices].reshape(xi.shape) + norm_x_optim = (np.asarray(x_optim) - xlim[0]) / x_range + norm_y_optim = (np.asarray(y_optim) - ylim[0]) / y_range + + # Compute regions + norm_x, norm_y, f, norm_regions = _voronoi_regions( + norm_x_optim, norm_y_optim, f, (0, 1), (0, 1) + ) + + # Create a normalised grid + norm_xi = np.linspace(0, 1, resolution) + norm_xi, norm_yi = np.meshgrid(norm_xi, norm_xi) + + # Assign a value to each point in the grid + zi = assign_nearest_value(norm_x, norm_y, f, norm_xi, norm_yi) + + # Rescale for plotting + x = norm_x * x_range + xlim[0] + y = norm_y * y_range + ylim[0] + regions = [] + for norm_region in norm_regions: + region = np.empty_like(norm_region) + region[:, 0] = norm_region[:, 0] * x_range + xlim[0] + region[:, 1] = norm_region[:, 1] * y_range + ylim[0] + regions.append(region) + + else: + # Compute regions + x, y, f, regions = _voronoi_regions(x_optim, y_optim, f, xlim, ylim) + + # Assign a value to each point in the grid + zi = assign_nearest_value(x, y, f, xi, yi) # Calculate the size of each Voronoi region region_sizes = np.array([len(region) for region in regions]) From 18ad3bfd4e779321c3c043d2dc2116329b735d5f Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Mon, 9 Dec 2024 13:33:58 +0000 Subject: [PATCH 2/4] Update test_plots.py --- tests/unit/test_plots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_plots.py b/tests/unit/test_plots.py index 733b81d42..9cf30a9e5 100644 --- a/tests/unit/test_plots.py +++ b/tests/unit/test_plots.py @@ -147,7 +147,7 @@ def test_optim_plots(self, optim): pybop.plot.contour(optim, gradient=True, steps=5) # Plot voronoi - pybop.plot.surface(optim) + pybop.plot.surface(optim, normalised_distance_metric=False) # Plot voronoi w/ bounds pybop.plot.surface(optim, bounds=bounds) From 8a6847c6a0b98b37aaec84875e1435b9d3706030 Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Wed, 11 Dec 2024 12:05:58 +0000 Subject: [PATCH 3/4] Rename argument and test bounds --- pybop/plot/voronoi.py | 20 ++++++++++---------- tests/unit/test_plots.py | 7 ++++++- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/pybop/plot/voronoi.py b/pybop/plot/voronoi.py index 042cf5379..9283251dc 100644 --- a/pybop/plot/voronoi.py +++ b/pybop/plot/voronoi.py @@ -228,7 +228,7 @@ def assign_nearest_value(x, y, f, xi, yi): def surface( optim: Union[BaseOptimiser, Optimisation], bounds=None, - normalised_distance_metric=True, + normalise=True, resolution=250, show=True, **layout_kwargs, @@ -243,7 +243,7 @@ def surface( bounds : numpy.ndarray, optional A 2x2 array specifying the [min, max] bounds for each parameter. If None, uses `cost.parameters.get_bounds_for_plotly`. - normalised_distance_metric : bool, optional + normalise : bool, optional If True, the voronoi regions are computed using the Euclidean distance between points normalised with respect to the bounds (default: True). resolution : int, optional @@ -275,11 +275,13 @@ def surface( yi = np.linspace(ylim[0], ylim[1], resolution) xi, yi = np.meshgrid(xi, yi) - if normalised_distance_metric: - # Normalise the region - x_range: float = xlim[1] - xlim[0] - y_range: float = ylim[1] - ylim[0] + if normalise: + if xlim[1] <= xlim[0] or ylim[1] <= ylim[0]: + raise ValueError("Lower bounds must be strictly less than upper bounds.") + # Normalise the region + x_range = xlim[1] - xlim[0] + y_range = ylim[1] - ylim[0] norm_x_optim = (np.asarray(x_optim) - xlim[0]) / x_range norm_y_optim = (np.asarray(y_optim) - ylim[0]) / y_range @@ -296,8 +298,6 @@ def surface( zi = assign_nearest_value(norm_x, norm_y, f, norm_xi, norm_yi) # Rescale for plotting - x = norm_x * x_range + xlim[0] - y = norm_y * y_range + ylim[0] regions = [] for norm_region in norm_regions: region = np.empty_like(norm_region) @@ -314,7 +314,7 @@ def surface( # Calculate the size of each Voronoi region region_sizes = np.array([len(region) for region in regions]) - normalized_sizes = (region_sizes - region_sizes.min()) / ( + relative_sizes = (region_sizes - region_sizes.min()) / ( region_sizes.max() - region_sizes.min() ) @@ -334,7 +334,7 @@ def surface( ) # Add Voronoi edges - for region, size in zip(regions, normalized_sizes): + for region, size in zip(regions, relative_sizes): x_region = region[:, 0].tolist() + [region[0, 0]] y_region = region[:, 1].tolist() + [region[0, 1]] diff --git a/tests/unit/test_plots.py b/tests/unit/test_plots.py index 9cf30a9e5..869f89dfb 100644 --- a/tests/unit/test_plots.py +++ b/tests/unit/test_plots.py @@ -147,11 +147,16 @@ def test_optim_plots(self, optim): pybop.plot.contour(optim, gradient=True, steps=5) # Plot voronoi - pybop.plot.surface(optim, normalised_distance_metric=False) + pybop.plot.surface(optim, normalise=False) # Plot voronoi w/ bounds pybop.plot.surface(optim, bounds=bounds) + with pytest.raises( + ValueError, match="Lower bounds must be strictly less than upper bounds." + ): + pybop.plot.surface(optim, bounds=[[0.5, 0.8], [0.7, 0.4]]) + @pytest.fixture def posterior_summary(self, fitting_problem): posterior = pybop.LogPosterior( From 340b23b7c34c5610ca90f736c18f0f624af144b0 Mon Sep 17 00:00:00 2001 From: Brady Planden Date: Wed, 11 Dec 2024 13:33:37 +0000 Subject: [PATCH 4/4] fix: extend multistart unit test for failing XNES final cost --- tests/unit/test_optimisation.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_optimisation.py b/tests/unit/test_optimisation.py index b3c094d0c..ba22828bc 100644 --- a/tests/unit/test_optimisation.py +++ b/tests/unit/test_optimisation.py @@ -163,7 +163,7 @@ def check_bounds_handling(optim, expected_bounds, should_raise=False): with pytest.raises( ValueError, match="Either all bounds or no bounds must be set" ): - optim = optimiser(cost=cost, bounds=expected_bounds) + optimiser(cost=cost, bounds=expected_bounds) else: assert optim.bounds == expected_bounds @@ -180,8 +180,9 @@ def check_multistart(optim, n_iters, multistarts): assert_log_update(optim) check_incorrect_update(optim) - multistart_optim = optimiser(cost, multistart=2, max_iterations=2) - check_multistart(multistart_optim, 2, 2) + # Test multistart + multistart_optim = optimiser(cost, multistart=2, max_iterations=6) + check_multistart(multistart_optim, 6, 2) if optimiser in [pybop.GradientDescent, pybop.Adam, pybop.NelderMead]: optim = optimiser(cost=cost, bounds=cost_bounds)