diff --git a/GALLERY.md b/GALLERY.md index 29946d36..961c06ac 100644 --- a/GALLERY.md +++ b/GALLERY.md @@ -19,6 +19,7 @@ This gallery shows examples from all available datasets using their default conf - [game_of_life](#game_of_life) - [gcd](#gcd) - [intermediate_integration](#intermediate_integration) +- [largest_island](#largest_island) - [lcm](#lcm) - [leg_counting](#leg_counting) - [letter_counting](#letter_counting) @@ -34,7 +35,6 @@ This gallery shows examples from all available datasets using their default conf - [prime_factorization](#prime_factorization) - [propositional_logic](#propositional_logic) - [quantum_lock](#quantum_lock) -- [largest_island](#largest_island) - [rubiks_cube](#rubiks_cube) - [sentence_reordering](#sentence_reordering) - [simple_equations](#simple_equations) @@ -885,6 +885,92 @@ Metadata: {'integrand': '2*asin(x)', 'problem_type': 'by_parts', 'variable': 'x' ```` +### largest_island +Generates Largest Island exercises with configurable difficulty + +Default configuration: +```python +rows = 10 +cols = 10 +max_num_islands = 5 +max_island_size = 10 +size = 500 +seed = 42 +``` + +Example tasks: +```` +Example 1: +Question: You are given the following 10 x 10 binary matrix grid: +0 0 0 1 0 0 0 0 0 0 +1 1 0 1 0 0 0 0 0 1 +0 1 0 1 1 0 0 0 0 1 +0 1 0 0 0 0 0 0 0 1 +0 0 0 0 0 0 0 0 0 1 +0 0 0 0 0 0 0 0 1 1 +0 0 0 0 0 0 0 0 1 0 +0 0 0 0 0 0 0 0 1 0 +1 1 0 1 1 0 0 0 1 1 +1 1 1 1 1 0 0 0 0 0 + +An island is a group of 1's (representing land) connected 4-directionally (horizontal or vertical). +You may assume all four edges of the grid are surrounded by water. + +The area of an island is the number of cells with a value 1 in the island. + +Return the maximum area of an island in grid. If there is no island, return 0. + +Answer: 10 +Metadata: {'grid': [[0, 0, 0, 1, 0, 0, 0, 0, 0, 0], [1, 1, 0, 1, 0, 0, 0, 0, 0, 1], [0, 1, 0, 1, 1, 0, 0, 0, 0, 1], [0, 1, 0, 0, 0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 0, 0, 0, 0, 1, 1], [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], [1, 1, 0, 1, 1, 0, 0, 0, 1, 1], [1, 1, 1, 1, 1, 0, 0, 0, 0, 0]], 'solution': 10} + +Example 2: +Question: You are given the following 10 x 10 binary matrix grid: +0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 + +An island is a group of 1's (representing land) connected 4-directionally (horizontal or vertical). +You may assume all four edges of the grid are surrounded by water. + +The area of an island is the number of cells with a value 1 in the island. + +Return the maximum area of an island in grid. If there is no island, return 0. + +Answer: 0 +Metadata: {'grid': [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], 'solution': 0} + +Example 3: +Question: You are given the following 10 x 10 binary matrix grid: +0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 +1 1 0 0 0 0 0 0 0 0 +1 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 1 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 0 0 +0 0 0 0 0 0 0 0 1 0 +0 0 0 0 0 0 0 0 0 0 + +An island is a group of 1's (representing land) connected 4-directionally (horizontal or vertical). +You may assume all four edges of the grid are surrounded by water. + +The area of an island is the number of cells with a value 1 in the island. + +Return the maximum area of an island in grid. If there is no island, return 0. + +Answer: 3 +Metadata: {'grid': [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], 'solution': 3} + +```` + ### lcm Generates Least Common Multiple (LCM) tasks @@ -1550,93 +1636,6 @@ Metadata: {'difficulty': 10, 'solution_path': ['B', 'B', 'B', 'B', 'B', 'B', 'B' ```` -### largest_island - -Generate a grid with islands and find the largest one - -Default configuration: -```python -rows = 10 -cols = 10 -max_num_islands = 5 -max_island_size = 10 -``` - -Example tasks: -```` -Example 1: -Question: You are given the following 10 x 10 binary matrix grid: -0 0 0 1 0 0 0 0 0 0 -1 1 0 1 0 0 0 0 0 1 -0 1 0 1 1 0 0 0 0 1 -0 1 0 0 0 0 0 0 0 1 -0 0 0 0 0 0 0 0 0 1 -0 0 0 0 0 0 0 0 1 1 -0 0 0 0 0 0 0 0 1 0 -0 0 0 0 0 0 0 0 1 0 -1 1 0 1 1 0 0 0 1 1 -1 1 1 1 1 0 0 0 0 0 - -An island is a group of 1's (representing land) connected 4-directionally (horizontal or vertical). -You may assume all four edges of the grid are surrounded by water. - -The area of an island is the number of cells with a value 1 in the island. - -Return the maximum area of an island in grid. If there is no island, return 0. - -Answer: 10 - -Metadata: {'grid': [[0, 0, 0, 1, 0, 0, 0, 0, 0, 0], [1, 1, 0, 1, 0, 0, 0, 0, 0, 1], [0, 1, 0, 1, 1, 0, 0, 0, 0, 1], [0, 1, 0, 0, 0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 0, 0, 0, 0, 1, 1], [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], [1, 1, 0, 1, 1, 0, 0, 0, 1, 1], [1, 1, 1, 1, 1, 0, 0, 0, 0, 0]], 'solution': 10} - -Example 2: -Question: You are given the following 10 x 10 binary matrix grid: -0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 - -An island is a group of 1's (representing land) connected 4-directionally (horizontal or vertical). -You may assume all four edges of the grid are surrounded by water. - -The area of an island is the number of cells with a value 1 in the island. - -Return the maximum area of an island in grid. If there is no island, return 0. - -Answer: 0 - -Metadata: {'grid': [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], 'solution': 0} - -Example 3: -Question: You are given the following 10 x 10 binary matrix grid: -0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 -1 1 0 0 0 0 0 0 0 0 -1 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 1 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 0 0 -0 0 0 0 0 0 0 0 1 0 -0 0 0 0 0 0 0 0 0 0 - -An island is a group of 1's (representing land) connected 4-directionally (horizontal or vertical). -You may assume all four edges of the grid are surrounded by water. - -The area of an island is the number of cells with a value 1 in the island. - -Return the maximum area of an island in grid. If there is no island, return 0. - -Answer: 3 - -Metadata: {'grid': [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], 'solution': 3} -```` - ### rubiks_cube Generates RubiksCube tasks @@ -2122,7 +2121,7 @@ Example tasks: ```` Example 1: Question: Transform the word ladder 'HAND' to 'GLEE' by changing one letter at a time. -Answer: HAND,BAND,BEND,FEND,FEED,FLED,FLEE,GLEE +Answer: HAND,SAND,SEND,SEED,FEED,FLED,FLEE,GLEE Metadata: {'start_word': 'HAND', 'end_word': 'GLEE', 'word_length': 4, 'chain_length': 8} Example 2: @@ -2132,7 +2131,7 @@ Metadata: {'start_word': 'JAZZ', 'end_word': 'DORM', 'word_length': 4, 'chain_le Example 3: Question: Transform the word ladder 'SNOG' to 'SUQS' by changing one letter at a time. -Answer: SNOG,SNOW,SHOW,SHEW,SHES,SUES,SUQS +Answer: SNOG,SNOT,SNIT,SUIT,SUET,SUES,SUQS Metadata: {'start_word': 'SNOG', 'end_word': 'SUQS', 'word_length': 4, 'chain_length': 7} ```` @@ -2218,68 +2217,67 @@ Example tasks: ```` Example 1: Question: This is a logic puzzle. There are 4 houses (numbered 1 on the left, 4 on the right), from the perspective of someone standing across the street from them. Each has a different person in them. They have different characteristics: - - Each person has a unique name: arnold, eric, alice, peter - - People use different phone models: samsung galaxy s21, iphone 13, google pixel 6, oneplus 9 - - Each person has a favorite drink: tea, water, milk, coffee - - The people keep different animals: fish, cat, horse, bird - -1. The tea drinker is in the second house. -2. The person who uses an iPhone 13 is in the third house. -3. Peter and the person who uses a OnePlus 9 are next to each other. -4. Arnold is in the second house. -5. Peter and the cat lover are next to each other. -6. The person who uses a Google Pixel 6 is the person who likes milk. -7. Alice is the person who likes milk. -8. The fish enthusiast is in the third house. -9. The coffee drinker and Alice are next to each other. -10. Peter is the bird keeper. + - Each person has a unique name: carol, arnold, alice, bob + - People use different phone models: huawei p50, samsung galaxy s21, oneplus 9, google pixel 6 + - Each person has a favorite drink: milk, boba tea, coffee, water + - The people keep different animals: bird, cat, fish, dog + +1. Alice is the cat lover. +2. The person who likes milk is in the third house. +3. The person who uses a Huawei P50 is Bob. +4. The one who only drinks water is the bird keeper. +5. The cat lover is in the second house. +6. The boba tea drinker is the dog owner. +7. The person who uses a Google Pixel 6 is directly left of Carol. +8. The one who only drinks water is Carol. +9. Carol is the person who uses a OnePlus 9. What is Name of the person who lives in House 1? -Answer: peter +Answer: bob Metadata: {'num_people': 4, 'num_characteristics': 4} Example 2: Question: This is a logic puzzle. There are 4 houses (numbered 1 on the left, 4 on the right), from the perspective of someone standing across the street from them. Each has a different person in them. They have different characteristics: - - Each person has a unique name: alice, eric, arnold, peter - - Each mother is accompanied by their child: fred, samantha, meredith, bella - - The people are of nationalities: norwegian, swede, brit, dane - - Everyone has something different for lunch: grilled cheese, pizza, stew, spaghetti - -1. The Norwegian is Peter. -2. The person's child is named Meredith and the person's child is named Fred are next to each other. -3. Peter and the Swedish person are next to each other. -4. Eric is directly left of the person's child is named Samantha. -5. The person who loves the spaghetti eater is directly left of the person's child is named Bella. -6. The person's child is named Fred is the person who loves the stew. -7. The person who is a pizza lover is the person's child is named Meredith. -8. The Dane is Eric. -9. The person who loves the stew and Peter are next to each other. -10. The person's child is named Samantha and Arnold are next to each other. + - Each person has a unique name: alice, bob, arnold, carol + - Each mother is accompanied by their child: alice, bella, billy, timothy + - The people are of nationalities: brit, german, chinese, dane + - Everyone has something different for lunch: soup, stir fry, grilled cheese, pizza + +1. The British person is Arnold. +2. The person's child is named Alice is directly left of the person who loves the soup. +3. The person who loves stir fry is the person's child is named Bella. +4. The Chinese is Carol. +5. The German is the person's child is named Bella. +6. The person's child is named Bella is Bob. +7. The person who loves the soup is in the second house. +8. The person who loves the soup is the British person. +9. The person's child is named Alice is Carol. +10. The British person is directly left of the German. +11. The person who is the mother of Billy is the person who is a pizza lover. What is Name of the person who lives in House 1? -Answer: alice +Answer: carol Metadata: {'num_people': 4, 'num_characteristics': 4} Example 3: Question: This is a logic puzzle. There are 4 houses (numbered 1 on the left, 4 on the right), from the perspective of someone standing across the street from them. Each has a different person in them. They have different characteristics: - - Each person has a unique name: alice, peter, eric, arnold - - Everyone has a different favorite cigar: prince, dunhill, pall mall, blue master - - Everyone has something different for lunch: stew, pizza, spaghetti, grilled cheese - - Each person has a favorite color: green, red, yellow, white - -1. The person who smokes Blue Master is in the first house. -2. The person who loves yellow and the person whose favorite color is red are next to each other. -3. The Dunhill smoker is the person who loves yellow. -4. Peter is directly left of the person who is a pizza lover. -5. The person who loves the spaghetti eater and the Dunhill smoker are next to each other. -6. The person whose favorite color is red is the person who loves eating grilled cheese. -7. The person who loves yellow is Arnold. -8. The person who loves eating grilled cheese and the Prince smoker are next to each other. -9. The person who loves white is the person who is a pizza lover. -10. Eric is the person who loves white. + - Each person has a unique name: alice, arnold, bob, carol + - Everyone has a different favorite cigar: pall mall, dunhill, blue master, prince + - Everyone has something different for lunch: stir fry, grilled cheese, soup, pizza + - Each person has a favorite color: blue, purple, brown, white + +1. The person who loves white is the person who loves stir fry. +2. The person who loves brown is directly left of the Prince smoker. +3. The person who is a pizza lover and Arnold are next to each other. +4. The person partial to Pall Mall is the person who loves white. +5. Alice is the person who loves the soup. +6. The person partial to Pall Mall is directly left of the person who loves the soup. +7. The person who smokes Blue Master is directly left of the Dunhill smoker. +8. The Dunhill smoker is Bob. +9. The person who loves the soup is the person who loves blue. What is Name of the person who lives in House 1? -Answer: alice +Answer: carol Metadata: {'num_people': 4, 'num_characteristics': 4} ```` diff --git a/reasoning_gym/logic/contrib/logic_puzzle/clues.py b/reasoning_gym/logic/contrib/logic_puzzle/clues.py index b5b01e37..1fddcc24 100644 --- a/reasoning_gym/logic/contrib/logic_puzzle/clues.py +++ b/reasoning_gym/logic/contrib/logic_puzzle/clues.py @@ -44,6 +44,9 @@ def as_cnf(self) -> Iterable[Tuple[str]]: ... @abstractmethod def __repr__(self) -> str: ... + def __lt__(self, other) -> bool: + return str(self) < str(other) + def comb(value: Literal, house: int) -> str: """Format how a value is shown at a given house""" diff --git a/reasoning_gym/logic/contrib/logic_puzzle/generate.py b/reasoning_gym/logic/contrib/logic_puzzle/generate.py index e9c5b870..e5404dff 100644 --- a/reasoning_gym/logic/contrib/logic_puzzle/generate.py +++ b/reasoning_gym/logic/contrib/logic_puzzle/generate.py @@ -4,6 +4,7 @@ This is a driver script that can be used to generate new zebra puzzles. """ +from collections import OrderedDict from itertools import product from random import Random from typing import Dict, Iterable, List, Set, Tuple, Type @@ -17,7 +18,7 @@ from .clues import Clue, beside, consecutive, found_at, left_of, not_at, one_between, right_of, same_house, two_between -def generate_found_at(puzzle: Puzzle, solution: Dict[Literal, int]) -> Set[Clue]: +def generate_found_at(puzzle: Puzzle, solution: OrderedDict[Literal, int]) -> Set[Clue]: """Generate the `found_at` / `not_at` Clue instances""" clues: Set[Clue] = set() for element, loc in solution.items(): @@ -26,18 +27,7 @@ def generate_found_at(puzzle: Puzzle, solution: Dict[Literal, int]) -> Set[Clue] return clues -def generate_not_found_at(puzzle: Puzzle, solution: Dict[Literal, int]) -> Set[Clue]: - """Generate the `found_at` / `not_at` Clue instances""" - clues: Set[Clue] = set() - for element, loc in solution.items(): - for house in puzzle.houses: - if house != loc: - clues.add(not_at(element, house)) - - return clues - - -def generate_same_house(puzzle: Puzzle, solution: Dict[Literal, int]) -> Set[Clue]: +def generate_same_house(puzzle: Puzzle, solution: OrderedDict[Literal, int]) -> Set[Clue]: """Generate the `same_house` Clue instances""" clues: Set[Clue] = set() @@ -52,7 +42,7 @@ def generate_same_house(puzzle: Puzzle, solution: Dict[Literal, int]) -> Set[Clu return clues -def generate_consecutive_beside(puzzle: Puzzle, solution: Dict[Literal, int]) -> Set[Clue]: +def generate_consecutive_beside(puzzle: Puzzle, solution: OrderedDict[Literal, int]) -> Set[Clue]: """Generate the `consecutive` / `beside` Clue instances (Note that consecutive is just a more informative version of beside. Since they have the same @@ -64,7 +54,7 @@ def generate_consecutive_beside(puzzle: Puzzle, solution: Dict[Literal, int]) -> items_left = {item: loc for item, loc in solution.items() if loc == left} items_right = {item: loc for item, loc in solution.items() if loc == right} pairs: Set[Tuple[Literal, Literal]] = {(item1, item2) for item1, item2 in product(items_left, items_right)} - for pair in pairs: + for pair in sorted(pairs): # consecutive is just a more informative version of beside, but they have same structure # because of this, don't include both if puzzle.rng.randint(0, 1) == 0: @@ -75,59 +65,7 @@ def generate_consecutive_beside(puzzle: Puzzle, solution: Dict[Literal, int]) -> return clues -def generate_left_right_of(puzzle: Puzzle, solution: Dict[Literal, int]) -> Set[Clue]: - """Generate the `left_of` / `right_of` Clue instances - - Note that since (x left-of y) is guaranteed to be redundant with (b right-of a), we only add - one of these clues to the final set. - """ - - clues: Set[Clue] = set() - for left, right in product(puzzle.houses, puzzle.houses): - if left >= right: - continue - - items_left = {item: loc for item, loc in solution.items() if loc == left} - items_right = {item: loc for item, loc in solution.items() if loc == right} - pairs: Set[Tuple[Literal, Literal]] = {(item1, item2) for item1, item2 in product(items_left, items_right)} - for pair in pairs: - if puzzle.rng.randint(0, 1) == 0: - clues.add(left_of(pair[0], pair[1], puzzle.houses)) - else: - clues.add(right_of(pair[1], pair[0], puzzle.houses)) - - return clues - - -def generate_one_between(puzzle: Puzzle, solution: Dict[Literal, int]) -> Set[Clue]: - """Generate the `one_between` Clue instances""" - - clues: Set[Clue] = set() - for left, right in zip(puzzle.houses, puzzle.houses[2:]): - items_left = {item: loc for item, loc in solution.items() if loc == left} - items_right = {item: loc for item, loc in solution.items() if loc == right} - pairs: Set[Tuple[Literal, Literal]] = {(item1, item2) for item1, item2 in product(items_left, items_right)} - for pair in pairs: - clues.add(one_between(pair[0], pair[1], puzzle.houses)) - - return clues - - -def generate_two_between(puzzle: Puzzle, solution: Dict[Literal, int]) -> Set[Clue]: - """Generate the `two_between` Clue instances""" - - clues: Set[Clue] = set() - for left, right in zip(puzzle.houses, puzzle.houses[3:]): - items_left = {item: loc for item, loc in solution.items() if loc == left} - items_right = {item: loc for item, loc in solution.items() if loc == right} - pairs: Set[Tuple[Literal, Literal]] = {(item1, item2) for item1, item2 in product(items_left, items_right)} - for pair in pairs: - clues.add(two_between(pair[0], pair[1], puzzle.houses)) - - return clues - - -def has_unique_solution(puzzle: Puzzle, clues: Iterable[Clue], remove_after=False) -> bool: +def has_unique_solution(puzzle: Puzzle, clues: Iterable[Clue], remove_after: bool = False) -> bool: """Test if a puzzle has a unique solution under a given set of clues.""" with puzzle.with_clues(clues, remove_after=remove_after): @@ -163,8 +101,8 @@ def weight(clue: Clue) -> float: return weights.get(type(clue), 1) - weights = [weight(clue) for clue in clues] - candidates: Set[Clue] = set(puzzle.rng.choices(list(clues), weights, k=n)) + weights = [weight(clue) for clue in sorted(clues)] + candidates: Set[Clue] = set(puzzle.rng.choices(sorted(clues), weights, k=n)) candidates = candidates - must_have clues = clues.difference(candidates) if has_unique_solution(puzzle, clues): @@ -186,7 +124,7 @@ def reduce_individually( and added to `removed`. If no clues can be removed, we return the original two sets. """ - candidates = set(puzzle.rng.sample(list(clues), len(clues))) + candidates = puzzle.rng.sample(sorted(clues), len(clues)) for clue in candidates: if clue not in must_have: clues.remove(clue) @@ -301,12 +239,12 @@ def question_generation(rng: Random, col_name, table_data): return questions_data -def generate_solution_dict(rng: Random, selected_elements: List[Literal], n: int) -> Dict[Literal, int]: - solution = {} +def generate_solution_dict(rng: Random, selected_elements: List[Literal], n: int) -> OrderedDict[Literal, int]: + solution = OrderedDict() house_ids = list(range(1, n + 1)) for element in selected_elements: rng.shuffle(house_ids) - attributes: List[Literal] = list(element.__members__.values()) + attributes: List[Literal] = sorted(element.__members__.values()) for i in range(n): solution[attributes[i]] = house_ids[i] return solution @@ -314,11 +252,11 @@ def generate_solution_dict(rng: Random, selected_elements: List[Literal], n: int def wrap_up_dict(rng: Random, random_elements, solution, puzzle, reduced, extra_clues, context, K, M): col_names = [e.__name__ for e in random_elements] - house_data = {} + house_data = OrderedDict() for item, house in solution.items(): element_name, attrname = str(item).split(".") if house not in house_data: - house_data[house] = {} + house_data[house] = OrderedDict() house_data[house][element_name] = attrname table_data = [] for i in range(1, len(house_data) + 1): @@ -333,7 +271,7 @@ def wrap_up_dict(rng: Random, random_elements, solution, puzzle, reduced, extra_ ## Generate multiple-choice questions q_data = question_generation(rng, col_names, table_data) - all_in_one = {} + all_in_one = OrderedDict() all_in_one["size"] = f"{K}*{M}" all_in_one["puzzle_context"] = context all_in_one["core_rules"] = [str(clue) for clue in reduced] @@ -342,7 +280,9 @@ def wrap_up_dict(rng: Random, random_elements, solution, puzzle, reduced, extra_ all_in_one["extra_rules_types"] = [str(type(clue)) for clue in extra_clues] all_in_one["puzzle"] = str(puzzle) all_in_one["questions"] = q_data - all_in_one["solution"] = {"table_str": table, "table_rows": table_data, "table_header": col_names} + all_in_one["solution"] = OrderedDict( + (("table_str", table), ("table_rows", table_data), ("table_header", col_names)) + ) return all_in_one diff --git a/reasoning_gym/logic/contrib/logic_puzzle/literals.py b/reasoning_gym/logic/contrib/logic_puzzle/literals.py index f1ecfabc..7e6cc354 100644 --- a/reasoning_gym/logic/contrib/logic_puzzle/literals.py +++ b/reasoning_gym/logic/contrib/logic_puzzle/literals.py @@ -29,6 +29,9 @@ class Literal(Enum): def description(cls) -> str: return "".join(cls.__members__) # type:ignore + def __lt__(self, other) -> bool: + return self.value < other.value + class Color(Literal): @classmethod diff --git a/reasoning_gym/logic/contrib/logic_puzzle/puzzle.py b/reasoning_gym/logic/contrib/logic_puzzle/puzzle.py index 56199e5f..afb6b752 100644 --- a/reasoning_gym/logic/contrib/logic_puzzle/puzzle.py +++ b/reasoning_gym/logic/contrib/logic_puzzle/puzzle.py @@ -116,7 +116,7 @@ def remove_clue(self, clue: Clue) -> Puzzle: def with_clues(self, clues: Iterable[Clue], remove_after=True) -> Generator[Puzzle]: """Create a context in which this Puzzle temporarily has clues added to it""" - clues = list(clues) # so we don't accidentally exhaust the iterable + clues = sorted(clues) # so we don't accidentally exhaust the iterable empty_clue = len(self.clues) == 0 for clue in clues: self.add_clue(clue) @@ -133,7 +133,7 @@ def as_cnf(self) -> List[Tuple[str]]: # this would be a comprehension if we could use iterable unpacking cnf = [] - for clue in self.clues: + for clue in sorted(self.clues): cnf.extend(clue.as_cnf()) cnf.extend(self.constraints) @@ -154,7 +154,10 @@ def __repr__(self) -> str: s += f" - {desc}: " + ", ".join(e.name.replace("_", " ") for e in literals) + "\n" s += "\n" - s += "".join(f"{i + 1}. {clue}\n" for i, clue in enumerate(self.clues)) + # generate deterministically shuffled order + clues = sorted(self.clues) + self.rng.shuffle(clues) + s += "".join(f"{i + 1}. {clue}\n" for i, clue in enumerate(clues)) return s