pysips 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pysips/__init__.py +53 -0
- pysips/crossover_proposal.py +138 -0
- pysips/laplace_nmll.py +104 -0
- pysips/metropolis.py +126 -0
- pysips/mutation_proposal.py +220 -0
- pysips/prior.py +106 -0
- pysips/random_choice_proposal.py +177 -0
- pysips/regressor.py +451 -0
- pysips/sampler.py +159 -0
- pysips-0.0.0.dist-info/METADATA +156 -0
- pysips-0.0.0.dist-info/RECORD +26 -0
- pysips-0.0.0.dist-info/WHEEL +5 -0
- pysips-0.0.0.dist-info/licenses/LICENSE +94 -0
- pysips-0.0.0.dist-info/top_level.txt +2 -0
- tests/integration/test_log_likelihood.py +18 -0
- tests/integration/test_prior_with_bingo.py +45 -0
- tests/regression/test_basic_end_to_end.py +131 -0
- tests/regression/test_regressor_end_to_end.py +95 -0
- tests/unit/test_crossover_proposal.py +156 -0
- tests/unit/test_laplace_nmll.py +111 -0
- tests/unit/test_metropolis.py +111 -0
- tests/unit/test_mutation_proposal.py +196 -0
- tests/unit/test_prior.py +135 -0
- tests/unit/test_random_choice_proposal.py +136 -0
- tests/unit/test_regressor.py +227 -0
- tests/unit/test_sampler.py +133 -0
@@ -0,0 +1,156 @@
|
|
1
|
+
import pytest
|
2
|
+
|
3
|
+
from pysips.crossover_proposal import CrossoverProposal
|
4
|
+
|
5
|
+
IMPORTMODULE = CrossoverProposal.__module__
|
6
|
+
|
7
|
+
|
8
|
+
class TestCrossoverProposal:
|
9
|
+
|
10
|
+
@pytest.fixture
|
11
|
+
def mock_model(self, mocker):
|
12
|
+
"""Create a mock model for testing"""
|
13
|
+
mock_model = mocker.MagicMock()
|
14
|
+
mock_model.__eq__.side_effect = lambda x: x is mock_model
|
15
|
+
return mock_model
|
16
|
+
|
17
|
+
@pytest.fixture
|
18
|
+
def mock_gene_pool(self, mocker, mock_model):
|
19
|
+
"""Create a mock gene pool with several models"""
|
20
|
+
model1 = mocker.MagicMock()
|
21
|
+
model2 = mocker.MagicMock()
|
22
|
+
|
23
|
+
# Configure models to be different from each other
|
24
|
+
model1.__eq__.side_effect = lambda x: x is model1
|
25
|
+
model2.__eq__.side_effect = lambda x: x is model2
|
26
|
+
|
27
|
+
return [mock_model, model1, model2]
|
28
|
+
|
29
|
+
def test_initialization(self, mock_gene_pool, mocker):
|
30
|
+
"""Test proper initialization of CrossoverProposal"""
|
31
|
+
# Mock dependencies
|
32
|
+
mock_agraph_crossover = mocker.MagicMock()
|
33
|
+
mocker.patch(
|
34
|
+
f"{IMPORTMODULE}.AGraphCrossover", return_value=mock_agraph_crossover
|
35
|
+
)
|
36
|
+
mock_random = mocker.patch("numpy.random.default_rng")
|
37
|
+
|
38
|
+
# Initialize CrossoverProposal
|
39
|
+
seed = 42
|
40
|
+
crossover_proposal = CrossoverProposal(mock_gene_pool, seed=seed)
|
41
|
+
|
42
|
+
# Assertions
|
43
|
+
assert crossover_proposal._gene_pool == mock_gene_pool
|
44
|
+
mock_random.assert_called_once_with(seed)
|
45
|
+
|
46
|
+
def test_call_method_selects_different_parent(
|
47
|
+
self, mock_gene_pool, mock_model, mocker
|
48
|
+
):
|
49
|
+
"""Test that __call__ selects a parent different from the input model"""
|
50
|
+
# Mock AGraphCrossover
|
51
|
+
mock_crossover = mocker.MagicMock()
|
52
|
+
child_1 = mocker.MagicMock(name="child_1")
|
53
|
+
child_2 = mocker.MagicMock(name="child_2")
|
54
|
+
mock_crossover.return_value = (child_1, child_2)
|
55
|
+
mocker.patch(f"{IMPORTMODULE}.AGraphCrossover", return_value=mock_crossover)
|
56
|
+
|
57
|
+
# Mock random number generator
|
58
|
+
mock_rng = mocker.MagicMock()
|
59
|
+
mock_rng.integers.side_effect = [0, 1]
|
60
|
+
mock_rng.random.return_value = 0.4 # Will select first child
|
61
|
+
|
62
|
+
# Initialize CrossoverProposal with mocked RNG
|
63
|
+
crossover_proposal = CrossoverProposal(mock_gene_pool)
|
64
|
+
crossover_proposal._rng = mock_rng
|
65
|
+
|
66
|
+
# Call the method
|
67
|
+
result = crossover_proposal(mock_model)
|
68
|
+
|
69
|
+
# Assertions
|
70
|
+
# Check that crossover was called with model and a different parent
|
71
|
+
mock_crossover.assert_called_once()
|
72
|
+
args = mock_crossover.call_args[0]
|
73
|
+
assert args[0] == mock_model # First arg is the input model
|
74
|
+
assert args[1] == mock_gene_pool[1] # Second arg should be a different model
|
75
|
+
assert args[1] != mock_model # Ensure it's not the same model
|
76
|
+
|
77
|
+
# Check we got the expected child
|
78
|
+
assert result == child_1
|
79
|
+
|
80
|
+
@pytest.mark.parametrize(
|
81
|
+
"random_value, expected_child_index",
|
82
|
+
[
|
83
|
+
(0.3, 0),
|
84
|
+
(0.8, 1),
|
85
|
+
],
|
86
|
+
)
|
87
|
+
def test_call_method_selects_child_based_on_random(
|
88
|
+
self, mock_gene_pool, mock_model, mocker, random_value, expected_child_index
|
89
|
+
):
|
90
|
+
"""Test that __call__ selects the appropriate child based on random value"""
|
91
|
+
mock_crossover = mocker.MagicMock()
|
92
|
+
child_1 = mocker.MagicMock(name="child_1")
|
93
|
+
child_2 = mocker.MagicMock(name="child_2")
|
94
|
+
children = (child_1, child_2)
|
95
|
+
mock_crossover.return_value = children
|
96
|
+
mocker.patch(f"{IMPORTMODULE}.AGraphCrossover", return_value=mock_crossover)
|
97
|
+
|
98
|
+
# Mock random number generator
|
99
|
+
mock_rng = mocker.MagicMock()
|
100
|
+
mock_rng.integers.return_value = 1
|
101
|
+
mock_rng.random.return_value = random_value
|
102
|
+
|
103
|
+
# Initialize and call
|
104
|
+
crossover_proposal = CrossoverProposal(mock_gene_pool)
|
105
|
+
crossover_proposal._rng = mock_rng
|
106
|
+
result = crossover_proposal(mock_model)
|
107
|
+
|
108
|
+
# Assertions
|
109
|
+
expected_child = children[expected_child_index]
|
110
|
+
assert result == expected_child
|
111
|
+
|
112
|
+
def test_update_method(self, mock_gene_pool, mocker):
|
113
|
+
"""Test update method updates the gene pool"""
|
114
|
+
# Mock AGraphCrossover
|
115
|
+
mocker.patch(f"{IMPORTMODULE}.AGraphCrossover")
|
116
|
+
|
117
|
+
# Initialize
|
118
|
+
crossover_proposal = CrossoverProposal(mock_gene_pool)
|
119
|
+
|
120
|
+
# Create new gene pool
|
121
|
+
new_model1 = mocker.MagicMock(name="new_model1")
|
122
|
+
new_model2 = mocker.MagicMock(name="new_model2")
|
123
|
+
new_gene_pool = (new_model1, new_model2)
|
124
|
+
|
125
|
+
# Update gene pool
|
126
|
+
crossover_proposal.update(new_gene_pool)
|
127
|
+
|
128
|
+
# Assertions
|
129
|
+
assert crossover_proposal._gene_pool == list(new_gene_pool)
|
130
|
+
|
131
|
+
# @pytest.mark.parametrize("seed", [None, 0, 42, 12345])
|
132
|
+
# def test_random_seed(self, mock_gene_pool, seed, mocker):
|
133
|
+
# """Test that random seed is properly used"""
|
134
|
+
# # Mock AGraphCrossover
|
135
|
+
# mocker.patch(f"{IMPORTMODULE}.AGraphCrossover")
|
136
|
+
|
137
|
+
# # Mock numpy random generator
|
138
|
+
# mock_rng = mocker.patch("numpy.random.default_rng")
|
139
|
+
|
140
|
+
# # Initialize with different seeds
|
141
|
+
# CrossoverProposal(mock_gene_pool, seed=seed)
|
142
|
+
|
143
|
+
# # Check the RNG was initialized with the right seed
|
144
|
+
# mock_rng.assert_called_once_with(seed)
|
145
|
+
|
146
|
+
def test_empty_gene_pool_error(self, mocker):
|
147
|
+
"""Test that an empty gene pool results in appropriate failure"""
|
148
|
+
# Mock AGraphCrossover
|
149
|
+
mocker.patch(f"{IMPORTMODULE}.AGraphCrossover")
|
150
|
+
|
151
|
+
# Initialize with empty pool
|
152
|
+
crossover_proposal = CrossoverProposal([])
|
153
|
+
|
154
|
+
# Call should raise exception due to empty pool
|
155
|
+
with pytest.raises(ValueError) as excinfo:
|
156
|
+
crossover_proposal(mocker.MagicMock())
|
@@ -0,0 +1,111 @@
|
|
1
|
+
import pytest
|
2
|
+
import numpy as np
|
3
|
+
|
4
|
+
from pysips.laplace_nmll import LaplaceNmll
|
5
|
+
|
6
|
+
IMPORTMODULE = LaplaceNmll.__module__
|
7
|
+
|
8
|
+
|
9
|
+
class TestLaplaceNmll:
|
10
|
+
|
11
|
+
@pytest.fixture
|
12
|
+
def sample_data(self):
|
13
|
+
"""Fixture to provide sample data for tests."""
|
14
|
+
X = np.array([[1, 2], [3, 4], [5, 6]])
|
15
|
+
y = np.array([10, 20, 30])
|
16
|
+
return X, y
|
17
|
+
|
18
|
+
@pytest.fixture
|
19
|
+
def mock_model(self, mocker):
|
20
|
+
"""Fixture to provide a mock bingo AGraph model."""
|
21
|
+
model = mocker.MagicMock()
|
22
|
+
model.get_local_optimization_params.return_value = np.array([1.0, 2.0])
|
23
|
+
return model
|
24
|
+
|
25
|
+
def test_negative_is_applied_to_regression_output(
|
26
|
+
self, sample_data, mock_model, mocker
|
27
|
+
):
|
28
|
+
X, y = sample_data
|
29
|
+
|
30
|
+
mock_regression = mocker.MagicMock(return_value=-5.0)
|
31
|
+
mocker.patch(f"{IMPORTMODULE}.ExplicitRegression", return_value=mock_regression)
|
32
|
+
|
33
|
+
mock_scipy_optimizer = mocker.MagicMock()
|
34
|
+
mocker.patch(
|
35
|
+
f"{IMPORTMODULE}.ScipyOptimizer",
|
36
|
+
return_value=mock_scipy_optimizer,
|
37
|
+
)
|
38
|
+
|
39
|
+
laplace_nmll = LaplaceNmll(X, y)
|
40
|
+
result = laplace_nmll(mock_model)
|
41
|
+
|
42
|
+
assert result == 5.0
|
43
|
+
|
44
|
+
@pytest.mark.parametrize("opt_restarts", [1, 3, 5, 10])
|
45
|
+
def test_number_of_restarts(self, sample_data, mock_model, mocker, opt_restarts):
|
46
|
+
"""Test that the optimizer is run the correct number of times based on opt_restarts."""
|
47
|
+
X, y = sample_data
|
48
|
+
|
49
|
+
mock_regression = mocker.MagicMock()
|
50
|
+
mock_regression.return_value = -1.0 # Constant return value
|
51
|
+
mocker.patch(f"{IMPORTMODULE}.ExplicitRegression", return_value=mock_regression)
|
52
|
+
|
53
|
+
mock_scipy_optimizer = mocker.MagicMock()
|
54
|
+
mocker.patch(
|
55
|
+
f"{IMPORTMODULE}.ScipyOptimizer",
|
56
|
+
return_value=mock_scipy_optimizer,
|
57
|
+
)
|
58
|
+
|
59
|
+
laplace_nmll = LaplaceNmll(X, y, opt_restarts=opt_restarts)
|
60
|
+
laplace_nmll(mock_model)
|
61
|
+
assert mock_scipy_optimizer.call_count == opt_restarts
|
62
|
+
|
63
|
+
def test_constants_kept_from_best_trial(self, sample_data, mock_model, mocker):
|
64
|
+
"""Test that constants are kept from optimizer trial with highest nmll."""
|
65
|
+
X, y = sample_data
|
66
|
+
|
67
|
+
# Return increasingly better values (-3 > -5 > -10 when negated)
|
68
|
+
mock_regression = mocker.MagicMock()
|
69
|
+
mock_regression.side_effect = [10.0, 5.0, 3.0]
|
70
|
+
mocker.patch(f"{IMPORTMODULE}.ExplicitRegression", return_value=mock_regression)
|
71
|
+
|
72
|
+
# Create different parameter sets for different optimization runs
|
73
|
+
mock_scipy_optimizer = mocker.MagicMock()
|
74
|
+
params_run1 = np.array([1.0, 1.0])
|
75
|
+
params_run2 = np.array([2.0, 2.0])
|
76
|
+
params_run3 = np.array([3.0, 3.0]) # This should be kept as the best
|
77
|
+
mock_model.get_local_optimization_params.side_effect = [
|
78
|
+
params_run1,
|
79
|
+
params_run2,
|
80
|
+
params_run3,
|
81
|
+
]
|
82
|
+
mocker.patch(
|
83
|
+
f"{IMPORTMODULE}.ScipyOptimizer",
|
84
|
+
return_value=mock_scipy_optimizer,
|
85
|
+
)
|
86
|
+
|
87
|
+
laplace_nmll = LaplaceNmll(X, y, opt_restarts=3)
|
88
|
+
result = laplace_nmll(mock_model)
|
89
|
+
|
90
|
+
assert result == -3.0
|
91
|
+
mock_model.set_local_optimization_params.assert_called_once_with(params_run3)
|
92
|
+
|
93
|
+
def test_optimizer_kwargs_passed_through(self, sample_data, mocker):
|
94
|
+
"""Test that optimizer kwargs are passed to the bingo deterministic optimizer."""
|
95
|
+
X, y = sample_data
|
96
|
+
|
97
|
+
scipy_optimizer_spy = mocker.patch(f"{IMPORTMODULE}.ScipyOptimizer")
|
98
|
+
mocker.patch(f"{IMPORTMODULE}.ExplicitRegression")
|
99
|
+
|
100
|
+
custom_kwargs = {
|
101
|
+
"param_init_bounds": [-10, 10],
|
102
|
+
"tol": 1e-8,
|
103
|
+
"options": {"maxiter": 500},
|
104
|
+
}
|
105
|
+
LaplaceNmll(X, y, **custom_kwargs)
|
106
|
+
expected_kwargs = {"method": "lm", **custom_kwargs}
|
107
|
+
_, actual_kwargs = scipy_optimizer_spy.call_args
|
108
|
+
|
109
|
+
for key, value in expected_kwargs.items():
|
110
|
+
assert key in actual_kwargs
|
111
|
+
assert actual_kwargs[key] == value
|
@@ -0,0 +1,111 @@
|
|
1
|
+
import inspect
|
2
|
+
import numpy as np
|
3
|
+
import pytest
|
4
|
+
from pysips.sampler import Metropolis # Adjust import as needed
|
5
|
+
|
6
|
+
IMPORTMODULE = Metropolis.__module__
|
7
|
+
|
8
|
+
|
9
|
+
def dummy_likelihood(x):
|
10
|
+
return x.value * 2
|
11
|
+
|
12
|
+
|
13
|
+
@pytest.fixture
|
14
|
+
def metropolis(mocker):
|
15
|
+
mock_likelihood = dummy_likelihood
|
16
|
+
mock_proposal = mocker.Mock()
|
17
|
+
return Metropolis(
|
18
|
+
likelihood=mock_likelihood,
|
19
|
+
proposal=mock_proposal,
|
20
|
+
prior=None,
|
21
|
+
multiprocess=False,
|
22
|
+
)
|
23
|
+
|
24
|
+
|
25
|
+
class DummyGraph:
|
26
|
+
def __init__(self, value):
|
27
|
+
self.value = value
|
28
|
+
self.fitness = None
|
29
|
+
|
30
|
+
|
31
|
+
class TestMetropolis:
|
32
|
+
def test_evaluate_model_returns_none(self, metropolis):
|
33
|
+
assert metropolis.evaluate_model() is None
|
34
|
+
|
35
|
+
def test_evaluate_log_priors_returns_ones(self, metropolis):
|
36
|
+
x = np.empty((5, 1))
|
37
|
+
np.testing.assert_array_equal(
|
38
|
+
metropolis.evaluate_log_priors(x), np.ones((5, 1))
|
39
|
+
)
|
40
|
+
|
41
|
+
def test_evaluate_log_likelihood_no_multiproc(self, metropolis):
|
42
|
+
graphs = np.array(
|
43
|
+
[[DummyGraph(1)], [DummyGraph(2)], [DummyGraph(3)]], dtype=object
|
44
|
+
)
|
45
|
+
result = metropolis.evaluate_log_likelihood(graphs)
|
46
|
+
np.testing.assert_array_equal(result, np.c_[[2, 4, 6]])
|
47
|
+
|
48
|
+
def test_evaluate_log_likelihood_multiproc(self, mocker, metropolis):
|
49
|
+
metropolis._is_multiprocess = True
|
50
|
+
|
51
|
+
dummy_pool = mocker.MagicMock()
|
52
|
+
dummy_pool.__enter__.return_value = dummy_pool
|
53
|
+
dummy_pool.__exit__.return_value = None
|
54
|
+
dummy_pool.map.side_effect = lambda func, iterable: list(map(func, iterable))
|
55
|
+
|
56
|
+
mocker.patch(f"{IMPORTMODULE}.Pool", return_value=dummy_pool)
|
57
|
+
|
58
|
+
graphs = np.array(
|
59
|
+
[[DummyGraph(1)], [DummyGraph(2)], [DummyGraph(3)]], dtype=object
|
60
|
+
)
|
61
|
+
result = metropolis.evaluate_log_likelihood(graphs)
|
62
|
+
|
63
|
+
expected = np.c_[[2, 4, 6]]
|
64
|
+
np.testing.assert_array_equal(result, expected)
|
65
|
+
|
66
|
+
for g, l in zip(graphs.flatten(), expected.flatten()):
|
67
|
+
assert g.fitness == l
|
68
|
+
|
69
|
+
|
70
|
+
def test_smc_metropolis(mocker, metropolis):
|
71
|
+
inputs = np.array([[DummyGraph(1)], [DummyGraph(2)]], dtype=object)
|
72
|
+
num_samples = 2
|
73
|
+
|
74
|
+
mocker.patch.object(
|
75
|
+
metropolis,
|
76
|
+
"_initialize_probabilities",
|
77
|
+
return_value=(np.array([0, 0]), np.array([10, 10])),
|
78
|
+
)
|
79
|
+
mocker.patch.object(
|
80
|
+
metropolis,
|
81
|
+
"_perform_mcmc_step",
|
82
|
+
side_effect=lambda inputs, a, b, c: (inputs, b, None, None),
|
83
|
+
)
|
84
|
+
|
85
|
+
update_spy = mocker.spy(metropolis._equ_proposal, "update")
|
86
|
+
|
87
|
+
out_inputs, out_log_like = metropolis.smc_metropolis(inputs, num_samples)
|
88
|
+
|
89
|
+
update_spy.assert_called_once()
|
90
|
+
called_args, called_kwargs = update_spy.call_args
|
91
|
+
|
92
|
+
assert "gene_pool" in called_kwargs
|
93
|
+
np.testing.assert_array_equal(called_kwargs["gene_pool"], inputs.flatten())
|
94
|
+
assert np.array_equal(out_inputs, inputs)
|
95
|
+
assert np.array_equal(out_log_like, np.array([10, 10]))
|
96
|
+
|
97
|
+
|
98
|
+
def test_smc_metropolis_accepts_cov_kwarg(metropolis):
|
99
|
+
sig = inspect.signature(metropolis.smc_metropolis)
|
100
|
+
assert "cov" in sig.parameters
|
101
|
+
|
102
|
+
|
103
|
+
def test_proposal_applies_equ_proposal_elementwise():
|
104
|
+
equ_proposal = lambda x: x * 2
|
105
|
+
m = Metropolis(prior=None, likelihood=lambda x: 0, proposal=equ_proposal)
|
106
|
+
|
107
|
+
x = np.c_[[1.0, 2.0]]
|
108
|
+
result = m.proposal(x, None)
|
109
|
+
|
110
|
+
expected = np.c_[[2.0, 4.0]]
|
111
|
+
np.testing.assert_array_equal(result, expected)
|
@@ -0,0 +1,196 @@
|
|
1
|
+
import pytest
|
2
|
+
|
3
|
+
from pysips.mutation_proposal import MutationProposal
|
4
|
+
|
5
|
+
|
6
|
+
IMPORTMODULE = MutationProposal.__module__
|
7
|
+
|
8
|
+
|
9
|
+
class TestMutationProposal:
|
10
|
+
|
11
|
+
@pytest.fixture
|
12
|
+
def basic_setup(self):
|
13
|
+
"""Basic setup for mutation proposal tests"""
|
14
|
+
X_dim = 2
|
15
|
+
operators = ["+", "-", "*"]
|
16
|
+
return X_dim, operators
|
17
|
+
|
18
|
+
@pytest.fixture
|
19
|
+
def mock_model(self, mocker):
|
20
|
+
"""Create a mock model for testing"""
|
21
|
+
return mocker.MagicMock()
|
22
|
+
|
23
|
+
def test_initialization_passes_info_to_bingo(self, mocker):
|
24
|
+
"""Test proper initialization of MutationProposal"""
|
25
|
+
X_dim = 3
|
26
|
+
operators = ["+", "-", "*", "sin"]
|
27
|
+
|
28
|
+
mock_component_gen = mocker.MagicMock()
|
29
|
+
mock_component_gen_cls = mocker.patch(
|
30
|
+
f"{IMPORTMODULE}.ComponentGenerator", return_value=mock_component_gen
|
31
|
+
)
|
32
|
+
mock_agraph_mutation = mocker.MagicMock()
|
33
|
+
mock_agraph_mutation_cls = mocker.patch(
|
34
|
+
f"{IMPORTMODULE}.AGraphMutation", return_value=mock_agraph_mutation
|
35
|
+
)
|
36
|
+
|
37
|
+
mutation_proposal = MutationProposal(
|
38
|
+
x_dim=X_dim,
|
39
|
+
operators=operators,
|
40
|
+
terminal_probability=0.2,
|
41
|
+
constant_probability=0.5,
|
42
|
+
command_probability=0.1,
|
43
|
+
node_probability=0.3,
|
44
|
+
parameter_probability=0.15,
|
45
|
+
prune_probability=0.25,
|
46
|
+
fork_probability=0.2,
|
47
|
+
repeat_mutation_probability=0.1,
|
48
|
+
seed=42,
|
49
|
+
)
|
50
|
+
|
51
|
+
mock_component_gen_cls.assert_called_once_with(
|
52
|
+
input_x_dimension=X_dim, terminal_probability=0.2, constant_probability=0.5
|
53
|
+
)
|
54
|
+
|
55
|
+
assert mock_component_gen.add_operator.call_count == len(operators)
|
56
|
+
for op in operators:
|
57
|
+
mock_component_gen.add_operator.assert_any_call(op)
|
58
|
+
|
59
|
+
mock_agraph_mutation_cls.assert_called_once_with(
|
60
|
+
mock_component_gen,
|
61
|
+
0.1, # command_probability
|
62
|
+
0.3, # node_probability
|
63
|
+
0.15, # parameter_probability
|
64
|
+
0.25, # prune_probability
|
65
|
+
0.2, # fork_probability
|
66
|
+
)
|
67
|
+
|
68
|
+
def test_do_mutation_single(self, basic_setup, mock_model, mocker):
|
69
|
+
"""Test _do_mutation method without repeat"""
|
70
|
+
X_dim, operators = basic_setup
|
71
|
+
|
72
|
+
# Setup mocks
|
73
|
+
mock_mutation = mocker.MagicMock()
|
74
|
+
mutated_model = mocker.MagicMock()
|
75
|
+
mock_mutation.return_value = mutated_model
|
76
|
+
|
77
|
+
# Patch random generator to ensure no repeat
|
78
|
+
mock_rng = mocker.MagicMock()
|
79
|
+
mock_rng.random.return_value = 1.0 # > repeat_mutation_prob
|
80
|
+
|
81
|
+
mocker.patch(f"{IMPORTMODULE}.ComponentGenerator")
|
82
|
+
mocker.patch(f"{IMPORTMODULE}.AGraphMutation", return_value=mock_mutation)
|
83
|
+
|
84
|
+
mutation_proposal = MutationProposal(x_dim=X_dim, operators=operators)
|
85
|
+
mutation_proposal._rng = mock_rng
|
86
|
+
|
87
|
+
result = mutation_proposal._do_mutation(mock_model)
|
88
|
+
mock_mutation.assert_called_once_with(mock_model)
|
89
|
+
assert result == mutated_model
|
90
|
+
assert mock_rng.random.call_count == 1
|
91
|
+
|
92
|
+
def test_do_mutation_with_repeat(self, basic_setup, mock_model, mocker):
|
93
|
+
"""Test _do_mutation method with repeated mutations"""
|
94
|
+
X_dim, operators = basic_setup
|
95
|
+
|
96
|
+
mock_mutation = mocker.MagicMock()
|
97
|
+
mutated_model1 = mocker.MagicMock(name="mutated_model1")
|
98
|
+
mutated_model2 = mocker.MagicMock(name="mutated_model2")
|
99
|
+
mutated_model3 = mocker.MagicMock(name="mutated_model3")
|
100
|
+
mock_mutation.side_effect = [mutated_model1, mutated_model2, mutated_model3]
|
101
|
+
|
102
|
+
# Patch random generator to ensure repeat twice
|
103
|
+
mock_rng = mocker.MagicMock()
|
104
|
+
mock_rng.random.side_effect = [
|
105
|
+
0.1,
|
106
|
+
0.2,
|
107
|
+
0.9,
|
108
|
+
] # First two < repeat_mutation_prob
|
109
|
+
|
110
|
+
mocker.patch(f"{IMPORTMODULE}.ComponentGenerator")
|
111
|
+
mocker.patch(f"{IMPORTMODULE}.AGraphMutation", return_value=mock_mutation)
|
112
|
+
|
113
|
+
mutation_proposal = MutationProposal(
|
114
|
+
x_dim=X_dim, operators=operators, repeat_mutation_probability=0.5
|
115
|
+
)
|
116
|
+
mutation_proposal._rng = mock_rng
|
117
|
+
|
118
|
+
result = mutation_proposal(mock_model)
|
119
|
+
|
120
|
+
assert mock_mutation.call_count == 3
|
121
|
+
mock_mutation.assert_any_call(mock_model)
|
122
|
+
mock_mutation.assert_any_call(mutated_model1)
|
123
|
+
mock_mutation.assert_any_call(mutated_model2)
|
124
|
+
assert result == mutated_model3
|
125
|
+
assert mock_rng.random.call_count == 3
|
126
|
+
|
127
|
+
def test_call_different_model(self, basic_setup, mock_model, mocker):
|
128
|
+
"""Test __call__ when mutation immediately produces a different model"""
|
129
|
+
X_dim, operators = basic_setup
|
130
|
+
|
131
|
+
mocker.patch(f"{IMPORTMODULE}.ComponentGenerator")
|
132
|
+
|
133
|
+
mutated_model = mocker.MagicMock()
|
134
|
+
mock_mutation = mocker.MagicMock(return_value=mutated_model)
|
135
|
+
mocker.patch(f"{IMPORTMODULE}.AGraphMutation", return_value=mock_mutation)
|
136
|
+
|
137
|
+
# Ensure models are different
|
138
|
+
mock_model.__eq__.return_value = False
|
139
|
+
|
140
|
+
mutation_proposal = MutationProposal(
|
141
|
+
x_dim=X_dim, operators=operators, repeat_mutation_probability=0.0
|
142
|
+
)
|
143
|
+
result = mutation_proposal(mock_model)
|
144
|
+
|
145
|
+
# Assertions
|
146
|
+
mock_mutation.assert_called_once_with(mock_model)
|
147
|
+
assert result == mutated_model
|
148
|
+
|
149
|
+
def test_call_retry_mutation(self, basic_setup, mock_model, mocker):
|
150
|
+
"""Test __call__ when mutation initially produces an equivalent model"""
|
151
|
+
X_dim, operators = basic_setup
|
152
|
+
|
153
|
+
mocker.patch(f"{IMPORTMODULE}.ComponentGenerator")
|
154
|
+
|
155
|
+
equivalent_model = mock_model
|
156
|
+
different_model = mocker.MagicMock()
|
157
|
+
mock_mutation = mocker.MagicMock(
|
158
|
+
side_effect=[equivalent_model, different_model]
|
159
|
+
)
|
160
|
+
mocker.patch(f"{IMPORTMODULE}.AGraphMutation", return_value=mock_mutation)
|
161
|
+
|
162
|
+
mock_model.__eq__.side_effect = lambda other: other is equivalent_model
|
163
|
+
|
164
|
+
mutation_proposal = MutationProposal(
|
165
|
+
x_dim=X_dim, operators=operators, repeat_mutation_probability=0.0
|
166
|
+
)
|
167
|
+
result = mutation_proposal(mock_model)
|
168
|
+
|
169
|
+
assert mock_mutation.call_count == 2
|
170
|
+
assert result == different_model
|
171
|
+
|
172
|
+
def test_update_method(self, basic_setup, mocker):
|
173
|
+
"""Test update method (should be a no-op)"""
|
174
|
+
X_dim, operators = basic_setup
|
175
|
+
|
176
|
+
mocker.patch(f"{IMPORTMODULE}.ComponentGenerator")
|
177
|
+
mocker.patch(f"{IMPORTMODULE}.AGraphMutation")
|
178
|
+
|
179
|
+
mutation_proposal = MutationProposal(x_dim=X_dim, operators=operators)
|
180
|
+
|
181
|
+
# This should not raise any errors
|
182
|
+
mutation_proposal.update(some_param=10)
|
183
|
+
mutation_proposal.update()
|
184
|
+
|
185
|
+
@pytest.mark.parametrize("seed", [None, 0, 42, 12345])
|
186
|
+
def test_random_seed(self, mocker, basic_setup, seed):
|
187
|
+
"""Test that random seed is properly used"""
|
188
|
+
X_dim, operators = basic_setup
|
189
|
+
|
190
|
+
# Create class with different seeds
|
191
|
+
mocker.patch(f"{IMPORTMODULE}.ComponentGenerator"),
|
192
|
+
mocker.patch(f"{IMPORTMODULE}.AGraphMutation"),
|
193
|
+
mock_rng = mocker.patch("numpy.random.default_rng")
|
194
|
+
|
195
|
+
_ = MutationProposal(x_dim=X_dim, operators=operators, seed=seed)
|
196
|
+
mock_rng.assert_called_once_with(seed)
|