Skip to content

Commit 9d9e001

Browse files
committed
Fix EvolutionStrategy: constraint handling sort order and get_name
- Fix constraint handling bug in replacement(): negate overall_constraint_violation_degree so feasible solutions (degree 0) are ranked before infeasible ones (degree < 0). - Fix get_name() to return the correct variant name based on the elitist flag: '(mu + lambda)' or '(mu, lambda)'. - Add 17 unit and integration tests covering construction, selection, reproduction, replacement, constraint handling, and full algorithm runs on the Sphere problem.
1 parent a70f5fc commit 9d9e001

2 files changed

Lines changed: 381 additions & 2 deletions

File tree

src/jmetal/algorithm/singleobjective/evolution_strategy.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def replacement(self, population: List[S], offspring_population: List[S]) -> Lis
7676
else:
7777
population_pool.extend(offspring_population)
7878

79-
population_pool.sort(key=lambda s: (overall_constraint_violation_degree(s), s.objectives[0]))
79+
population_pool.sort(key=lambda s: (- overall_constraint_violation_degree(s), s.objectives[0]))
8080

8181
new_population = []
8282
for i in range(self.mu):
@@ -88,4 +88,7 @@ def result(self) -> R:
8888
return self.solutions[0]
8989

9090
def get_name(self) -> str:
91-
return "Elitist evolution Strategy"
91+
if self.elitist:
92+
return "(mu + lambda) Evolution Strategy"
93+
else:
94+
return "(mu, lambda) Evolution Strategy"
Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
"""Tests for the EvolutionStrategy algorithm.
2+
3+
Covers unit tests for each public method and a lightweight integration test
4+
that runs the algorithm on the Sphere problem.
5+
"""
6+
7+
from copy import copy
8+
from unittest.mock import MagicMock
9+
10+
import pytest
11+
12+
from jmetal.algorithm.singleobjective.evolution_strategy import EvolutionStrategy
13+
from jmetal.core.solution import FloatSolution
14+
from jmetal.operator.mutation import PolynomialMutation
15+
from jmetal.problem import Sphere
16+
from jmetal.util.termination_criterion import StoppingByEvaluations
17+
18+
19+
# ---------------------------------------------------------------------------
20+
# Fixtures
21+
# ---------------------------------------------------------------------------
22+
23+
@pytest.fixture
24+
def sphere_problem() -> Sphere:
25+
"""A 3-variable Sphere problem used across multiple tests."""
26+
return Sphere(number_of_variables=3)
27+
28+
29+
@pytest.fixture
30+
def mutation(sphere_problem: Sphere) -> PolynomialMutation:
31+
"""Default polynomial mutation operator for the Sphere problem."""
32+
return PolynomialMutation(
33+
probability=1.0 / sphere_problem.number_of_variables(),
34+
distribution_index=20,
35+
)
36+
37+
38+
@pytest.fixture
39+
def termination() -> StoppingByEvaluations:
40+
"""A short termination criterion for unit tests."""
41+
return StoppingByEvaluations(max_evaluations=100)
42+
43+
44+
def _build_algorithm(
45+
problem: Sphere,
46+
mutation: PolynomialMutation,
47+
termination: StoppingByEvaluations,
48+
*,
49+
mu: int = 5,
50+
lambda_: int = 5,
51+
elitist: bool = True,
52+
) -> EvolutionStrategy:
53+
"""Helper to create an EvolutionStrategy with common defaults."""
54+
return EvolutionStrategy(
55+
problem=problem,
56+
mu=mu,
57+
lambda_=lambda_,
58+
elitist=elitist,
59+
mutation=mutation,
60+
termination_criterion=termination,
61+
)
62+
63+
64+
def _make_solution(
65+
objective: float,
66+
constraint_violation: float | None = None,
67+
) -> FloatSolution:
68+
"""Create a minimal FloatSolution with a given objective value and optional constraint."""
69+
n_constraints = 1 if constraint_violation is not None else 0
70+
solution = FloatSolution(
71+
lower_bound=[0.0],
72+
upper_bound=[1.0],
73+
number_of_objectives=1,
74+
number_of_constraints=n_constraints,
75+
)
76+
solution.objectives[0] = objective
77+
solution.variables = [0.5]
78+
if constraint_violation is not None:
79+
solution.constraints[0] = constraint_violation
80+
return solution
81+
82+
83+
# ---------------------------------------------------------------------------
84+
# Unit tests – construction
85+
# ---------------------------------------------------------------------------
86+
87+
class TestEvolutionStrategyConstruction:
88+
"""Tests for correct initialisation of the algorithm."""
89+
90+
def test_given_valid_params_when_created_then_stores_mu_and_lambda(
91+
self, sphere_problem, mutation, termination
92+
) -> None:
93+
algorithm = _build_algorithm(
94+
sphere_problem, mutation, termination, mu=10, lambda_=20
95+
)
96+
97+
assert algorithm.mu == 10
98+
assert algorithm.lambda_ == 20
99+
100+
def test_given_elitist_true_when_created_then_flag_is_true(
101+
self, sphere_problem, mutation, termination
102+
) -> None:
103+
algorithm = _build_algorithm(
104+
sphere_problem, mutation, termination, elitist=True
105+
)
106+
107+
assert algorithm.elitist is True
108+
109+
def test_given_elitist_false_when_created_then_flag_is_false(
110+
self, sphere_problem, mutation, termination
111+
) -> None:
112+
algorithm = _build_algorithm(
113+
sphere_problem, mutation, termination, elitist=False
114+
)
115+
116+
assert algorithm.elitist is False
117+
118+
119+
# ---------------------------------------------------------------------------
120+
# Unit tests – get_name
121+
# ---------------------------------------------------------------------------
122+
123+
class TestGetName:
124+
"""Tests for the get_name method returning the correct variant label."""
125+
126+
def test_given_elitist_when_get_name_then_returns_mu_plus_lambda(
127+
self, sphere_problem, mutation, termination
128+
) -> None:
129+
algorithm = _build_algorithm(
130+
sphere_problem, mutation, termination, elitist=True
131+
)
132+
133+
assert algorithm.get_name() == "(mu + lambda) Evolution Strategy"
134+
135+
def test_given_non_elitist_when_get_name_then_returns_mu_comma_lambda(
136+
self, sphere_problem, mutation, termination
137+
) -> None:
138+
algorithm = _build_algorithm(
139+
sphere_problem, mutation, termination, elitist=False
140+
)
141+
142+
assert algorithm.get_name() == "(mu, lambda) Evolution Strategy"
143+
144+
145+
# ---------------------------------------------------------------------------
146+
# Unit tests – selection
147+
# ---------------------------------------------------------------------------
148+
149+
class TestSelection:
150+
"""Tests for the selection operator (identity in ES)."""
151+
152+
def test_given_population_when_selection_then_returns_same_population(
153+
self, sphere_problem, mutation, termination
154+
) -> None:
155+
algorithm = _build_algorithm(sphere_problem, mutation, termination)
156+
population = [_make_solution(i) for i in range(5)]
157+
158+
result = algorithm.selection(population)
159+
160+
assert result is population
161+
162+
163+
# ---------------------------------------------------------------------------
164+
# Unit tests – reproduction
165+
# ---------------------------------------------------------------------------
166+
167+
class TestReproduction:
168+
"""Tests for offspring generation via mutation."""
169+
170+
def test_given_population_when_reproduction_then_offspring_size_equals_lambda(
171+
self, sphere_problem, mutation, termination
172+
) -> None:
173+
mu, lambda_ = 5, 10
174+
algorithm = _build_algorithm(
175+
sphere_problem, mutation, termination, mu=mu, lambda_=lambda_
176+
)
177+
population = [sphere_problem.create_solution() for _ in range(mu)]
178+
for s in population:
179+
sphere_problem.evaluate(s)
180+
181+
offspring = algorithm.reproduction(population)
182+
183+
assert len(offspring) == lambda_
184+
185+
def test_given_mu_equals_lambda_when_reproduction_then_one_child_per_parent(
186+
self, sphere_problem, mutation, termination
187+
) -> None:
188+
mu = lambda_ = 4
189+
algorithm = _build_algorithm(
190+
sphere_problem, mutation, termination, mu=mu, lambda_=lambda_
191+
)
192+
population = [sphere_problem.create_solution() for _ in range(mu)]
193+
for s in population:
194+
sphere_problem.evaluate(s)
195+
196+
offspring = algorithm.reproduction(population)
197+
198+
assert len(offspring) == lambda_
199+
200+
201+
# ---------------------------------------------------------------------------
202+
# Unit tests – replacement
203+
# ---------------------------------------------------------------------------
204+
205+
class TestReplacement:
206+
"""Tests for the replacement step, including elitist vs non-elitist behaviour."""
207+
208+
def test_given_elitist_when_replacement_then_pool_contains_parents_and_offspring(
209+
self, sphere_problem, mutation, termination
210+
) -> None:
211+
algorithm = _build_algorithm(
212+
sphere_problem, mutation, termination, mu=2, lambda_=2, elitist=True
213+
)
214+
parents = [_make_solution(10.0), _make_solution(20.0)]
215+
offspring = [_make_solution(5.0), _make_solution(15.0)]
216+
217+
result = algorithm.replacement(parents, offspring)
218+
219+
# Best 2 from merged pool (5.0, 10.0, 15.0, 20.0) → [5.0, 10.0]
220+
assert len(result) == 2
221+
assert result[0].objectives[0] == 5.0
222+
assert result[1].objectives[0] == 10.0
223+
224+
def test_given_non_elitist_when_replacement_then_pool_contains_only_offspring(
225+
self, sphere_problem, mutation, termination
226+
) -> None:
227+
algorithm = _build_algorithm(
228+
sphere_problem, mutation, termination, mu=2, lambda_=3, elitist=False
229+
)
230+
parents = [_make_solution(1.0), _make_solution(2.0)]
231+
offspring = [_make_solution(10.0), _make_solution(5.0), _make_solution(8.0)]
232+
233+
result = algorithm.replacement(parents, offspring)
234+
235+
# Only offspring compete: best 2 from (5.0, 8.0, 10.0) → [5.0, 8.0]
236+
assert len(result) == 2
237+
assert result[0].objectives[0] == 5.0
238+
assert result[1].objectives[0] == 8.0
239+
240+
def test_given_replacement_when_called_then_returns_mu_solutions(
241+
self, sphere_problem, mutation, termination
242+
) -> None:
243+
mu = 3
244+
algorithm = _build_algorithm(
245+
sphere_problem, mutation, termination, mu=mu, lambda_=5, elitist=True
246+
)
247+
parents = [_make_solution(float(i)) for i in range(mu)]
248+
offspring = [_make_solution(float(i + 10)) for i in range(5)]
249+
250+
result = algorithm.replacement(parents, offspring)
251+
252+
assert len(result) == mu
253+
254+
255+
# ---------------------------------------------------------------------------
256+
# Unit tests – constraint handling in replacement
257+
# ---------------------------------------------------------------------------
258+
259+
class TestConstraintHandling:
260+
"""Tests verifying that feasible solutions are preferred over infeasible ones."""
261+
262+
def test_given_feasible_and_infeasible_when_replacement_then_feasible_first(
263+
self, sphere_problem, mutation, termination
264+
) -> None:
265+
"""A feasible solution with a worse objective must be preferred over
266+
an infeasible solution with a better objective."""
267+
algorithm = _build_algorithm(
268+
sphere_problem, mutation, termination, mu=1, lambda_=1, elitist=True
269+
)
270+
feasible = _make_solution(objective=100.0, constraint_violation=0.0)
271+
infeasible = _make_solution(objective=1.0, constraint_violation=-5.0)
272+
273+
result = algorithm.replacement([feasible], [infeasible])
274+
275+
assert len(result) == 1
276+
assert result[0].objectives[0] == 100.0 # feasible wins
277+
278+
def test_given_two_infeasible_when_replacement_then_less_violated_first(
279+
self, sphere_problem, mutation, termination
280+
) -> None:
281+
"""Between two infeasible solutions, the one with smaller violation
282+
(closer to 0) should be ranked first."""
283+
algorithm = _build_algorithm(
284+
sphere_problem, mutation, termination, mu=2, lambda_=2, elitist=True
285+
)
286+
slightly_violated = _make_solution(objective=50.0, constraint_violation=-1.0)
287+
heavily_violated = _make_solution(objective=10.0, constraint_violation=-10.0)
288+
289+
result = algorithm.replacement(
290+
[slightly_violated], [heavily_violated]
291+
)
292+
293+
# Slightly violated (violation degree -1.0) should rank before heavily violated (-10.0)
294+
assert result[0].constraints[0] == -1.0
295+
296+
def test_given_two_feasible_when_replacement_then_best_objective_first(
297+
self, sphere_problem, mutation, termination
298+
) -> None:
299+
"""When both solutions are feasible the objective value decides the ranking."""
300+
algorithm = _build_algorithm(
301+
sphere_problem, mutation, termination, mu=2, lambda_=2, elitist=True
302+
)
303+
better = _make_solution(objective=3.0, constraint_violation=0.0)
304+
worse = _make_solution(objective=7.0, constraint_violation=0.0)
305+
306+
result = algorithm.replacement([worse], [better])
307+
308+
assert result[0].objectives[0] == 3.0
309+
assert result[1].objectives[0] == 7.0
310+
311+
def test_given_no_constraints_when_replacement_then_sorted_by_objective(
312+
self, sphere_problem, mutation, termination
313+
) -> None:
314+
"""For unconstrained problems the ordering should be purely by objective."""
315+
algorithm = _build_algorithm(
316+
sphere_problem, mutation, termination, mu=3, lambda_=3, elitist=True
317+
)
318+
solutions = [_make_solution(5.0), _make_solution(1.0), _make_solution(3.0)]
319+
320+
result = algorithm.replacement([], solutions)
321+
322+
objectives = [s.objectives[0] for s in result]
323+
assert objectives == [1.0, 3.0, 5.0]
324+
325+
326+
# ---------------------------------------------------------------------------
327+
# Integration test – run on Sphere
328+
# ---------------------------------------------------------------------------
329+
330+
class TestEvolutionStrategyIntegration:
331+
"""Lightweight integration tests that run the full algorithm loop."""
332+
333+
def test_given_sphere_when_elitist_es_runs_then_finds_near_optimal_solution(
334+
self,
335+
) -> None:
336+
problem = Sphere(number_of_variables=3)
337+
algorithm = EvolutionStrategy(
338+
problem=problem,
339+
mu=10,
340+
lambda_=10,
341+
elitist=True,
342+
mutation=PolynomialMutation(
343+
probability=1.0 / problem.number_of_variables(),
344+
distribution_index=20,
345+
),
346+
termination_criterion=StoppingByEvaluations(max_evaluations=5000),
347+
)
348+
349+
algorithm.run()
350+
result = algorithm.result()
351+
352+
# Sphere optimum is 0.0; after 5 000 evaluations we expect a value < 1.0
353+
assert result.objectives[0] < 1.0
354+
355+
def test_given_sphere_when_non_elitist_es_runs_then_completes_without_error(
356+
self,
357+
) -> None:
358+
problem = Sphere(number_of_variables=3)
359+
algorithm = EvolutionStrategy(
360+
problem=problem,
361+
mu=10,
362+
lambda_=10,
363+
elitist=False,
364+
mutation=PolynomialMutation(
365+
probability=1.0 / problem.number_of_variables(),
366+
distribution_index=20,
367+
),
368+
termination_criterion=StoppingByEvaluations(max_evaluations=1000),
369+
)
370+
371+
algorithm.run()
372+
result = algorithm.result()
373+
374+
# Simply assert that the algorithm completed and returned a valid solution
375+
assert result is not None
376+
assert len(result.objectives) == 1

0 commit comments

Comments
 (0)