diff --git a/emukit/core/initial_designs/latin_design.py b/emukit/core/initial_designs/latin_design.py index 5c29c69c..4ad58f04 100644 --- a/emukit/core/initial_designs/latin_design.py +++ b/emukit/core/initial_designs/latin_design.py @@ -6,11 +6,7 @@ import numpy as np - -try: - import pyDOE -except ImportError: - raise ImportError("pyDOE needs to be installed in order to use latin design") +from scipy.stats import qmc from .. import ParameterSpace from .base import InitialDesignBase @@ -20,7 +16,8 @@ class LatinDesign(InitialDesignBase): """ Latin hypercube experiment design. - Based on pyDOE implementation. For further reference see https://pythonhosted.org/pyDOE/randomized.html#latin-hypercube + Based on scipy implementation. For further reference see + https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.qmc.LatinHypercube.html """ def __init__(self, parameter_space: ParameterSpace) -> None: @@ -37,15 +34,14 @@ def get_samples(self, point_count: int) -> np.ndarray: :return: A numpy array of generated samples, shape (point_count x space_dim) """ bounds = self.parameter_space.get_bounds() - X_design_aux = pyDOE.lhs(len(bounds), point_count, criterion="center") - ones = np.ones((X_design_aux.shape[0], 1)) - - lower_bound = np.asarray(bounds)[:, 0].reshape(1, len(bounds)) - upper_bound = np.asarray(bounds)[:, 1].reshape(1, len(bounds)) - diff = upper_bound - lower_bound + d = len(bounds) + lower_bounds = [x[0] for x in bounds] + upper_bounds = [x[1] for x in bounds] - X_design = np.dot(ones, lower_bound) + X_design_aux * np.dot(ones, diff) + sampler = qmc.LatinHypercube(d) + samples = sampler.random(n=point_count) + samples = qmc.scale(samples, lower_bounds, upper_bounds) - samples = self.parameter_space.round(X_design) + X_design = self.parameter_space.round(samples) - return samples + return X_design diff --git a/emukit/core/initial_designs/sobol_design.py b/emukit/core/initial_designs/sobol_design.py index b1c8d2fa..c05c2c94 100644 --- a/emukit/core/initial_designs/sobol_design.py +++ b/emukit/core/initial_designs/sobol_design.py @@ -1,20 +1,12 @@ -# Copyright 2020-2024 The Emukit Authors. All Rights Reserved. +# Copyright 2020-2026 The Emukit Authors. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 # Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 - - import numpy as np - -try: - from sobol_seq import i4_sobol_generate -except ImportError: - raise ImportError("sobol_seq needs to be installed in order to use sobol design") +from scipy.stats import qmc from .. import ParameterSpace from .base import InitialDesignBase @@ -23,7 +15,8 @@ class SobolDesign(InitialDesignBase): """ Sobol experiment design. - Based on sobol_seq implementation. For further reference see https://github.com/naught101/sobol_seq + Based on scipy implementation. For further reference see + https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.qmc.Sobol.html """ def __init__(self, parameter_space: ParameterSpace) -> None: @@ -40,12 +33,14 @@ def get_samples(self, point_count: int) -> np.ndarray: :return: A numpy array of generated samples, shape (point_count x space_dim) """ bounds = self.parameter_space.get_bounds() - lower_bound = np.asarray(bounds)[:, 0].reshape(1, len(bounds)) - upper_bound = np.asarray(bounds)[:, 1].reshape(1, len(bounds)) - diff = upper_bound - lower_bound + d = len(bounds) + lower_bounds = [x[0] for x in bounds] + upper_bounds = [x[1] for x in bounds] - X_design = np.dot(i4_sobol_generate(len(bounds), point_count), np.diag(diff[0, :])) + lower_bound + sampler = qmc.Sobol(d) + samples = sampler.random(n=point_count) + samples = qmc.scale(samples, lower_bounds, upper_bounds) - samples = self.parameter_space.round(X_design) + X_design = self.parameter_space.round(samples) - return samples + return X_design diff --git a/pyproject.toml b/pyproject.toml index b378325f..8848a9bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,8 +31,6 @@ dependencies = [ "scipy", "matplotlib>=3.9", "emcee>=2.2.1", - "pydoe<0.9.6", - "sobol_seq>=0.1.2", ] [project.optional-dependencies] diff --git a/requirements/integration_test_requirements.txt b/requirements/integration_test_requirements.txt index 167d3b0d..fa7a75ec 100644 --- a/requirements/integration_test_requirements.txt +++ b/requirements/integration_test_requirements.txt @@ -2,8 +2,6 @@ scikit-learn # For BNN model pybnn==0.0.5 -# For Latin design -PyDOE>=0.3.0 # For notebook tests jupyter==1.0.0 pandas>=1.0.5 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 015ae14f..ae8f8de3 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -7,5 +7,3 @@ matplotlib>=3.9 # GPy>=1.13.0 emcee>=2.2.1 scipy -PyDOE>=0.3.0 -sobol_seq>=0.1.2 diff --git a/tests/emukit/core/test_model_free_designs.py b/tests/emukit/core/test_model_free_designs.py index c3f5ab29..0abd71bf 100644 --- a/tests/emukit/core/test_model_free_designs.py +++ b/tests/emukit/core/test_model_free_designs.py @@ -1,9 +1,10 @@ -# Copyright 2020-2024 The Emukit Authors. All Rights Reserved. +# Copyright 2020-2026 The Emukit Authors. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 # Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 +import numpy as np from emukit.core import CategoricalParameter, ContinuousParameter, DiscreteParameter, ParameterSpace from emukit.core.initial_designs import RandomDesign @@ -25,8 +26,22 @@ def test_design_returns_correct_number_of_points(): points = design.get_samples(points_count) assert points_count == len(points) - columns_count = 1 - assert all([len(p) == columns_count for p in points]) + assert all([len(p) == 1 for p in points]) + + +def test_design_returns_points_within_bounds(): + p1 = ContinuousParameter("p1", 0.01, 0.05) + p2 = ContinuousParameter("p2", -100.0, -90.0) + space = ParameterSpace([p1, p2]) + points_count = 5 + + designs = create_model_free_designs(space) + for design in designs: + points = design.get_samples(points_count) + + for i, p in enumerate(space.parameters): + assert np.all(p.min <= points[:, i]) + assert np.all(points[:, i] <= p.max) def test_design_with_mixed_domain(encoding):