diff --git a/GALLERY.md b/GALLERY.md index a712c1d6..77a91742 100644 --- a/GALLERY.md +++ b/GALLERY.md @@ -24,6 +24,7 @@ This gallery shows examples from all available datasets using their default conf - [gsm_symbolic](#gsm_symbolic) - [intermediate_integration](#intermediate_integration) - [isomorphic_strings](#isomorphic_strings) +- [rotate_matrix](#rotate_matrix) - [largest_island](#largest_island) - [lcm](#lcm) - [leg_counting](#leg_counting) @@ -1167,6 +1168,166 @@ Metadata: {'words': ['hogtytyof', 'kgqwfwfgh'], 'solution': True, 'solvable': Tr ```` + +### rotate_matrix +Rotate a matrix by 90 degrees clockwise + +Default configuration: +```python +max_n: int = 10 # Maximum size of the matrix +``` + +```` +Sample 1: +Question: Given a square matrix, your job is to rotate it by 90 degrees clockwise. + +Example: + +Input: +1 2 3 +4 5 6 +7 8 9 + +Output: +7 4 1 +8 5 2 +9 6 3 + +Rotate the matrix below by 90 degrees clockwise: +3 1 +2 0 + +Answer: +2 3 +0 1 + +Metadata: {'matrix': [[3, 1], [2, 0]], 'solution': [[2, 3], [0, 1]]} + +-------------------------------------------------- + +Sample 2: +Question: Given a square matrix, your job is to rotate it by 90 degrees clockwise. + +Example: + +Input: +1 2 3 +4 5 6 +7 8 9 + +Output: +7 4 1 +8 5 2 +9 6 3 + +Rotate the matrix below by 90 degrees clockwise: +0 + +Answer: +0 + +Metadata: {'matrix': [[0]], 'solution': [[0]]} + +-------------------------------------------------- + +Sample 3: +Question: Given a square matrix, your job is to rotate it by 90 degrees clockwise. + +Example: + +Input: +1 2 3 +4 5 6 +7 8 9 + +Output: +7 4 1 +8 5 2 +9 6 3 + +Rotate the matrix below by 90 degrees clockwise: +28 17 38 29 8 15 26 +35 13 37 39 27 40 20 +4 30 23 16 3 5 48 +9 25 2 46 47 21 22 +31 12 41 43 19 32 10 +6 0 36 45 42 1 18 +14 24 11 7 44 34 33 + +Answer: +14 6 31 9 4 35 28 +24 0 12 25 30 13 17 +11 36 41 2 23 37 38 +7 45 43 0 16 39 29 +44 42 19 47 3 27 8 +34 1 32 21 5 40 15 +33 18 10 22 48 20 26 + +Metadata: {'matrix': [[28, 17, 38, 29, 8, 15, 26], [35, 13, 37, 39, 27, 40, 20], [4, 30, 23, 16, 3, 5, 48], [9, 25, 2, 46, 47, 21, 22], [31, 12, 41, 43, 19, 32, 10], [6, 0, 36, 45, 42, 1, 18], [14, 24, 11, 7, 44, 34, 33]], 'solution': [[14, 6, 31, 9, 4, 35, 28], [24, 0, 12, 25, 30, 13, 17], [11, 36, 41, 2, 23, 37, 38], [7, 45, 43, 0, 16, 39, 29], [44, 42, 19, 47, 3, 27, 8], [34, 1, 32, 21, 5, 40, 15], [33, 18, 10, 22, 48, 20, 26]]} + +-------------------------------------------------- + +Sample 4: +Question: Given a square matrix, your job is to rotate it by 90 degrees clockwise. + +Example: + +Input: +1 2 3 +4 5 6 +7 8 9 + +Output: +7 4 1 +8 5 2 +9 6 3 + +Rotate the matrix below by 90 degrees clockwise: +20 3 6 19 22 +5 7 17 11 16 +24 14 12 4 1 +18 23 21 0 10 +9 2 8 15 13 + +Answer: +9 18 24 5 20 +2 23 14 7 3 +8 21 0 17 6 +15 0 4 11 19 +13 10 1 16 22 + +Metadata: {'matrix': [[20, 3, 6, 19, 22], [5, 7, 17, 11, 16], [24, 14, 12, 4, 1], [18, 23, 21, 0, 10], [9, 2, 8, 15, 13]], 'solution': [[9, 18, 24, 5, 20], [2, 23, 14, 7, 3], [8, 21, 0, 17, 6], [15, 0, 4, 11, 19], [13, 10, 1, 16, 22]]} + +-------------------------------------------------- + +Sample 5: +Question: Given a square matrix, your job is to rotate it by 90 degrees clockwise. + +Example: + +Input: +1 2 3 +4 5 6 +7 8 9 + +Output: +7 4 1 +8 5 2 +9 6 3 + +Rotate the matrix below by 90 degrees clockwise: +1 2 +0 3 + +Answer: +0 1 +3 2 + +Metadata: {'matrix': [[1, 2], [0, 3]], 'solution': [[0, 1], [3, 2]]} + +-------------------------------------------------- +```` + ### largest_island Generates Largest Island exercises with configurable difficulty diff --git a/README.md b/README.md index f06de185..03b8d4de 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ See the [Dataset Gallery](https://github.com/open-thought/reasoning-gym/blob/mai - `WordLadderDataset`: Generate word ladder puzzles where one word is transformed into another by changing one letter at a time - `GroupAnagramsDataset`: Group anagrams together in a list of words - `IsomorphicStrings`: Check if two strings are isomorphic (have the same character mapping) +- `RotateMatrix`: Rotate a matrix by 90 degrees clockwise ### Code Tasks diff --git a/reasoning_gym/algorithmic/__init__.py b/reasoning_gym/algorithmic/__init__.py index 60247372..f103d0c8 100644 --- a/reasoning_gym/algorithmic/__init__.py +++ b/reasoning_gym/algorithmic/__init__.py @@ -15,6 +15,7 @@ from .letter_jumble import LetterJumbleConfig, LetterJumbleDataset from .number_filtering import NumberFilteringConfig, NumberFilteringDataset from .number_sorting import NumberSortingConfig, NumberSortingDataset from .palindrome_generation import PalindromeConfig, PalindromeDataset +from .rotate_matrix import RotateMatrixConfig, RotateMatrixDataset from .sentence_reordering import SentenceReorderingConfig, SentenceReorderingDataset from .spell_backward import SpellBackwardConfig, SpellBackwardDataset from .word_ladder import WordLadderConfig, WordLadderDataset @@ -51,4 +52,6 @@ __all__ = [ "GroupAnagramsDataset", "IsomorphicStringsConfig", "IsomorphicStringsDataset", + "RotateMatrixConfig", + "RotateMatrixDataset", ] diff --git a/reasoning_gym/algorithmic/rotate_matrix.py b/reasoning_gym/algorithmic/rotate_matrix.py new file mode 100644 index 00000000..a2c8bf22 --- /dev/null +++ b/reasoning_gym/algorithmic/rotate_matrix.py @@ -0,0 +1,97 @@ +"""Rotate a square matrix by 90 degrees clockwise. + +A popular Leetcode problem: +https://leetcode.com/problems/rotate-image/description/ +""" + +from copy import deepcopy +from dataclasses import dataclass +from random import Random +from typing import Optional + +from ..factory import ProceduralDataset, register_dataset + +QUESTION_TEMPLATE = """Given a square matrix, your job is to rotate it by 90 degrees clockwise. + +Example: + +Input: +1 2 3 +4 5 6 +7 8 9 + +Output: +7 4 1 +8 5 2 +9 6 3 + +Rotate the matrix below by 90 degrees clockwise: +{matrix} +""" + + +@dataclass +class RotateMatrixConfig: + """Configuration for Rotate Matrix dataset generation""" + + max_n: int = 10 # Maximum size of the matrix + + size: int = 500 # Virtual dataset size + seed: Optional[int] = None + + def validate(self): + """Validate configuration parameters""" + assert 1 <= self.max_n, "max_n must be at least 1" + + +class RotateMatrixDataset(ProceduralDataset): + """Generates Rotate Matrix exercises with configurable difficulty""" + + def __init__(self, config: RotateMatrixConfig): + super().__init__(config=config, seed=config.seed, size=config.size) + + def _get_matrix(self, rng: Random) -> list[list[int]]: + """Generate a random matrix""" + n = rng.randint(1, self.config.max_n) + numbers = list(range(n**2)) + rng.shuffle(numbers) + matrix = [numbers[i * n : (i + 1) * n] for i in range(n)] + return matrix + + def _get_rotated(self, matrix: list[list[int]]) -> list[list[int]]: + """Rotate the matrix by 90 degrees clockwise""" + n = len(matrix) + output = deepcopy(matrix) + + for l in range(n // 2): + for i in range(l, n - 1 - l): + (output[l][i], output[i][n - 1 - l], output[n - 1 - l][n - 1 - i], output[n - 1 - i][l]) = ( + matrix[n - 1 - i][l], + matrix[l][i], + matrix[i][n - 1 - l], + matrix[n - 1 - l][n - 1 - i], + ) + + return output + + def _matrix_to_str(self, matrix: list[list[int]]) -> str: + """Get a string representation of the matrix""" + return "\n".join(" ".join(str(x) for x in row) for row in matrix) + + def __getitem__(self, idx: int) -> dict: + """Generate a single Spiral Matrix question""" + rng = Random(self.seed + idx) + + matrix = self._get_matrix(rng) + matrix_str = self._matrix_to_str(matrix) + answer = self._get_rotated(matrix) + answer_str = self._matrix_to_str(answer) + + return { + "question": QUESTION_TEMPLATE.format(matrix=matrix_str), + "answer": answer_str, + "metadata": {"matrix": matrix, "solution": answer}, + } + + +register_dataset("rotate_matrix", RotateMatrixDataset, RotateMatrixConfig) diff --git a/tests/test_rotate_matrix.py b/tests/test_rotate_matrix.py new file mode 100644 index 00000000..61fabe95 --- /dev/null +++ b/tests/test_rotate_matrix.py @@ -0,0 +1,100 @@ +"""Tests for Rotate Matrix questions generation""" + +import pytest + +from reasoning_gym.algorithmic.rotate_matrix import RotateMatrixConfig, RotateMatrixDataset + + +def test_rotate_matrix_config_validation(): + """Test that invalid configs raise appropriate errors""" + with pytest.raises(AssertionError): + config = RotateMatrixConfig(max_n=-1) # Negative not allowed + config.validate() + + with pytest.raises(AssertionError): + config = RotateMatrixConfig(max_n=0) # Zero not allowed + config.validate() + + +def test_rotate_matrix_dataset_deterministic(): + """Test that dataset generates same items with same seed""" + config = RotateMatrixConfig(seed=42, size=10) + dataset1 = RotateMatrixDataset(config) + dataset2 = RotateMatrixDataset(config) + + for i in range(len(dataset1)): + assert dataset1[i] == dataset2[i] + + +def test_rotate_matrix_dataset_items(): + """Test basic properties of generated items""" + config = RotateMatrixConfig(max_n=7, size=10, seed=42) + dataset = RotateMatrixDataset(config) + + for i in range(len(dataset)): + item = dataset[i] + # Check item structure + assert isinstance(item, dict) + assert "question" in item + assert "answer" in item + assert "metadata" in item + + # Check metadata + assert "matrix" in item["metadata"] + assert "solution" in item["metadata"] + + matrix = item["metadata"]["matrix"] + solution = item["metadata"]["solution"] + + # Verify matrix dimensions + assert len(matrix) <= config.max_n + assert all(len(row) <= config.max_n for row in matrix) + assert len(solution) <= config.max_n + assert all(len(row) <= config.max_n for row in solution) + assert set(e for row in matrix for e in row) == set(e for row in solution for e in row) + + +def test_rotate_matrix_dataset_iteration(): + """Test that iteration respects dataset size""" + config = RotateMatrixConfig(size=5, seed=42) + dataset = RotateMatrixDataset(config) + + items = list(dataset) + assert len(items) == config.size + + # Test multiple iterations yield same items + assert items == list(dataset) + + +def test_rotate_matrix_answer(): + """Test the _get_rotated method""" + config = RotateMatrixConfig(seed=42) + dataset = RotateMatrixDataset(config) + + # n = 1 + matrix = [[8]] + expected = [[8]] + assert dataset._get_rotated(matrix) == expected + + # n = 2 + matrix = [ + [0, 1], + [2, 3], + ] + expected = [ + [2, 0], + [3, 1], + ] + assert dataset._get_rotated(matrix) == expected + + # n = 3 + matrix = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + ] + expected = [ + [6, 3, 0], + [7, 4, 1], + [8, 5, 2], + ]