diff --git a/reasoning_gym/algorithmic/__init__.py b/reasoning_gym/algorithmic/__init__.py index 7eb204d0..931955d0 100644 --- a/reasoning_gym/algorithmic/__init__.py +++ b/reasoning_gym/algorithmic/__init__.py @@ -17,7 +17,7 @@ from .game_of_life import GameOfLifeConfig, GameOfLifeDataset from .game_of_life_halting import GameOfLifeHaltingConfig, GameOfLifeHaltingDataset from .graph_color import GraphColorConfig, GraphColorDataset from .group_anagrams import GroupAnagramsConfig, GroupAnagramsCurriculum, GroupAnagramsDataset -from .isomorphic_strings import IsomorphicStringsConfig, IsomorphicStringsDataset +from .isomorphic_strings import IsomorphicStringsConfig, IsomorphicStringsCurriculum, IsomorphicStringsDataset from .jugs import JugsConfig, JugsDataset from .letter_counting import LetterCountingConfig, LetterCountingDataset from .letter_jumble import LetterJumbleConfig, LetterJumbleDataset @@ -86,6 +86,7 @@ __all__ = [ "RansomNoteDataset", "IsomorphicStringsConfig", "IsomorphicStringsDataset", + "IsomorphicStringsCurriculum", "RotateMatrixConfig", "RotateMatrixDataset", "ManipulateMatrixConfig", diff --git a/reasoning_gym/algorithmic/isomorphic_strings.py b/reasoning_gym/algorithmic/isomorphic_strings.py index bba46343..0edd3a16 100644 --- a/reasoning_gym/algorithmic/isomorphic_strings.py +++ b/reasoning_gym/algorithmic/isomorphic_strings.py @@ -10,6 +10,7 @@ from dataclasses import dataclass from random import Random from typing import Optional +from ..coaching import AttributeType, BaseCurriculum, RangeAttributeDefinition from ..factory import ProceduralDataset, register_dataset QUESTION_TEMPLATE = """Two strings are isomorphic if the characters in one string can be replaced to get the second string. @@ -27,6 +28,7 @@ Return True if the following two strings are isomorphic, or False otherwise: class IsomorphicStringsConfig: """Configuration for Isomorphic Strings dataset generation""" + min_string_length: int = 2 # Minimum length of the strings max_string_length: int = 10 # Maximum length of the strings p_solvable: float = 0.5 # Probability that the generated question is solvable @@ -35,7 +37,9 @@ class IsomorphicStringsConfig: def validate(self): """Validate configuration parameters""" - assert 2 <= self.max_string_length, "max_string_length must be at least 2" + assert ( + 2 <= self.min_string_length <= self.max_string_length + ), "min_string_length must be between 2 and max_string_length" assert 0 <= self.p_solvable <= 1, "p_solvable must be between 0 and 1" @@ -62,13 +66,13 @@ class IsomorphicStringsDataset(ProceduralDataset): return True - def _generate_inputs(self, rng: Random, solvable: bool) -> tuple[str, str]: + def _generate_inputs(self, rng: Random, string_length: int, solvable: bool) -> tuple[str, str]: """Generate the two input strings""" s, t = [], [] mapping = {} # Generate a valid isomorphic pair first (leave one character for potential conflict) - for _ in range(rng.randint(1, self.config.max_string_length - 1)): + for _ in range(string_length - 1): char_s = rng.choice(list(self.letters)) if char_s not in mapping: # Choose a random character that is not already mapped @@ -94,15 +98,42 @@ class IsomorphicStringsDataset(ProceduralDataset): """Generate a single Isomorphic Strings question""" rng = Random(self.seed + idx) + string_length = rng.randint(self.config.min_string_length, self.config.max_string_length) solvable = rng.random() < self.config.p_solvable - s, t = self._generate_inputs(rng, solvable) + s, t = self._generate_inputs(rng, string_length, solvable) answer = self._check_isomorphic(s, t) return { "question": QUESTION_TEMPLATE.format(s=s, t=t), "answer": str(answer), - "metadata": {"words": [s, t], "solution": answer, "solvable": solvable}, + "metadata": { + "words": [s, t], + "solution": answer, + "solvable": solvable, + "difficulty": { + "string_length": string_length, + }, + }, } -register_dataset("isomorphic_strings", IsomorphicStringsDataset, IsomorphicStringsConfig) +class IsomorphicStringsCurriculum(BaseCurriculum): + def __init__(self): + super().__init__(IsomorphicStringsCurriculum.__name__, IsomorphicStringsConfig) + + # Define attributes + self._define_attributes( + RangeAttributeDefinition( + name="string_length", + levels=[10, 50, 100, 1000], + default_level=0, + description="Length of the strings", + attr_type=AttributeType.APPEND, + min_value=2, + lower_field_name="min_string_length", + upper_field_name="max_string_length", + ) + ) + + +register_dataset("isomorphic_strings", IsomorphicStringsDataset, IsomorphicStringsConfig, IsomorphicStringsCurriculum) diff --git a/tests/test_isomorphic_strings.py b/tests/test_isomorphic_strings.py index 6e515cf7..f9cb5d39 100644 --- a/tests/test_isomorphic_strings.py +++ b/tests/test_isomorphic_strings.py @@ -4,7 +4,11 @@ import json import pytest -from reasoning_gym.algorithmic.isomorphic_strings import IsomorphicStringsConfig, IsomorphicStringsDataset +from reasoning_gym.algorithmic.isomorphic_strings import ( + IsomorphicStringsConfig, + IsomorphicStringsCurriculum, + IsomorphicStringsDataset, +) def test_isomorphic_strings_config_validation(): @@ -106,3 +110,24 @@ def test_isomorphic_strings_answer(): "", ) assert dataset._check_isomorphic(s, t) == True + + +def test_isomorphic_strings_curriculum(): + curriculum = IsomorphicStringsCurriculum() + + base_value = {"size": 150, "seed": 1} + + base_cfg: IsomorphicStringsConfig = curriculum.generate_configuration(base_value) + assert base_cfg.seed == 1 + assert base_cfg.size == 150 + assert base_cfg.min_string_length == 10 and base_cfg.max_string_length == 10 + + # test incrementing attribute levels + curriculum.increment_attr_level("string_length") + increased_cfg = curriculum.generate_configuration(base_value) + assert increased_cfg.min_string_length == 10 and increased_cfg.max_string_length == 50 + + # test incrementing attribute levels again + curriculum.increment_attr_level("string_length") + increased_cfg = curriculum.generate_configuration(base_value) + assert increased_cfg.min_string_length == 10 and increased_cfg.max_string_length == 100