Skip to content

Commit 4d36811

Browse files
committed
add integration tests for pure python
1 parent 0a6c2c6 commit 4d36811

4 files changed

Lines changed: 454 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,13 +240,65 @@ jobs:
240240
run: |
241241
python -m pytest tests/test_dependencies/test_no_scipy.py -v --tb=short -p no:warnings
242242
243+
# ===========================================================================
244+
# STAGE 3c: Dependency Isolation Tests - No numpy (pure Python)
245+
# ===========================================================================
246+
test-no-numpy:
247+
name: Tests (no numpy, pure Python)
248+
runs-on: ubuntu-latest
249+
needs: api-smoke-tests
250+
steps:
251+
- name: Checkout code
252+
uses: actions/checkout@v4
253+
254+
- name: Set up Python 3.11
255+
uses: actions/setup-python@v5
256+
with:
257+
python-version: "3.11"
258+
259+
- name: Install minimal dependencies (no numpy, no scipy)
260+
run: |
261+
python -m pip install --upgrade pip
262+
python -m pip install build pandas pytest tqdm
263+
264+
- name: Build and install package
265+
run: |
266+
python -m pip install build
267+
python -m build
268+
pip install dist/*.whl
269+
270+
- name: Verify numpy is NOT installed
271+
run: |
272+
python -c "
273+
import sys
274+
try:
275+
import numpy
276+
print('ERROR: numpy is installed but should not be!')
277+
sys.exit(1)
278+
except ImportError:
279+
print('OK: numpy is not installed')
280+
"
281+
282+
- name: Verify array backend uses pure Python
283+
run: |
284+
python -c "
285+
from gradient_free_optimizers._array_backend import _backend_name, HAS_NUMPY
286+
assert not HAS_NUMPY, 'HAS_NUMPY should be False'
287+
assert _backend_name == 'pure', f'Expected pure backend, got {_backend_name}'
288+
print('OK: using pure Python array backend')
289+
"
290+
291+
- name: Run dependency isolation tests
292+
run: |
293+
python -m pytest tests/test_dependencies/test_no_numpy.py -v --tb=short -p no:warnings
294+
243295
# ===========================================================================
244296
# STAGE 4: Coverage Report (only on Ubuntu with full dependencies)
245297
# ===========================================================================
246298
coverage:
247299
name: Coverage Report
248300
runs-on: ubuntu-latest
249-
needs: [test-matrix, test-no-scipy] # Also depends on: test-no-sklearn (currently disabled)
301+
needs: [test-matrix, test-no-scipy, test-no-numpy]
250302
steps:
251303
- name: Checkout code
252304
uses: actions/checkout@v4
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Author: Simon Blanke
2+
# Email: simon.blanke@yahoo.com
3+
# License: MIT License
4+
5+
"""
6+
Dependency Isolation Tests - No numpy.
7+
8+
These tests verify that Gradient-Free-Optimizers works correctly
9+
when numpy is NOT installed. All array operations fall back to the
10+
pure Python GFOArray backend.
11+
12+
Test Configuration:
13+
- initialize: {"random": 3} (minimal warm-up)
14+
- n_iter: 10 (quick iteration count)
15+
- Small discrete search space using backend linspace
16+
"""
17+
18+
import pytest
19+
20+
try:
21+
import numpy
22+
23+
pytest.skip(
24+
"numpy is installed - these tests require numpy to be absent",
25+
allow_module_level=True,
26+
)
27+
except ImportError:
28+
pass
29+
30+
from gradient_free_optimizers import (
31+
BayesianOptimizer,
32+
DifferentialEvolutionOptimizer,
33+
DirectAlgorithm,
34+
DownhillSimplexOptimizer,
35+
EvolutionStrategyOptimizer,
36+
ForestOptimizer,
37+
GeneticAlgorithmOptimizer,
38+
GridSearchOptimizer,
39+
HillClimbingOptimizer,
40+
LipschitzOptimizer,
41+
ParallelTemperingOptimizer,
42+
ParticleSwarmOptimizer,
43+
PatternSearch,
44+
PowellsMethod,
45+
RandomAnnealingOptimizer,
46+
RandomRestartHillClimbingOptimizer,
47+
RandomSearchOptimizer,
48+
RepulsingHillClimbingOptimizer,
49+
SimulatedAnnealingOptimizer,
50+
SpiralOptimization,
51+
StochasticHillClimbingOptimizer,
52+
TreeStructuredParzenEstimators,
53+
)
54+
from gradient_free_optimizers._array_backend import linspace
55+
56+
SEARCH_SPACE = {
57+
"x1": linspace(-5, 5, 11),
58+
"x2": linspace(-5, 5, 11),
59+
}
60+
61+
62+
def objective_function(para):
63+
return -(para["x1"] ** 2 + para["x2"] ** 2)
64+
65+
66+
OPTIMIZERS = [
67+
HillClimbingOptimizer,
68+
StochasticHillClimbingOptimizer,
69+
RepulsingHillClimbingOptimizer,
70+
SimulatedAnnealingOptimizer,
71+
DownhillSimplexOptimizer,
72+
RandomSearchOptimizer,
73+
GridSearchOptimizer,
74+
RandomRestartHillClimbingOptimizer,
75+
PowellsMethod,
76+
PatternSearch,
77+
LipschitzOptimizer,
78+
DirectAlgorithm,
79+
RandomAnnealingOptimizer,
80+
ParallelTemperingOptimizer,
81+
ParticleSwarmOptimizer,
82+
SpiralOptimization,
83+
GeneticAlgorithmOptimizer,
84+
EvolutionStrategyOptimizer,
85+
DifferentialEvolutionOptimizer,
86+
BayesianOptimizer,
87+
TreeStructuredParzenEstimators,
88+
ForestOptimizer,
89+
]
90+
91+
92+
class TestNoNumpy:
93+
"""Test all optimizers work without numpy installed."""
94+
95+
@pytest.mark.parametrize("Optimizer", OPTIMIZERS)
96+
def test_optimizer_without_numpy(self, Optimizer):
97+
opt = Optimizer(
98+
SEARCH_SPACE,
99+
initialize={"random": 3},
100+
random_state=42,
101+
)
102+
opt.search(
103+
objective_function,
104+
n_iter=10,
105+
verbosity=False,
106+
)
107+
108+
assert opt.best_para is not None
109+
assert opt.best_score is not None
110+
assert opt.search_data is not None
111+
assert len(opt.search_data) > 0
112+
113+
114+
def test_numpy_not_installed():
115+
"""Verify numpy is not available in this test environment."""
116+
with pytest.raises(ImportError):
117+
import numpy # noqa: F401

tests/test_internal/test_array_backend/test_core.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,157 @@ def test_isinf(self):
492492
# =============================================================================
493493

494494

495+
class TestTriangularExtraction:
496+
"""Test upper triangle extraction."""
497+
498+
def test_triu_k0(self):
499+
mat = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
500+
np_result = np_backend.triu(np_backend.array(mat), k=0)
501+
pure_result = pure_backend.triu(pure_backend.array(mat), k=0)
502+
assert to_list(np_result.flatten()) == to_list(pure_result.flatten())
503+
assert to_list(pure_result.flatten()) == [1, 2, 3, 0, 5, 6, 0, 0, 9]
504+
505+
def test_triu_k1(self):
506+
mat = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
507+
np_result = np_backend.triu(np_backend.array(mat), k=1)
508+
pure_result = pure_backend.triu(pure_backend.array(mat), k=1)
509+
assert to_list(np_result.flatten()) == to_list(pure_result.flatten())
510+
assert to_list(pure_result.flatten()) == [0, 2, 3, 0, 0, 6, 0, 0, 0]
511+
512+
def test_triu_k_neg1(self):
513+
mat = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
514+
np_result = np_backend.triu(np_backend.array(mat), k=-1)
515+
pure_result = pure_backend.triu(pure_backend.array(mat), k=-1)
516+
assert to_list(np_result.flatten()) == to_list(pure_result.flatten())
517+
assert to_list(pure_result.flatten()) == [1, 2, 3, 4, 5, 6, 0, 8, 9]
518+
519+
520+
class TestInvert:
521+
"""Test boolean inversion."""
522+
523+
def test_invert_list(self):
524+
bools = [True, False, True, False, True]
525+
pure_result = pure_backend.invert(pure_backend.array(bools))
526+
assert to_list(pure_result) == [False, True, False, True, False]
527+
528+
def test_invert_gfoarray(self):
529+
arr = pure_backend.array([True, False, False, True])
530+
result = pure_backend.invert(arr)
531+
assert to_list(result) == [False, True, True, False]
532+
533+
534+
class TestDefaultRng:
535+
"""Test Generator factory via default_rng."""
536+
537+
def test_generator_has_methods(self):
538+
rng = pure_backend.random.default_rng(42)
539+
assert callable(getattr(rng, "random", None))
540+
assert callable(getattr(rng, "standard_normal", None))
541+
assert callable(getattr(rng, "uniform", None))
542+
assert callable(getattr(rng, "integers", None))
543+
544+
def test_seeded_determinism(self):
545+
rng1 = pure_backend.random.default_rng(123)
546+
vals1 = to_list(rng1.random(size=5))
547+
rng2 = pure_backend.random.default_rng(123)
548+
vals2 = to_list(rng2.random(size=5))
549+
assert vals1 == vals2
550+
551+
def test_array_output_shape(self):
552+
rng = pure_backend.random.default_rng(0)
553+
result = rng.uniform(0.0, 1.0, size=10)
554+
assert len(to_list(result)) == 10
555+
556+
def test_integers_range(self):
557+
rng = pure_backend.random.default_rng(7)
558+
result = rng.integers(0, 10, size=50)
559+
values = to_list(result)
560+
assert all(0 <= v < 10 for v in values)
561+
562+
563+
class TestLinAlgError:
564+
"""Test LinAlgError exception type."""
565+
566+
def test_is_exception_class(self):
567+
assert issubclass(pure_backend.linalg.LinAlgError, Exception)
568+
assert issubclass(np_backend.linalg.LinAlgError, Exception)
569+
570+
def test_can_be_raised_and_caught(self):
571+
with pytest.raises(pure_backend.linalg.LinAlgError):
572+
raise pure_backend.linalg.LinAlgError("singular matrix")
573+
574+
def test_can_be_caught_as_exception(self):
575+
with pytest.raises(Exception, match="test"):
576+
raise np_backend.linalg.LinAlgError("test")
577+
578+
579+
class TestLinAlgEigh:
580+
"""Test eigendecomposition."""
581+
582+
def test_numpy_backend_returns_eigenvalues_and_vectors(self):
583+
mat = np_backend.array([[2.0, 1.0], [1.0, 3.0]])
584+
eigenvalues, eigenvectors = np_backend.linalg.eigh(mat)
585+
assert len(to_list(eigenvalues)) == 2
586+
assert eigenvectors.shape == (2, 2)
587+
588+
def test_pure_backend_raises(self):
589+
mat = pure_backend.array([[2.0, 1.0], [1.0, 3.0]])
590+
with pytest.raises(NotImplementedError):
591+
pure_backend.linalg.eigh(mat)
592+
593+
594+
class TestNdarrayType:
595+
"""Test ndarray type export."""
596+
597+
def test_numpy_isinstance(self):
598+
import gradient_free_optimizers._array_backend as arr_backend
599+
600+
arr = arr_backend.array([1, 2, 3])
601+
assert isinstance(arr, arr_backend.ndarray)
602+
603+
def test_pure_isinstance(self):
604+
arr = pure_backend.array([1, 2, 3])
605+
from gradient_free_optimizers._array_backend._pure import GFOArray
606+
607+
assert isinstance(arr, GFOArray)
608+
609+
610+
class TestBooleanSetitem:
611+
"""Test boolean indexing with __setitem__."""
612+
613+
def test_setitem_scalar(self):
614+
arr = pure_backend.array([1.0, 2.0, 3.0, 4.0])
615+
mask = [True, False, True, False]
616+
arr[mask] = 0.0
617+
assert to_list(arr) == [0.0, 2.0, 0.0, 4.0]
618+
619+
def test_setitem_array_values(self):
620+
arr = pure_backend.array([1.0, 2.0, 3.0, 4.0])
621+
mask = [False, True, False, True]
622+
arr[mask] = pure_backend.array([20.0, 40.0])
623+
assert to_list(arr) == [1.0, 20.0, 3.0, 40.0]
624+
625+
626+
class TestReshapeNegativeOne:
627+
"""Test reshape with -1 dimension inference."""
628+
629+
def test_reshape_neg1_first(self):
630+
arr = pure_backend.array([1, 2, 3, 4, 5, 6])
631+
result = arr.reshape((-1, 2))
632+
assert result.shape == (3, 2)
633+
634+
def test_reshape_neg1_second(self):
635+
arr = pure_backend.array([1, 2, 3, 4, 5, 6])
636+
result = arr.reshape((2, -1))
637+
assert result.shape == (2, 3)
638+
639+
def test_reshape_neg1_matches_numpy(self):
640+
values = list(range(12))
641+
np_result = np_backend.array(values).reshape((-1, 3))
642+
pure_result = pure_backend.array(values).reshape((-1, 3))
643+
assert np_result.shape == pure_result.shape == (4, 3)
644+
645+
495646
class TestBackendSelection:
496647
"""Test that backend selection works correctly."""
497648

0 commit comments

Comments
 (0)