Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## Unreleased

### Fixed

- Knapsack and ZDT5 now initialize solutions with their sampled random bits
instead of leaving all bits false. Knapsack also supports an empty bit vector.

* [Current]
* Added the multi-objective TSP problem
* Added the Average Hausdorff Distance (AHD) quality indicator.
Expand Down
15 changes: 9 additions & 6 deletions src/jmetal/problem/multiobjective/zdt.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,13 @@ def eval_v(self, value: int) -> float:
return 1.0

def create_solution(self, rng: np.random.Generator | None = None) -> BinarySolution:
"""
Create a new random solution.
"""Create a solution containing the sampled random bits.

Args:
rng: Random generator. Uses Python's global random generator when omitted.

Returns:
A binary solution whose bits are true for scalar draws below 0.5.
"""
solution = BinarySolution(
number_of_variables=self.total_number_of_bits,
Expand All @@ -234,11 +239,9 @@ def create_solution(self, rng: np.random.Generator | None = None) -> BinarySolut

# Initialize with random bits
if rng is not None:
for i in range(self.total_number_of_bits):
solution.variables[i] = rng.random() < 0.5
solution.variables = [rng.random() < 0.5 for _ in range(self.total_number_of_bits)]
else:
for i in range(self.total_number_of_bits):
solution.variables[i] = random.random() < 0.5
solution.variables = [random.random() < 0.5 for _ in range(self.total_number_of_bits)]

return solution

Expand Down
15 changes: 11 additions & 4 deletions src/jmetal/problem/singleobjective/knapsack.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,24 @@ def evaluate(self, solution: BinarySolution) -> BinarySolution:
return solution

def create_solution(self, rng: np.random.Generator | None = None) -> BinarySolution:
"""Create a solution containing the sampled random bits.

Args:
rng: Random generator. Uses NumPy's global generator when omitted.

Returns:
A binary solution with one sampled bit per item, or no bits for zero items.
"""
new_solution = BinarySolution(
number_of_variables=self.number_of_bits,
number_of_objectives=self.number_of_objectives(),
)

# The BinarySolution initializes with empty variables, we need to set the bits
# The bits will be stored in variables[0] as a numpy array
# Initialize the flat bit vector through its storage setter.
if rng is not None:
new_solution.variables[0] = rng.choice([True, False], size=self.number_of_bits)
new_solution.bits = rng.choice([True, False], size=self.number_of_bits)
else:
new_solution.variables[0] = np.random.choice([True, False], size=self.number_of_bits)
new_solution.bits = np.random.choice([True, False], size=self.number_of_bits)
return new_solution

def name(self):
Expand Down
25 changes: 25 additions & 0 deletions tests/problem/multiobjective/test_zdt.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import unittest
from unittest import mock

import numpy as np
import pytest

from jmetal.problem.multiobjective.zdt import ZDT1, ZDT2, ZDT3, ZDT4, ZDT5, ZDT6

Expand Down Expand Up @@ -247,5 +251,26 @@ def test_should_get_name_return_the_right_name(self):
self.assertEqual("ZDT5", problem.name())


class TestZDT5Initialization:
"""Regression tests for storing scalar samples as binary values."""

@pytest.mark.parametrize("use_generator", [True, False])
def test_should_store_bits_using_the_existing_threshold(self, use_generator: bool) -> None:
"""A draw equal to 0.5 remains false for both random sources."""
draws: list[float] = [0.0, 0.49, 0.5, 0.99] * 20
expected: list[bool] = [True, True, False, False] * 20
rng = mock.Mock(spec=np.random.Generator)
rng.random.side_effect = draws
with mock.patch(
"jmetal.problem.multiobjective.zdt.random.random", side_effect=draws
) as global_random:
solution = ZDT5().create_solution(rng if use_generator else None)

assert solution.variables == expected
selected = rng.random if use_generator else global_random
assert selected.call_args_list == [mock.call()] * len(draws)
(global_random if use_generator else rng.random).assert_not_called()


if __name__ == "__main__":
unittest.main()
32 changes: 32 additions & 0 deletions tests/problem/singleobjective/test_knapsack.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import unittest
from unittest import mock

import numpy as np
import pytest

from jmetal.problem.singleobjective.knapsack import Knapsack


Expand Down Expand Up @@ -58,3 +61,32 @@ def test_should_create_solution_from_file(self) -> None:
def test_should_get_name_return_the_right_name(self):
problem = Knapsack()
self.assertEqual("Knapsack", problem.name())


class TestKnapsackInitialization:
"""Regression tests for storing sampled bits during initialization."""

@pytest.mark.parametrize("use_generator", [True, False])
def test_should_store_sampled_bits(self, use_generator: bool) -> None:
"""Both random sources must populate the returned solution's storage."""
expected: np.ndarray = np.array([True, False, False, True, True])
rng = mock.Mock(spec=np.random.Generator)
rng.choice.return_value = expected
with mock.patch(
"jmetal.problem.singleobjective.knapsack.np.random.choice", return_value=expected
) as global_choice:
solution = Knapsack(5).create_solution(rng if use_generator else None)

np.testing.assert_array_equal(solution.bits, expected)
selected = rng.choice if use_generator else global_choice
selected.assert_called_once_with([True, False], size=5)
(global_choice if use_generator else rng.choice).assert_not_called()

@pytest.mark.parametrize("use_generator", [True, False])
def test_should_initialize_zero_items(self, use_generator: bool) -> None:
"""Zero items produce an empty vector for either random source."""
rng = np.random.default_rng(42) if use_generator else None

solution = Knapsack(0).create_solution(rng)

assert solution.bits.size == 0
Loading