Skip to content

Commit 932be70

Browse files
Make scikit-learn optional
1 parent 61d04aa commit 932be70

File tree

11 files changed

+108
-78
lines changed

11 files changed

+108
-78
lines changed

.github/workflows/python-package.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
- name: Install dependencies
3838
run: |
3939
python -m pip install --upgrade pip
40-
pip install -e .
40+
pip install -e .[nomad,sklearn]
4141
- name: Lint with ruff
4242
run: |
4343
python -m pip install flake8 jupyter
@@ -47,7 +47,7 @@ jobs:
4747
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
4848
- name: Install test dependencies
4949
run: |
50-
python -m pip install pytest pytest-timeout PyNomadBBO pygoblet
50+
python -m pip install pytest pytest-timeout pygoblet
5151
python -m pip list
5252
- name: Test with pytest (fast tests)
5353
run: |

examples/vlse_benchmark/vlse_bench.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,6 @@ def run_optimizer(
200200
)
201201
),
202202
}
203-
algorithms["GP"] = {
204-
"model": gp.GaussianProcess(normalize_y=True, random_state=42),
205-
"optimizer": optimize.bayesian_optimization,
206-
"acquisition": acquisition.MaximizeEI(seed=42),
207-
}
208203

209204
# Maximum number of evaluations per function. 100*n, where n is the input
210205
# dimension

pdm.lock

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@ dependencies = [
2222
"numpy",
2323
"scipy",
2424
"pymoo",
25-
"scikit-learn",
2625
"autograd",
2726
"networkx",
2827
]
2928

29+
[project.optional-dependencies]
30+
nomad = ["PyNomadBBO"]
31+
sklearn = ["scikit-learn"]
32+
3033
[project.urls]
3134
Homepage = "https://github.com/NREL/soogo"
3235
Docs = "https://nrel.github.io/soogo"

soogo/model/gp.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,18 @@
2222
import scipy.optimize as scipy_opt
2323

2424
# Scikit-learn imports
25-
from sklearn.exceptions import ConvergenceWarning
26-
from sklearn.gaussian_process import GaussianProcessRegressor
27-
from sklearn.gaussian_process.kernels import (
28-
RBF as GPkernelRBF,
29-
ConstantKernel as GPConstantKernel,
30-
)
25+
try:
26+
from sklearn.exceptions import ConvergenceWarning
27+
from sklearn.gaussian_process import GaussianProcessRegressor
28+
from sklearn.gaussian_process.kernels import (
29+
RBF as GPkernelRBF,
30+
ConstantKernel as GPConstantKernel,
31+
)
32+
except ImportError:
33+
GaussianProcessRegressor = None
34+
GPkernelRBF = None
35+
GPConstantKernel = None
36+
ConvergenceWarning = None
3137

3238
# Local imports
3339
from .base import Surrogate
@@ -64,6 +70,11 @@ class GaussianProcess(Surrogate):
6470
"""
6571

6672
def __init__(self, scaler=None, **kwargs) -> None:
73+
if GaussianProcessRegressor is None:
74+
raise ImportError(
75+
"scikit-learn is required to use the GaussianProcess surrogate model."
76+
)
77+
6778
super().__init__()
6879

6980
# Scaler for x
@@ -167,7 +178,7 @@ def min_design_space_size(self, dim: int) -> int:
167178
"""Return the minimum design space size for a given space dimension."""
168179
return 1 if dim > 0 else 0
169180

170-
def check_initial_design(self, _sample: np.ndarray) -> int:
181+
def check_initial_design(self, sample: np.ndarray) -> int:
171182
return 0
172183

173184
def update(self, Xnew, ynew) -> None:

tests/model/test_gp.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@
2525
@pytest.mark.parametrize("n", (10, 100))
2626
@pytest.mark.parametrize("copy_X_train", (True, False))
2727
def test_X(n: int, copy_X_train: bool):
28-
gp = GaussianProcess(copy_X_train=copy_X_train)
28+
try:
29+
gp = GaussianProcess(copy_X_train=copy_X_train)
30+
except ImportError:
31+
pytest.skip("GaussianProcess could not be imported, likely because scikit-learn is not installed.")
2932

3033
X0 = np.random.rand(n, 3)
3134
y = np.random.rand(n)

tests/model/test_surrogate.py

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@ def test_rbf_model_is_surrogate():
3838

3939
def test_gaussian_process_is_surrogate():
4040
"""Test that GaussianProcess is a subclass of Surrogate."""
41-
gp = GaussianProcess()
42-
assert isinstance(gp, Surrogate)
41+
try:
42+
gp = GaussianProcess()
43+
assert isinstance(gp, Surrogate)
44+
except ImportError:
45+
pytest.skip("GaussianProcess could not be imported, likely because scikit-learn is not installed.")
4346

4447

4548
def test_surrogate_interface_methods():
@@ -67,21 +70,24 @@ def test_surrogate_interface_methods():
6770
assert distances.shape == (1, 3)
6871

6972
# Test Gaussian Process
70-
gp = GaussianProcess()
71-
72-
# Test all required methods exist and work
73-
gp.update(x_train, y_train)
74-
assert gp.ntrain == 3
75-
assert gp.X.shape == (3, 2)
76-
assert gp.Y.shape == (3,)
77-
assert gp.min_design_space_size(2) >= 0
78-
assert gp.check_initial_design(x_train) == 0
79-
assert isinstance(gp.iindex, tuple)
80-
81-
# Test prediction
82-
y_pred, y_std = gp(x_test, return_std=True)
83-
assert y_pred.shape == (1,)
84-
assert y_std.shape == (1,)
73+
try:
74+
gp = GaussianProcess()
75+
76+
# Test all required methods exist and work
77+
gp.update(x_train, y_train)
78+
assert gp.ntrain == 3
79+
assert gp.X.shape == (3, 2)
80+
assert gp.Y.shape == (3,)
81+
assert gp.min_design_space_size(2) >= 0
82+
assert gp.check_initial_design(x_train) == 0
83+
assert isinstance(gp.iindex, tuple)
84+
85+
# Test prediction
86+
y_pred, y_std = gp(x_test, return_std=True)
87+
assert y_pred.shape == (1,)
88+
assert y_std.shape == (1,)
89+
except ImportError:
90+
pass # If scikit-learn is not installed, this will fail, but that's expected
8591

8692

8793
if __name__ == "__main__":

tests/test_docs.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -296,15 +296,16 @@ def test_module_imports_in_docs(self):
296296
# Basic smoke test - instantiate some classes
297297
rbf_model = RbfModel()
298298
assert rbf_model is not None
299-
300-
gp_model = GaussianProcess()
301-
assert gp_model is not None
302-
303-
# Test that classes and functions have expected attributes
304299
assert hasattr(WeightedAcquisition, "optimize")
305300
assert hasattr(OptimizeResult, "__init__")
306301
assert callable(surrogate_optimization)
307-
assert callable(bayesian_optimization)
302+
303+
try:
304+
gp_model = GaussianProcess()
305+
assert gp_model is not None
306+
assert callable(bayesian_optimization)
307+
except ImportError:
308+
pass # If scikit-learn is not installed, this will fail, but that's expected
308309

309310
except ImportError as e:
310311
pytest.fail(

tests/test_examples.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,6 @@ def test_examples_import_soogo(self):
279279
from soogo import gosac, socemo, surrogate_optimization # noqa: F401
280280
from soogo import RbfModel # noqa: F401
281281
from soogo import acquisition, sampling # noqa: F401
282-
# Note: GaussianProcess import might fail in some environments
283282
except ImportError as e:
284283
pytest.fail(f"Failed to import required modules: {e}")
285284

tests/test_optimize.py

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,15 @@ def callback(intermediate_result: OptimizeResult):
6767
assert (
6868
intermediate_result.sample.shape[1] == intermediate_result.x.size
6969
)
70-
71-
minimize(
72-
lambda x: np.sum(x**2, axis=1),
73-
((-10, 3), (-1, 1)),
74-
maxeval=10,
75-
callback=callback,
76-
)
70+
try:
71+
minimize(
72+
lambda x: np.sum(x**2, axis=1),
73+
((-10, 3), (-1, 1)),
74+
maxeval=10,
75+
callback=callback,
76+
)
77+
except ImportError:
78+
pytest.skip(f"{minimize.__name__} requires additional dependencies that are not installed.")
7779

7880

7981
@pytest.mark.parametrize(
@@ -104,16 +106,18 @@ def ackley(x, n: int = 2):
104106
)
105107

106108
bounds = [[-32.768, 20], [-32.768, 32.768]]
109+
try:
110+
res0 = minimize(lambda x: [ackley(x[0], 2)], bounds, maxeval=10, seed=123)
111+
res1 = minimize(lambda x: [ackley(x[0], 2)], bounds, maxeval=10, seed=123)
107112

108-
res0 = minimize(lambda x: [ackley(x[0], 2)], bounds, maxeval=10, seed=123)
109-
res1 = minimize(lambda x: [ackley(x[0], 2)], bounds, maxeval=10, seed=123)
110-
111-
assert np.all(res0.x == res1.x)
112-
assert np.all(res0.fx == res1.fx)
113-
assert res0.nit == res1.nit
114-
assert res0.nfev == res1.nfev
115-
assert np.all(res0.sample == res1.sample)
116-
assert np.all(res0.fsample == res1.fsample)
113+
assert np.all(res0.x == res1.x)
114+
assert np.all(res0.fx == res1.fx)
115+
assert res0.nit == res1.nit
116+
assert res0.nfev == res1.nfev
117+
assert np.all(res0.sample == res1.sample)
118+
assert np.all(res0.fsample == res1.fsample)
119+
except ImportError:
120+
pytest.skip(f"{minimize.__name__} requires additional dependencies that are not installed.")
117121

118122

119123
def test_batched_sampling():
@@ -133,11 +137,14 @@ def ackley(x, n: int = 2):
133137

134138
bounds = [[-32.768, 20], [-32.768, 32.768]]
135139

136-
out = bayesian_optimization(
137-
lambda x: [ackley(xi - 3.14) for xi in x],
138-
bounds=bounds,
139-
maxeval=100,
140-
batchSize=10,
141-
acquisitionFunc=MaximizeEI(pool_size=200, avoid_clusters=True),
142-
)
143-
assert out.nfev == 100
140+
try:
141+
out = bayesian_optimization(
142+
lambda x: [ackley(xi - 3.14) for xi in x],
143+
bounds=bounds,
144+
maxeval=100,
145+
batchSize=10,
146+
acquisitionFunc=MaximizeEI(pool_size=200, avoid_clusters=True),
147+
)
148+
assert out.nfev == 100
149+
except ImportError:
150+
pytest.skip("scikit-learn is not installed, skipping Bayesian optimization tests.")

0 commit comments

Comments
 (0)