From 01e1c8f9af9710042d7db82f1a6c08dcabd9108f Mon Sep 17 00:00:00 2001 From: Zafir Stojanovski Date: Mon, 3 Mar 2025 21:55:53 +0100 Subject: [PATCH 01/12] fix: Unify Prompts (#254) * remove cot * fix prompt template * fix pool matrix * spiral matrix fixed --- reasoning_gym/algorithmic/ab.py | 14 +-------- reasoning_gym/algorithmic/base_conversion.py | 13 -------- .../algorithmic/binary_alternation.py | 4 --- reasoning_gym/algorithmic/binary_matrix.py | 17 +--------- reasoning_gym/algorithmic/cryptarithm.py | 23 -------------- reasoning_gym/algorithmic/group_anagrams.py | 7 +---- .../algorithmic/isomorphic_strings.py | 13 -------- reasoning_gym/algorithmic/letter_jumble.py | 15 +-------- .../algorithmic/palindrome_generation.py | 8 +---- .../algorithmic/palindrome_partitioning.py | 8 +---- reasoning_gym/algorithmic/pool_matrix.py | 21 ++----------- reasoning_gym/algorithmic/rotate_matrix.py | 12 +------ reasoning_gym/algorithmic/rotten_oranges.py | 7 ----- reasoning_gym/algorithmic/spiral_matrix.py | 21 +++++-------- reasoning_gym/algorithmic/string_insertion.py | 10 +----- .../algorithmic/string_manipulation.py | 13 +------- reasoning_gym/algorithmic/string_splitting.py | 11 ------- reasoning_gym/algorithmic/string_synthesis.py | 10 ------ reasoning_gym/algorithmic/word_sorting.py | 11 +------ reasoning_gym/arithmetic/leg_counting.py | 10 ------ reasoning_gym/arithmetic/number_format.py | 7 +---- reasoning_gym/arithmetic/power_function.py | 14 --------- reasoning_gym/cognition/rectangle_count.py | 28 +---------------- reasoning_gym/games/countdown.py | 1 + reasoning_gym/games/emoji_mystery.py | 21 +++++++------ reasoning_gym/games/mahjong.py | 8 +---- reasoning_gym/games/n_queens.py | 15 +-------- reasoning_gym/games/tower_of_hanoi.py | 11 +++---- reasoning_gym/geometry/advanced_geometry.py | 16 ++++------ reasoning_gym/graphs/shortest_path.py | 19 +++--------- reasoning_gym/logic/propositional_logic.py | 31 +++++++++---------- 31 files changed, 65 insertions(+), 354 deletions(-) diff --git a/reasoning_gym/algorithmic/ab.py b/reasoning_gym/algorithmic/ab.py index 139e4af8..9ec679af 100644 --- a/reasoning_gym/algorithmic/ab.py +++ b/reasoning_gym/algorithmic/ab.py @@ -102,19 +102,7 @@ def __getitem__(self, idx: int) -> dict: B# #B ... becomes ... nothing In other words, whenever two neighbor tokens have their '#' facing each-other, -they must be rewritten according to the corresponding rule. For example, the -first example shown here is computed as: - - B# A# #B #A B# = - B# #B A# #A B# = - A# #A B# = - B# - -The steps were: -1. We replaced `A# #B` by `#B A#`. -2. We replaced `B# #B` by nothing. -3. We replaced `A# #A` by nothing. -The final result was just `B#`. +they must be rewritten according to the corresponding rule. Now, consider the following program: diff --git a/reasoning_gym/algorithmic/base_conversion.py b/reasoning_gym/algorithmic/base_conversion.py index bd897a89..5ab5b5b5 100644 --- a/reasoning_gym/algorithmic/base_conversion.py +++ b/reasoning_gym/algorithmic/base_conversion.py @@ -10,19 +10,6 @@ If the target base is > 10, use lowercase letters a-z for digits above 9. -Example: -- Input: Convert the base-9 number 440 to base-5 -- Output: 2420 -- Explanation - - First, we convert the base-9 number 440 to base-10: 4 * 9**2 + 4 * 9**1 + 0 * 9**0 = 324 + 36 + 0 = 360 - - Next, we convert the base-10 number 360 to base-5: - - 360 // 5 = 72 remainder 0 - - 72 // 5 = 14 remainder 2 - - 14 // 5 = 2 remainder 4 - - 2 // 5 = 0 remainder 2 - - Reading the remainders in reverse order gives us the base-5 number 2 4 2 0 - - Hence, the final answer is 2420 - Now, convert the {source_name} number {source_repr} to {target_name} """ diff --git a/reasoning_gym/algorithmic/binary_alternation.py b/reasoning_gym/algorithmic/binary_alternation.py index ca204c6d..ea50b0c8 100644 --- a/reasoning_gym/algorithmic/binary_alternation.py +++ b/reasoning_gym/algorithmic/binary_alternation.py @@ -15,10 +15,6 @@ Any two characters may be swapped, even if they are not adjacent. -Example: -- Input: Determine the minimum number of swaps to make the following binary string alternating: 111000 -- Output: 1 - Now, determine the minimum number of swaps to make the following binary string alternating: {string} """ diff --git a/reasoning_gym/algorithmic/binary_matrix.py b/reasoning_gym/algorithmic/binary_matrix.py index 92317e78..4584a7fb 100644 --- a/reasoning_gym/algorithmic/binary_matrix.py +++ b/reasoning_gym/algorithmic/binary_matrix.py @@ -13,22 +13,7 @@ QUESTION_TEMPLATE = """Given a square matrix, your job is to find the taxicab (Manhattan) distance of the nearest 0 for each cell. -Example: -- Input: Find the distance to the nearest 0 for each cell in the matrix below: -0 0 0 -0 1 0 -1 1 1 -- Output: -0 0 0 -0 1 0 -1 2 1 -- Explanation - - Each cell with a 0 has a distance of 0 to itself. - - The cell at (1, 1) has a distance of 1 to the nearest 0 (any of the three 0's at (1, 0), (0, 1), (1, 2)). - - The cell at (2, 0) has a distance of 1 to the nearest 0 (the 0 at (1, 0)). - - The cell at (2, 1) has a distance of 2 to the nearest 0 (any of the two 0's at (1, 0), (1, 2)) - - The cell at (2, 2) has a distance of 1 to the nearest 0 (the 0 at (1, 2)). - - Hence, the final answer is the matrix is the output shown above, where each cell contains the distance to the nearest 0, in the same format as the input matrix. +The output should be a matrix of the same size as the input matrix, where each cell contains the distance to the nearest 0. Find the distance to the nearest 0 for each cell in the matrix below: {matrix} diff --git a/reasoning_gym/algorithmic/cryptarithm.py b/reasoning_gym/algorithmic/cryptarithm.py index 15264254..f0946278 100644 --- a/reasoning_gym/algorithmic/cryptarithm.py +++ b/reasoning_gym/algorithmic/cryptarithm.py @@ -17,26 +17,6 @@ from ..factory import ProceduralDataset, register_dataset -EXAMPLE_CASE = """- Input: - BASE -+ BALL ------- - GAMES - -- Output: B=7, A=4, S=8, E=3, L=5, M=9, G=1 -- Explanation: - * BASE + BALL = GAMES, two 4-digit numbers sum to 5 digits, so G = 1. - * Units: E + L = S (no carry). - * Tens: S + L = E + 10 (carry 1). Substitute S = E + L to get E + 2L = E + 10, so L = 5. - * Since S = E + 5 and S is one digit, E < 5. - * Hundreds: 2A + 1 = M (with carry). - * Thousands: 2B = A + 10 (carry makes G = 1). So A = 2B - 10. - * Try B = 7: Then A = 4 and M = 2(4) + 1 = 9. - * With E < 5, try E = 3: Then S = 8. - * Solution: B = 7, A = 4, S = 8, E = 3, L = 5, M = 9, G = 1 - * Verify: BASE (7483) + BALL (7455) = GAMES (14938). -""" - @dataclass class CryptarithmConfig: @@ -45,7 +25,6 @@ class CryptarithmConfig: min_words: int = 2 # Minimum number of addends max_words: int = 3 # Maximum number of addends allow_leading_zero: bool = False - include_example: bool = True seed: Optional[int] = None size: int = 500 # Number of puzzle instances to generate @@ -189,8 +168,6 @@ def int_to_letter_str(num: int) -> str: ) + 'Provide a comma separated mapping from letters to digits that satisfies the equation in your final answer. Output format: "A=1,B=2,C=3" (without quotes)\n' ) - if self.config.include_example: - question_str += "\nHere's an example:\n" + EXAMPLE_CASE # 8) Create a human-readable answer, e.g. "A=1,B=0,C=9,..." sorted_letter_keys = sorted(letter_to_digit.keys()) diff --git a/reasoning_gym/algorithmic/group_anagrams.py b/reasoning_gym/algorithmic/group_anagrams.py index caf46357..b6630ac0 100644 --- a/reasoning_gym/algorithmic/group_anagrams.py +++ b/reasoning_gym/algorithmic/group_anagrams.py @@ -21,12 +21,7 @@ Your job is to group the anagrams together. You can return the answer in any order. -Example: -Input: ["eat", "tea", "tan", "ate", "nat", "bat"] -Output: [["bat"], ["nat", "tan"], ["ate", "eat", "tea"]] -Explanation: - - There is no string in the input that can be rearranged to form "bat". - - The strings "nat" and "tan" are anagrams as they can be rearranged to form each other. +The output is a list of lists of strings, where each outer list contains a group of anagrams, e.g. [["eat", "tea"], ["tan", "nat"]]. Group the following list of words into anagrams: {words} diff --git a/reasoning_gym/algorithmic/isomorphic_strings.py b/reasoning_gym/algorithmic/isomorphic_strings.py index 3b4a59e5..bba46343 100644 --- a/reasoning_gym/algorithmic/isomorphic_strings.py +++ b/reasoning_gym/algorithmic/isomorphic_strings.py @@ -18,19 +18,6 @@ No two characters may map to the same character, but a character may map to itself. -Example 1: -Input: egg add -Output: True -Explanation: The strings s and t can be made identical by: - - Mapping 'e' to 'a'. - - Mapping 'g' to 'd'. - -Example 2: -Input: foo bar -Output: False -Explanation: - - The strings cannot be made identical as 'o' needs to be mapped to both 'a' and 'r'. - Return True if the following two strings are isomorphic, or False otherwise: {s} {t} """ diff --git a/reasoning_gym/algorithmic/letter_jumble.py b/reasoning_gym/algorithmic/letter_jumble.py index 3aab43f8..5917e55c 100644 --- a/reasoning_gym/algorithmic/letter_jumble.py +++ b/reasoning_gym/algorithmic/letter_jumble.py @@ -15,20 +15,7 @@ The order of the words in the sentence is preserved. Moreover, the style of the sentence is preserved (i.e. punctuation, capitalization, new lines, etc.). -Example: -- Input: Unscramble these words: raendgmeins yWh nya hilcd anc od hatt -- Output: meanderings Why any child can do that -- Explanation - - We unscramble each of the words independently. - - raendgmeins -> meanderings - - yWh -> Why - - nya -> any - - hilcd -> child - - anc -> can - - od -> do - - hatt -> that - - The final answer is: meanderings Why any child can do that - - Notice that the order of the words is preserved, no new words / symbols (e.g. new lines) are added. +Your output should be a sentence with the words unscrambled. Now, unscramble these words: {words} """ diff --git a/reasoning_gym/algorithmic/palindrome_generation.py b/reasoning_gym/algorithmic/palindrome_generation.py index 0e54579a..2f7b5fb5 100644 --- a/reasoning_gym/algorithmic/palindrome_generation.py +++ b/reasoning_gym/algorithmic/palindrome_generation.py @@ -11,13 +11,7 @@ If there are multiple possible answers, only respond with one of them. You must use all the letters provided. -Example: -- Input: Form a valid palindrome using the following letters: a, a, b -- Output: aba -- Explanation: - - The phrase aba reads the same forwards and backwards. - - The output answer is a valid palindrome using all the letters provided. - - The answer is a string, rather than a list of characters. +Your output should be a single string, with no spaces or punctuation. Now, form a valid palindrome using the following letters: {letters} """ diff --git a/reasoning_gym/algorithmic/palindrome_partitioning.py b/reasoning_gym/algorithmic/palindrome_partitioning.py index e0d41870..8a0c07b3 100644 --- a/reasoning_gym/algorithmic/palindrome_partitioning.py +++ b/reasoning_gym/algorithmic/palindrome_partitioning.py @@ -18,13 +18,7 @@ You may return all possible palindrome partitioning in any order. -Example: -- Input: Partition the following string into palindromes: aab -- Output: [["a","a","b"],["aa","b"]] -- Explanation: - - One way to partition the string is "a" | "a" | "b", where each substring is a palindrome. - - Another way to partition the string is "aa" | "b", where again each substring is a palindrome. - - Therefore, the final result is a list of the two palindrome partitions. +Your output should be a list of lists, where each list represents a palindrome partition, e.g. [["a","a","b"],["aa","b"]]. Partition the following string into palindromes: {string} """ diff --git a/reasoning_gym/algorithmic/pool_matrix.py b/reasoning_gym/algorithmic/pool_matrix.py index 002c0c0c..bf839ad4 100644 --- a/reasoning_gym/algorithmic/pool_matrix.py +++ b/reasoning_gym/algorithmic/pool_matrix.py @@ -11,25 +11,8 @@ QUESTION_TEMPLATE = """Your job is to perform max/average pooling on the given matrix. The stride is equal to the kernel size, meaning there is no overlap between the pooling regions. -Example 1: -- Input: Perform max pooling on the following matrix with a kernel size of 2: -1 2 3 4 -5 6 7 8 -9 10 11 12 -13 14 15 16 -- Output: -6 8 -14 16 - -Example 2: -- Input: Perform average pooling on the following matrix with a kernel size of 2: -1 2 3 4 -5 6 7 8 -9 10 11 12 -13 14 15 16 -- Output: -3.5 5.5 -11.5 13.5 +Your output should be a matrix in the same format as the input matrix. +The output matrix is smaller than the input matrix when the kernel size is greater than 1, and its elements may be floating-point numbers. Perform {pool_type} pooling on the following matrix with a kernel size of {pool_size}: {matrix} diff --git a/reasoning_gym/algorithmic/rotate_matrix.py b/reasoning_gym/algorithmic/rotate_matrix.py index adeaa47c..2154243f 100644 --- a/reasoning_gym/algorithmic/rotate_matrix.py +++ b/reasoning_gym/algorithmic/rotate_matrix.py @@ -13,17 +13,7 @@ QUESTION_TEMPLATE = """Given a square matrix, your job is to rotate it clockwise. -Example: - -Input: Rotate the matrix below by 90 degrees clockwise: -1 2 3 -4 5 6 -7 8 9 - -Output: -7 4 1 -8 5 2 -9 6 3 +Your output should be a matrix in the same format as the input. Rotate the matrix below by {degrees} degrees clockwise: {matrix} diff --git a/reasoning_gym/algorithmic/rotten_oranges.py b/reasoning_gym/algorithmic/rotten_oranges.py index 92e35a20..3b849c4d 100644 --- a/reasoning_gym/algorithmic/rotten_oranges.py +++ b/reasoning_gym/algorithmic/rotten_oranges.py @@ -21,13 +21,6 @@ Your task is determine the minimum number of minutes that must elapse until no cell has a fresh orange. If this is impossible, return -1. -Example: -- Input: Determine the minimum number of minutes that must elapse until no cell in the grid below has a fresh orange: - 2 1 1 - 1 1 0 - 0 1 1 -- Output: 4 - Now, determine the minimum number of minutes that must elapse until no cell in the grid below has a fresh orange: {matrix} """ diff --git a/reasoning_gym/algorithmic/spiral_matrix.py b/reasoning_gym/algorithmic/spiral_matrix.py index 17aff844..63492a37 100644 --- a/reasoning_gym/algorithmic/spiral_matrix.py +++ b/reasoning_gym/algorithmic/spiral_matrix.py @@ -12,19 +12,14 @@ QUESTION_TEMPLATE = """Given a matrix, your job is to generate a list of elements in spiral order, starting from the top-left element. -Example: -- Input: For the matrix below, what is the list of elements in spiral order? -1 2 3 -4 5 6 -7 8 9 -- Output: 1 2 3 6 9 8 7 4 5 -- Explanation: - - We start from the top-left element (1) and move right until we reach the end of the row: 1 2 3 - - Then, we move down until we reach the last column: 1 2 3 6 9 - - Next, we move left until we reach the first column: 1 2 3 6 9 8 7 - - Then, we move up until we reach the second row (i.e. one below the previously traversed row): 1 2 3 6 9 8 7 4 - - Finally, we move right until we reach the second to last column: 1 2 3 6 9 8 7 4 5 - - The output format is a space-separated list of elements in spiral order (as opposed to a python list) +The spiral order is clockwise, starting from the top-left corner. More precisely: +- Start from the top-left corner and move right. +- Move down towards the bottom-right corner. +- Move left towards the bottom-left corner. +- Move up towards the top-right corner. +- Repeat the steps for the inner elements of the matrix until every entry is visited. + +Your output should be a space-separated list of integers, e.g. 1 2 3 4 5 6 For the matrix below, what is the list of elements in spiral order? {matrix} diff --git a/reasoning_gym/algorithmic/string_insertion.py b/reasoning_gym/algorithmic/string_insertion.py index d09b8a92..1d597364 100644 --- a/reasoning_gym/algorithmic/string_insertion.py +++ b/reasoning_gym/algorithmic/string_insertion.py @@ -18,15 +18,7 @@ Once you have inserted a character, you have to skip over the substring and the inserted character and continue the search from the next character. -Example -- Input: DDABCDEEDEAB -- Output: DDABCDAEEDEABD -- Explanation: - - Theere are two inserted characters: DDABCD[A]EEDEAB[D] (shown in square brackets) - - First, we insert A after ABCD. - - Even though with the newly inserted 'A' we can obtain the substring BCD[A], we can't use it to insert another character. - - Lastly, we insert D after DEAB. - - Therefore, the final answer is DDABCDAEEDEABD (represented as a string, instead of a list of characters). +Your output should be a string that has been modified according to the pattern. Given the following string, provide the answer after inserting the characters according to the pattern: {string} """ diff --git a/reasoning_gym/algorithmic/string_manipulation.py b/reasoning_gym/algorithmic/string_manipulation.py index b382921f..434a241e 100644 --- a/reasoning_gym/algorithmic/string_manipulation.py +++ b/reasoning_gym/algorithmic/string_manipulation.py @@ -17,18 +17,7 @@ Once you have applied a rule, repeat the process with the new string until no further transformations can be performed (i.e. the string doesn't change), or a state is repeated. If a state is repeated, the process is terminated, and the repeated state is discarded (i.e. is not considered as the final answer) and the state before the repeated state is considered as the final answer. -Example: -- Input: - - String: abbac - - Rules: - 1. If the string prefix is 'ab', replace it with 'ca'. - 2. If the string prefix is 'ca', replace it with 'bb' and append 'c' to the end. - 3. If the string ends with 'aa', replace it with 'cc'. -- Output: bbbacc -- Explanation: - - In the first iteration, rule 1 is applied to the string abbac, resulting in cabac - - In the second interation, rule 1 doesn't apply, but rule 2 is applied to the string cabac, resulting in bbbacc - - In the third iteration, none of the rules (1, 2, 3) apply, so the process is terminated, and the final answer is bbbacc +Your output should be the final transformed string after applying all the rules. Transform the following string according to the above list of rules: {string} diff --git a/reasoning_gym/algorithmic/string_splitting.py b/reasoning_gym/algorithmic/string_splitting.py index da3b82e0..6679e812 100644 --- a/reasoning_gym/algorithmic/string_splitting.py +++ b/reasoning_gym/algorithmic/string_splitting.py @@ -23,17 +23,6 @@ The output should be the count of each machine and part type after the rules have been exhaustively applied in the following order: A B C X Y Z. For example 1 0 1 5 4 3 means that you have 1 machine A, 0 machine B, 1 machine C, 5 part X, 4 part Y, and 3 part Z. -Example: -- Input: You have 2 machines A, 0 machines B, and 1 machine C. -- Output: 0 0 1 2 0 2 -- Explanation - 0. Initial state: 2 0 1 0 0 0 - 1. We can apply rule 1 and trade 1 machine A for 2 part X and 1 part Y: 1 0 1 2 1 0 - 2. Starting over, we can apply rule 1 again: 0 0 1 4 2 0 - 3. In the next iteration, we can apply rule 5 and trade 1 part X and 1 part Y for 1 part Z: 0 0 1 3 1 1 - 4. In the next iteration, we can apply rule 5 again: 0 0 1 2 0 2 - 5. We can't apply any more rules, so the final answer is 0 0 1 2 0 2 - Now, you have {A_machine} machine A, {B_machine} machine B, and {C_machine} machine C. Provide the count of each machine and part type after applying the above rules. """ diff --git a/reasoning_gym/algorithmic/string_synthesis.py b/reasoning_gym/algorithmic/string_synthesis.py index c78ed35b..63cafa98 100644 --- a/reasoning_gym/algorithmic/string_synthesis.py +++ b/reasoning_gym/algorithmic/string_synthesis.py @@ -23,16 +23,6 @@ The output should be the count of each block type after the rules have been applied in the order they are listed above. For example 1 0 3 0 2 0 0 0 1 means that you have 1 [A] 0 [B] 3 [C] 0 {{A}} 2 {{B}} 0 {{C}} 0 (A) 0 (B) 1 (C). -Example: -- Input: You have 2 [A], 3 [B], and 3 [C]. -- Output: 0 0 0 2 1 0 0 0 0 -- Explanation: - 0. Initial state: 2 3 3 0 0 0 0 0 0 - 1. We can apply Rule 1 and obtain 1 {{A}}. New state: 1 2 2 1 0 0 0 0 0 - 2. We can apply Rule 1 again and obtain 1 {{A}}. New state 0 1 1 2 0 0 0 0 0 - 3. We can apply Rule 3 and obtain 1 {{B}}. New state 0 0 0 2 1 0 0 0 0 - 4. No more rules can be applied. The answer is 0 0 0 2 1 0 0 0 0 - Now, you have {A_square} [A], {B_square} [B], and {C_square} [C] blocks. Provide the count of each block type after applying the above rules. """ diff --git a/reasoning_gym/algorithmic/word_sorting.py b/reasoning_gym/algorithmic/word_sorting.py index 61c8b61d..fc65c976 100644 --- a/reasoning_gym/algorithmic/word_sorting.py +++ b/reasoning_gym/algorithmic/word_sorting.py @@ -21,16 +21,7 @@ class TextTransformation(StrEnum): QUESTION_TEMPLATE = """Your task is to sort words in ascending or descending order using ASCII/Unicode ordering. -Example: -- Input: Sort these words in ascending order (using ASCII/Unicode ordering) and return them as a comma-separated list: freely, idea, indemnify, last, END, solving -- Output: END, freely, idea, indemnify, last, solving -- Explanation: - - Uppercase letters come before lowercase letters, hence why "END" comes first. - - "freely" comes before "idea" because "f" comes before "i". - - "idea" comes before "indemnify" because even though they both start with "i", "d" comes before "n". - - "indemnify" comes before "last" because "i" comes before "l". - - "last" comes before "solving" because "l" comes before "s". - - Finally, the output is provided as a comma separated list of the sorted words. +Your output should be a comma-separated list of words, e.g. word_1, word_2, word_3 Now, sort these words in {direction} order (using ASCII/Unicode ordering) and return them as a comma-separated list: {words} """ diff --git a/reasoning_gym/arithmetic/leg_counting.py b/reasoning_gym/arithmetic/leg_counting.py index f7da2d35..3acc2f32 100644 --- a/reasoning_gym/arithmetic/leg_counting.py +++ b/reasoning_gym/arithmetic/leg_counting.py @@ -56,16 +56,6 @@ QUESTION_TEMPLATE = """Your task is to count how many legs there are in total when given a list of animals. -Example: -- Input: How many legs are there in total if you have 1 duck, 2 deers, 1 spider, 3 cows? -- Output: 30 -- Explanation: - - Ducks have 2 legs each, so 1 duck has 2 legs. - - Deers have 4 legs each, so 2 deers have 8 legs. - - Spiders have 8 legs each, so 1 spider has 8 legs. - - Cows have 4 legs each, so 3 cows have 12 legs. - - Therefore, the total number of legs is 2 + 8 + 8 + 12 = 30 - Now, how many legs are there in total if you have {animals}? """ diff --git a/reasoning_gym/arithmetic/number_format.py b/reasoning_gym/arithmetic/number_format.py index 36e66c1d..0c2b79d5 100644 --- a/reasoning_gym/arithmetic/number_format.py +++ b/reasoning_gym/arithmetic/number_format.py @@ -8,12 +8,7 @@ QUESTION_TEMPLATE = """Your task is to pick the largest/smallest number out of several options. -Example -- Input: Pick the largest number of the following candidates: 857575.23 8.975554e+05 887,555.62 -- Output: 8.975554e+05 -- Explanation: - - Sorting the numbers written in various notations we get: 857575.23 < 887,555.62 < 8.975554e+05 - - Therefore, the largest number is 8.975554e+05 +Your output should be only the number of interest. Now, pick the {size} number of the following candidates: {numbers} """ diff --git a/reasoning_gym/arithmetic/power_function.py b/reasoning_gym/arithmetic/power_function.py index a4fb93c7..879f4848 100644 --- a/reasoning_gym/arithmetic/power_function.py +++ b/reasoning_gym/arithmetic/power_function.py @@ -9,20 +9,6 @@ QUESTION_TEMPLATE = """Your task is to compute an exponentiation of a number. -Example: -- Input: Compute 2^3 -- Output: 8 -- Explanation: - - 2^3 = 2 * 2 * 2 = 8 - - Therefore, the final answer is 8 - -Example: -- Input: Compute 412.5^3 -- Output: 70189453.125 -- Explanation: - - 412.5^3 = 412.5 * 412.5 * 412.5 = 70189453.125 - - Therefore, the final answer is 70189453.125 - Compute {base}^{exponent} """ diff --git a/reasoning_gym/cognition/rectangle_count.py b/reasoning_gym/cognition/rectangle_count.py index 5a86d467..b258b615 100644 --- a/reasoning_gym/cognition/rectangle_count.py +++ b/reasoning_gym/cognition/rectangle_count.py @@ -8,33 +8,7 @@ Single rectangles are outlined with a '#', overlapping rectangles (max 2) are shown with '█'. -Example: -- Input: How many rectangles are in the grid below? - - #### - # # - #### - - - - - - - - - - - ######### - # █## - # █ # - ########█ # - # # - ### -- Output: 3 -- Explanation: - - The first rectangle is the 3x4 rectangle in the top right. - - The other two rectangles are overlapping in the bottom left corner. - - Therefore, the final answer is 3. +Your output should be a single number, representing the total count of rectangles. Now, it's your turn. How many rectangles do you see in the grid below? {puzzle} diff --git a/reasoning_gym/games/countdown.py b/reasoning_gym/games/countdown.py index 88ad913d..751c67d7 100644 --- a/reasoning_gym/games/countdown.py +++ b/reasoning_gym/games/countdown.py @@ -9,6 +9,7 @@ from ..factory import ProceduralDataset, register_dataset QUESTION_FORMAT_TEMPLATE = """{question} + Final answer format instructions: 1. Provide your solution as a arithmetic expression (no '=' sign). 2. Do not include the target number in the expression. diff --git a/reasoning_gym/games/emoji_mystery.py b/reasoning_gym/games/emoji_mystery.py index 59b20b16..27f48d87 100644 --- a/reasoning_gym/games/emoji_mystery.py +++ b/reasoning_gym/games/emoji_mystery.py @@ -118,8 +118,8 @@ "🤍", ] - -hint_function = """ +# Keep the hint function in a separate variable to control the visibility of the hint +hint_function = """Here is a hint: ```python def variance_selector_to_byte(variation_selector): variation_selector_codepoint = ord(variation_selector) @@ -129,6 +129,7 @@ def variance_selector_to_byte(variation_selector): return variation_selector_codepoint - 0xE0100 + 16 else: return None + def decode(encoded_sentence): decoded_bytes = [] variation_selectors_part = encoded_sentence[1:] @@ -141,14 +142,14 @@ def decode(encoded_sentence): """ -QUESTION_TEMPLATE = "\n".join( - [ - "The following emoji is encoded with a sentence.", - "Decode the following sentence from the emoji: {sentence}", - "Here is a hint: {hint_function}", - "Return the secret sentence as your final answer.", - ] -) +QUESTION_TEMPLATE = """The following emoji is encoded with a sentence. + +Decode the following sentence from the emoji: {sentence} + +{hint_function} + +Return the secret sentence as your final answer. +""" @dataclass diff --git a/reasoning_gym/games/mahjong.py b/reasoning_gym/games/mahjong.py index 7816320b..77071057 100644 --- a/reasoning_gym/games/mahjong.py +++ b/reasoning_gym/games/mahjong.py @@ -19,13 +19,7 @@ 6. "Peng" takes precedence over "Chi". 7. The card that is removed does not affect the result determination of the current round. -Example: -- Input: Given the initial cards ABBCCDDEEFFGH, what is the result at the end of performing the following rounds of operations: -Round 1: Add a B card and remove an E card. -Round 2: Add a C card and remove an H card. -Round 3: Add an E card and remove a D card. -Round 4: Add a D card and remove an F card. -- Output: Chi +Your output should be one of the following: "Peng", "Chi", or "Pass" (without quotes). Now, given the initial cards {cards}, what is the result at the end of performing the following rounds of operations: {operations} diff --git a/reasoning_gym/games/n_queens.py b/reasoning_gym/games/n_queens.py index 61f6ea66..8fcecfd4 100644 --- a/reasoning_gym/games/n_queens.py +++ b/reasoning_gym/games/n_queens.py @@ -20,20 +20,7 @@ You can place a queen by replacing an underscore (_) with a Q. -Example: -- Input: Given the below board of size 4 x 4 your job is to place 2 queen(s) on the board such that no two queens attack each other. -_ Q _ _ -_ _ _ _ -_ _ _ _ -_ _ Q _ -- Output: -_ Q _ _ -_ _ _ Q -Q _ _ _ -_ _ Q _ -- Explanation - - None of the queens attack each other vertically, horizontally, or diagonally. - - The added queens are marked with Q at the positions (1, 3) and (2, 0). +Your output should be also a board in the same format as the input, with queens placed on the board by replacing underscores with the letter Q. Given the below board of size {n} x {n} your job is to place {num_removed} queen(s) on the board such that no two queens attack each other. {puzzle} diff --git a/reasoning_gym/games/tower_of_hanoi.py b/reasoning_gym/games/tower_of_hanoi.py index 47afbb05..052d72bf 100644 --- a/reasoning_gym/games/tower_of_hanoi.py +++ b/reasoning_gym/games/tower_of_hanoi.py @@ -13,16 +13,13 @@ - Only one disk can be moved at a time. - A larger disk cannot be placed on top of a smaller disk. - All disks must be on a peg at all times. -Example: -Move disk 1 from Peg 1 to Peg 3 -Move disk 2 from Peg 1 to Peg 2 -Move disk 1 from Peg 3 to Peg 2 Provide the sequence of moves. + Formatting guidelines: -Each instruction should be placed on a single line. -Each line should be formatted as 'Move disk X from Peg Y to Peg Z' -Do not include any other text or formatting. +- Each instruction should be placed on a single line. +- Each line should be formatted as 'Move disk X from Peg Y to Peg Z' +- Do not include any other text or formatting. """ diff --git a/reasoning_gym/geometry/advanced_geometry.py b/reasoning_gym/geometry/advanced_geometry.py index 90c8b463..816401e4 100644 --- a/reasoning_gym/geometry/advanced_geometry.py +++ b/reasoning_gym/geometry/advanced_geometry.py @@ -36,16 +36,12 @@ def validate(self): assert len(self.task_types) > 0, "Must specify at least one task type." -# Join format instructions into a single string -GEOMETRY_FORMAT_INSTRUCTIONS = "\n".join( - [ - "For all geometry problems:", - "1. Give coordinates in the form (x, y)", - "2. Round decimal answers to 3 decimal places", - "3. Use the degree symbol ° for angles", - "4. Return only th angle, coordinates, or radius as your answer.", - ] -) +GEOMETRY_FORMAT_INSTRUCTIONS = """For all geometry problems: +1. Give coordinates in the form (x, y) +2. Round decimal answers to 3 decimal places +3. Use the degree symbol ° for angles +4. Return only the angle, coordinates, or radius as your answer. +""" class AdvancedGeometryDataset(ProceduralDataset): diff --git a/reasoning_gym/graphs/shortest_path.py b/reasoning_gym/graphs/shortest_path.py index 0b6f4def..d915e114 100644 --- a/reasoning_gym/graphs/shortest_path.py +++ b/reasoning_gym/graphs/shortest_path.py @@ -16,23 +16,12 @@ - X: a blocked cell Therefore, you need to find the shortest path from * to #, moving only through open cells. + +You may only move in four directions: up, down, left, and right. + If there is no path from * to #, simply write "infeasible" (without quotes). -Example 1: -- Input: Find the length of the shortest path from * to # in the following grid: - X X X X X - X * O O X - X O X O X - X X X O # -- Output: right right down down right - -Example 2: -- Input: Find the length of the shortest path from * to # in the following grid: - X X X X X - X * O O X - X O X O X - X X X X # -- Output: infeasible +Your output should be a sequence of directions that leads from * to #, e.g. right right down down up left Now, find the length of the shortest path from * to # in the following grid: {grid} diff --git a/reasoning_gym/logic/propositional_logic.py b/reasoning_gym/logic/propositional_logic.py index dec8a5a7..8732bc1b 100644 --- a/reasoning_gym/logic/propositional_logic.py +++ b/reasoning_gym/logic/propositional_logic.py @@ -62,22 +62,21 @@ class Operator(StrEnum): IFF = "↔" -QUESTION_FORMAT = "\n".join( - [ - "The following question is a propositional logic reasoning question.", - "In the question we provide a list of premises", - "The task is to infer a correct conclusion from the premise.", - "FORMAT INSTRUCTIONS:", - "Return the conclusion logic statement, as your final answer.", - "Use the following notation to denote symbols", - "OR = \u2228", - "AND = \u2227", - "IMPLIES = \u2192", - "IFF = \u2194", - "NOT = \u00ac", - "Here is the question:", - ] -) +QUESTION_FORMAT = """The following question is a propositional logic reasoning question. + +In the question we provide a list of premises. The task is to infer a correct conclusion from the premise. + +FORMAT INSTRUCTIONS: +- Return the conclusion logic statement, as your final answer. +- Use the following notation to denote symbols + - OR = \u2228 + - AND = \u2227 + - IMPLIES = \u2192 + - IFF = \u2194 + - NOT = \u00ac + +Here is the question: +""" @dataclass From 68ecdca2bb32bf3c432671b0131c9eb776da8c8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20K=C3=B6pf?= Date: Mon, 3 Mar 2025 21:56:31 +0100 Subject: [PATCH 02/12] add Chain of Draft and direct system prompt styles (#255) --- reasoning_gym/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/reasoning_gym/utils.py b/reasoning_gym/utils.py index fe30e75c..2143961d 100644 --- a/reasoning_gym/utils.py +++ b/reasoning_gym/utils.py @@ -16,6 +16,8 @@ Do not explain your reasoning inside the answer tags, provide only the final answer. When an example is provided, you should strictly follow the format of the output/answer in that example. """, "simple": "You are a helpful assistant that answers questions accurately and concisely. When asked to solve a problem, show your work step by step. Provide your final answer between and tags.", + "direct": "Answer the question directly. Provide your answer between and tags. Do not return any preamble, explanation, or reasoning.", + "chain_of_draft": "Think step by step, but only keep a minimum draft for each thinking step, with 5 words at most. Return the answer at the end of the response enclosed in tags.", } From c0cf237474e710f04da43a9980d84f1e00fa6f62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20K=C3=B6pf?= Date: Mon, 3 Mar 2025 21:57:08 +0100 Subject: [PATCH 03/12] Reduce precision from 28 to 6 in DecimalArithmeticDataset (#256) --- reasoning_gym/arithmetic/decimal_arithmetic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/reasoning_gym/arithmetic/decimal_arithmetic.py b/reasoning_gym/arithmetic/decimal_arithmetic.py index 9d311bbf..bf00c2af 100644 --- a/reasoning_gym/arithmetic/decimal_arithmetic.py +++ b/reasoning_gym/arithmetic/decimal_arithmetic.py @@ -11,9 +11,9 @@ class DecimalArithmeticConfig: """Configuration for decimal arithmetic dataset generation""" - min_num_decimal_places: int = 6 - max_num_decimal_places: int = 6 - precision: int = 28 + min_num_decimal_places: int = 3 + max_num_decimal_places: int = 3 + precision: int = 6 terms: int = 6 seed: Optional[int] = None size: int = 500 From 6770ee3eef9a46e6724ab520248257665e769447 Mon Sep 17 00:00:00 2001 From: joesharratt1229 <118444587+joesharratt1229@users.noreply.github.com> Date: Mon, 3 Mar 2025 21:58:32 +0100 Subject: [PATCH 04/12] updated for config by dataset (#257) * updated for config by dataset * updated read me --- eval/README.md | 17 +++++++++++++++-- eval/generate_config.py | 30 +++++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/eval/README.md b/eval/README.md index f7f89c7c..a52c4789 100644 --- a/eval/README.md +++ b/eval/README.md @@ -126,10 +126,23 @@ Options: - `--size`: Default dataset size (default: 100) - `--seed`: Default dataset seed (default: 42) - `--include-params`: Include all configuration parameters (default: False) +- `--category`: Only include datasets from this category (default: None) -### Running Evaluations +#### Generating Config for a Specific Category + +To generate a configuration file containing only datasets from a specific category: + +```bash +python generate_config.py --category algorithmic --output algorithmic_datasets.yaml --model "anthropic/claude-3.5-sonnet" +``` -To run evaluations: +This will create a configuration file that includes only datasets in the "algorithmic" category. This is useful when you want to focus your evaluation on a specific type of reasoning tasks. + +Example categories include: math, arithmetic, reasoning, algorithmic, etc. The category is automatically extracted from the dataset's module name (e.g., from `reasoning_gym.math.dataset_name`, it extracts "math"). + +You can see all available categories by running the script without the `--category` option, as it will print all categories at the end of execution. + +### Running Evaluations ```bash python eval.py --config configs/your_config.yaml diff --git a/eval/generate_config.py b/eval/generate_config.py index 4ab31eb1..43cff904 100644 --- a/eval/generate_config.py +++ b/eval/generate_config.py @@ -15,6 +15,7 @@ --size SIZE Default dataset size (default: 100) --seed SEED Default dataset seed (default: 42) --include-params Include all configuration parameters (default: False) + --category CATEGORY Only include datasets from this category (default: None) """ import argparse @@ -35,14 +36,27 @@ def extract_category(module_name): return "other" -def generate_config(model, provider, size, seed, include_params): - """Generate configuration with all registered datasets.""" +def generate_config(model, provider, size, seed, include_params, category=None): + """Generate configuration with all registered datasets. + + Args: + model: Model name + provider: Provider name + size: Default dataset size + seed: Default dataset seed + include_params: Whether to include all configuration parameters + category: If specified, only include datasets from this category + """ # Group datasets by category categories = defaultdict(list) for dataset_name, (dataset_cls, config_cls) in DATASETS.items(): # Extract category from module name - category = extract_category(dataset_cls.__module__) + dataset_category = extract_category(dataset_cls.__module__) + + # Skip if a specific category was requested and this doesn't match + if category and dataset_category != category: + continue # Create dataset entry dataset_entry = {"dataset": dataset_name} @@ -62,7 +76,7 @@ def generate_config(model, provider, size, seed, include_params): dataset_entry["params"] = params # Add to appropriate category - categories[category].append(dataset_entry) + categories[dataset_category].append(dataset_entry) # Create configuration structure config = { @@ -90,12 +104,18 @@ def main(): parser.add_argument("--size", type=int, default=100, help="Default dataset size") parser.add_argument("--seed", type=int, default=42, help="Default dataset seed") parser.add_argument("--include-params", action="store_true", help="Include all configuration parameters") + parser.add_argument("--category", help="Only include datasets from this category") args = parser.parse_args() # Generate configuration config = generate_config( - model=args.model, provider=args.provider, size=args.size, seed=args.seed, include_params=args.include_params + model=args.model, + provider=args.provider, + size=args.size, + seed=args.seed, + include_params=args.include_params, + category=args.category, ) # Write to file From 0ba611985059af2b71b544d22f876cb356f96aa7 Mon Sep 17 00:00:00 2001 From: Rich Jones Date: Mon, 3 Mar 2025 22:22:39 +0100 Subject: [PATCH 05/12] Game of Life partial scoring and rule-clarification (#258) * partial scoring and rule clarification * better ql scoring * word seq reverse typos --- reasoning_gym/algorithmic/game_of_life.py | 43 ++++++++++++++++--- .../algorithmic/word_sequence_reversal.py | 2 +- reasoning_gym/graphs/quantum_lock.py | 4 +- tests/test_game_of_life.py | 20 +++++++++ 4 files changed, 60 insertions(+), 9 deletions(-) diff --git a/reasoning_gym/algorithmic/game_of_life.py b/reasoning_gym/algorithmic/game_of_life.py index 7a1647e7..d75f57c0 100644 --- a/reasoning_gym/algorithmic/game_of_life.py +++ b/reasoning_gym/algorithmic/game_of_life.py @@ -32,7 +32,7 @@ class GameOfLifeDataset(ProceduralDataset): def __init__(self, config: GameOfLifeConfig): self._prompt_templates = [ - "What will this Game of Life board look like after {simulation_steps} steps of simulation? Reply as array of arrays representing rows in the grid from top to bottom in JSON format. (An empty 3x3 grid would look like this: [[0,0,0],[0,0,0],[0,0,0]])\n\n{board}." + "What will this Game of Life board look like after {simulation_steps} steps of simulation? Assume a Moore neighborhood and wrapping topology. Reply as array of arrays representing rows in the grid from top to bottom in JSON format. (An empty 3x3 grid would look like this: [[0,0,0],[0,0,0],[0,0,0]])\n\n{board}." ] super().__init__(config=config, seed=config.seed, size=config.size) @@ -105,13 +105,42 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: try: ans_arr = json.loads(answer) correct_arr = json.loads(entry["answer"]) - - if correct_arr != ans_arr: - return 0.01 - else: - return 1.0 # Yay - except Exception as e: + except Exception: return 0.01 + total_cells = 0 + correct_cells = 0 + + # Determine if the array is 2D (i.e. a list of lists) + is_2d = correct_arr and isinstance(correct_arr[0], list) + + if is_2d: + # Iterate over rows and columns of the expected grid. + for i, expected_row in enumerate(correct_arr): + for j, expected_value in enumerate(expected_row): + total_cells += 1 + try: + if ans_arr[i][j] == expected_value: + correct_cells += 1 + except (IndexError, TypeError): + # Either the row or the cell is missing, treat as incorrect. + pass + else: + # 1D array case. + for i, expected_value in enumerate(correct_arr): + total_cells += 1 + try: + if ans_arr[i] == expected_value: + correct_cells += 1 + except IndexError: + pass + + # If for some reason there are no cells, return 0.0. + if total_cells == 0: + return 0.0 + + # Each cell contributes equally. + return correct_cells / total_cells + register_dataset("game_of_life", GameOfLifeDataset, GameOfLifeConfig) diff --git a/reasoning_gym/algorithmic/word_sequence_reversal.py b/reasoning_gym/algorithmic/word_sequence_reversal.py index 67f97ca1..84e297aa 100644 --- a/reasoning_gym/algorithmic/word_sequence_reversal.py +++ b/reasoning_gym/algorithmic/word_sequence_reversal.py @@ -8,7 +8,7 @@ from ..data import read_data_file from ..factory import ProceduralDataset, register_dataset -QUESTION_TEMPLATE = """Solve the following problem. Provide you answer as a comma-separated list of word with a space the comma. Reverse this list of words: {question}""" +QUESTION_TEMPLATE = """Solve the following problem. Provide you answer as a comma-separated list of words with a space after the comma. Reverse this list of words: {question}""" @dataclass diff --git a/reasoning_gym/graphs/quantum_lock.py b/reasoning_gym/graphs/quantum_lock.py index 4d810145..c32085f2 100644 --- a/reasoning_gym/graphs/quantum_lock.py +++ b/reasoning_gym/graphs/quantum_lock.py @@ -192,7 +192,9 @@ def normalize_seq(seq): # Partial credit for reaching target (optional) final_state = self.simulate_sequence(entry["metadata"], user_sequence) if final_state == entry["metadata"]["target_value"]: - return 0.5 # Alternative scoring option + if len(user_sequence) == len(target_sequence): + return 1.0 # Different answer, but qually correct + return 0.5 # Alternative scoring - you're correct, but not optimal return 0.1 diff --git a/tests/test_game_of_life.py b/tests/test_game_of_life.py index abdc17ae..924a12de 100644 --- a/tests/test_game_of_life.py +++ b/tests/test_game_of_life.py @@ -1,3 +1,5 @@ +import json + import pytest from reasoning_gym.algorithmic.game_of_life import GameOfLifeConfig, GameOfLifeDataset @@ -50,6 +52,24 @@ def test_game_of_life_basic_properties(): assert dataset.score_answer(answer=None, entry=item) == 0.0 assert dataset.score_answer(answer="invalid json", entry=item) == 0.01 + config = GameOfLifeConfig(seed=43, size=1, grid_size_x=3, grid_size_y=3, filled_cells=1, simulation_steps=1) + dataset = GameOfLifeDataset(config) + + for item in dataset: + assert isinstance(item, dict) + assert "question" in item + assert "answer" in item + assert "metadata" in item + + ja = json.loads(item["answer"]) + ja[0][0] = 1 + ja[0][1] = 1 + ja[0][2] = 1 + jas = json.dumps(ja) + + # Test the scoring + assert 0.1 < dataset.score_answer(answer=jas, entry=item) < 1.0 + def test_game_of_life_iteration(): """Test that iteration respects dataset size""" From 3672b231f1f3dfb053b344713d39f1d7f98b7486 Mon Sep 17 00:00:00 2001 From: vncntt <85441325+vncntt@users.noreply.github.com> Date: Tue, 4 Mar 2025 00:45:36 -0800 Subject: [PATCH 06/12] should exit if API key isn't defined (#259) * should exit if open-router and no api key --- eval/eval.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eval/eval.py b/eval/eval.py index 494d92d0..6978afc9 100755 --- a/eval/eval.py +++ b/eval/eval.py @@ -500,7 +500,7 @@ async def main_async(): print("Warning: OPENROUTER_API_KEY environment variable is not set") print("Please set it using: export OPENROUTER_API_KEY=your-api-key") print("Or provide it directly with --api-key") - print("Continuing without API key...") + return 1 # Load configuration config_path = args.config From 061282e373720f239e8f588518b7b3a844cadb85 Mon Sep 17 00:00:00 2001 From: joesharratt1229 <118444587+joesharratt1229@users.noreply.github.com> Date: Tue, 4 Mar 2025 21:37:57 +0100 Subject: [PATCH 07/12] implemented family_relationships score ans (#260) --- reasoning_gym/graphs/family_relationships.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/reasoning_gym/graphs/family_relationships.py b/reasoning_gym/graphs/family_relationships.py index 8011e375..973de976 100644 --- a/reasoning_gym/graphs/family_relationships.py +++ b/reasoning_gym/graphs/family_relationships.py @@ -2,7 +2,7 @@ from dataclasses import dataclass, field from enum import StrEnum from itertools import count -from typing import Optional +from typing import Any, Optional from ..factory import ProceduralDataset, register_dataset @@ -356,5 +356,19 @@ def _generate_story(self, family: set[Person]) -> str: return " ".join(story_parts) + def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: + reward = 0.0 + if answer is not None: + try: + answer_formatted = answer.strip().lower() + solved = answer_formatted == entry["answer"].strip().lower() + if solved: + reward = 1.0 + else: + reward = 0.01 + except: + reward = 0.01 + return reward + register_dataset("family_relationships", FamilyRelationshipsDataset, FamilyRelationshipsConfig) From 5d7fbac0ad8f926894d4397bddaaf1c8d412cfc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20K=C3=B6pf?= Date: Tue, 4 Mar 2025 21:55:09 +0100 Subject: [PATCH 08/12] Minor question template & score_answer improvements (#261) * math prompt improvements * ignore brackets in complex_arithmetic results * improve additional instruction in prompt of polynomial_equations * more strict tests for score_answer in polynomial_equations * simplify special reward handling * fix test_intermediate_integration * fix sokoban dataset * add common dataset score_answer consistency test --- reasoning_gym/algebra/complex_arithmetic.py | 4 ++ .../algebra/intermediate_integration.py | 15 +++--- reasoning_gym/algebra/polynomial_equations.py | 22 +++++---- .../algebra/polynomial_multiplication.py | 18 +++---- reasoning_gym/algebra/simple_integration.py | 12 ++--- reasoning_gym/algorithmic/ab.py | 7 +-- reasoning_gym/algorithmic/binary_matrix.py | 6 +-- reasoning_gym/algorithmic/cryptarithm.py | 2 +- reasoning_gym/algorithmic/game_of_life.py | 2 +- reasoning_gym/algorithmic/graph_color.py | 9 ++-- reasoning_gym/algorithmic/group_anagrams.py | 2 +- reasoning_gym/algorithmic/jugs.py | 4 +- reasoning_gym/algorithmic/letter_jumble.py | 18 ++++--- .../algorithmic/manipulate_matrix.py | 2 - .../algorithmic/palindrome_generation.py | 4 +- .../algorithmic/palindrome_partitioning.py | 3 +- reasoning_gym/algorithmic/pool_matrix.py | 4 +- reasoning_gym/algorithmic/ransom_note.py | 12 ++--- .../algorithmic/sentence_reordering.py | 2 +- reasoning_gym/algorithmic/spell_backward.py | 4 +- reasoning_gym/algorithmic/spiral_matrix.py | 8 ++-- reasoning_gym/algorithmic/string_insertion.py | 8 ++-- reasoning_gym/algorithmic/word_ladder.py | 12 ++--- reasoning_gym/algorithmic/word_sorting.py | 2 - reasoning_gym/arc/arc_agi.py | 2 +- reasoning_gym/arc/rearc.py | 2 +- .../arithmetic/bitwise_arithmetic.py | 20 ++++---- .../arithmetic/calendar_arithmetic.py | 5 +- .../arithmetic/decimal_arithmetic.py | 10 ++-- reasoning_gym/arithmetic/decimal_chain_sum.py | 19 +++++--- reasoning_gym/arithmetic/dice.py | 11 ++--- reasoning_gym/arithmetic/number_format.py | 5 +- reasoning_gym/arithmetic/power_function.py | 6 +-- reasoning_gym/arithmetic/time_intervals.py | 2 +- reasoning_gym/code/bf.py | 25 +++++----- reasoning_gym/cognition/figlet_fonts.py | 2 +- reasoning_gym/cognition/needle_haystack.py | 18 ++++--- reasoning_gym/cognition/rectangle_count.py | 10 ++-- reasoning_gym/dataset.py | 3 -- .../games/contrib/sokoban/levels/lvl0.dat | 10 ---- .../games/contrib/sokoban/levels/lvl1.dat | 5 -- .../games/contrib/sokoban/levels/lvl2.dat | 6 --- .../games/contrib/sokoban/levels/lvl3.dat | 7 --- .../games/contrib/sokoban/levels/lvl4.dat | 7 --- .../games/contrib/sokoban/levels/lvl5.dat | 7 --- .../games/contrib/sokoban/levels/lvl6.dat | 9 ---- .../games/contrib/sokoban/levels/lvl7.dat | 6 --- .../games/contrib/sokoban/src/astar.py | 11 +++-- .../games/contrib/sokoban/src/game.py | 13 +++-- .../games/contrib/sokoban/src/generator.py | 38 +++++++++------ reasoning_gym/games/futoshiki.py | 2 +- reasoning_gym/games/knight_swap.py | 27 +++++------ reasoning_gym/games/mini_sudoku.py | 2 +- reasoning_gym/games/n_queens.py | 6 +-- reasoning_gym/games/rush_hour.py | 2 +- reasoning_gym/games/sokoban.py | 40 +++++++++++----- reasoning_gym/games/sudoku.py | 2 +- reasoning_gym/games/tower_of_hanoi.py | 24 +++------- reasoning_gym/graphs/family_relationships.py | 10 ++-- reasoning_gym/graphs/quantum_lock.py | 16 ++----- reasoning_gym/graphs/shortest_path.py | 6 +-- reasoning_gym/logic/circuit_logic.py | 18 ++++--- reasoning_gym/logic/knights_knaves.py | 8 ++-- reasoning_gym/logic/propositional_logic.py | 6 +-- reasoning_gym/logic/self_reference.py | 14 ++---- reasoning_gym/logic/zebra_puzzles.py | 10 ++-- reasoning_gym/utils.py | 1 - tests/test_ab.py | 2 +- tests/test_arc_1d.py | 2 +- tests/test_arc_agi.py | 2 +- tests/test_bf.py | 2 +- tests/test_binary_matrix.py | 2 +- tests/test_bitwise_arithmetic.py | 2 +- tests/test_complex_arithmetic.py | 1 + tests/test_dataset.py | 2 +- tests/test_dataset_common.py | 17 +++++++ tests/test_decimal_chain_sum.py | 8 ++-- tests/test_game_of_life.py | 2 +- tests/test_intermediate_integration.py | 14 +++--- tests/test_knight_swap.py | 4 +- tests/test_knights_knaves.py | 3 +- tests/test_manipulate_matrix.py | 2 +- tests/test_n_queens.py | 2 +- tests/test_needle_haystack.py | 2 +- tests/test_number_format.py | 2 +- tests/test_palindrome.py | 4 +- tests/test_palindrome_partitioning.py | 12 ++--- tests/test_polynomial_equations.py | 10 ++-- tests/test_polynomial_multiplication.py | 48 ++----------------- tests/test_pool_matrix.py | 2 +- tests/test_power_function.py | 2 +- tests/test_products.py | 2 +- tests/test_propositional_logic.py | 2 +- tests/test_quantum_lock.py | 9 ++-- tests/test_ransom_note.py | 2 +- tests/test_rearc.py | 2 +- tests/test_self_reference.py | 12 ++--- tests/test_shortest_path.py | 2 +- tests/test_simple_integration.py | 14 +++--- tests/test_sokoban.py | 18 +++++-- tests/test_spiral_matrix.py | 4 +- tests/test_string_insertion.py | 2 +- tests/test_tower_of_hanoi.py | 21 ++++---- tests/test_utils.py | 2 +- tests/test_word_ladder.py | 12 ++--- tests/test_word_sorting.py | 4 +- 106 files changed, 394 insertions(+), 498 deletions(-) delete mode 100644 reasoning_gym/games/contrib/sokoban/levels/lvl0.dat delete mode 100644 reasoning_gym/games/contrib/sokoban/levels/lvl1.dat delete mode 100644 reasoning_gym/games/contrib/sokoban/levels/lvl2.dat delete mode 100644 reasoning_gym/games/contrib/sokoban/levels/lvl3.dat delete mode 100644 reasoning_gym/games/contrib/sokoban/levels/lvl4.dat delete mode 100644 reasoning_gym/games/contrib/sokoban/levels/lvl5.dat delete mode 100644 reasoning_gym/games/contrib/sokoban/levels/lvl6.dat delete mode 100644 reasoning_gym/games/contrib/sokoban/levels/lvl7.dat create mode 100644 tests/test_dataset_common.py diff --git a/reasoning_gym/algebra/complex_arithmetic.py b/reasoning_gym/algebra/complex_arithmetic.py index 30ffe972..903cdb99 100644 --- a/reasoning_gym/algebra/complex_arithmetic.py +++ b/reasoning_gym/algebra/complex_arithmetic.py @@ -103,6 +103,10 @@ def parse_string_to_complex(answer: str) -> complex: # Normalize the answer string by removing spaces and converting to lowercase answer = answer.replace(" ", "").lower() + # remove brackets + while len(answer) > 1 and answer[0] == "(" and answer[-1] == ")": + answer = answer[1:-1] + # Convert mathematical notation 'i' to Python's 'j' for complex numbers answer = answer.replace("i", "j") diff --git a/reasoning_gym/algebra/intermediate_integration.py b/reasoning_gym/algebra/intermediate_integration.py index c967c7be..de664450 100644 --- a/reasoning_gym/algebra/intermediate_integration.py +++ b/reasoning_gym/algebra/intermediate_integration.py @@ -77,9 +77,10 @@ def __init__(self, config: IntermediateIntegrationConfig): "Evaluate the indefinite integral: ∫ {integrand} dx", ] self.added_instruction = """ -In addition, when doing calculation, use the following instructions together with your mathematical ingenuity to solve the integral problems -## 1. Use ** instead ^ to represent powers. For example 7*X**2 instead of 7*X^2. -## 2. Always use * when doing all sorts of multiplcation in your reasoning steps. For example Use [-3*X**3*sin(X) - 9*X**2*cos(X) + 18*X*sin(X) + 18*cos(X) + C] instead of [-3x3sin(x) - 9x2cos(x) + 18xsin(x) + 18cos(x) + C]. +When performing calculations, please follow these guidelines: +1. Use ** instead of ^ to represent exponents. For example, write 7*X**2 instead of 7*X^2. +2. Always include the * symbol for all multiplication operations in your reasoning steps. For example, write `-3*X**3*sin(X) - 9*X**2*cos(X) + 18*X*sin(X) + 18*cos(X) + C` instead of `-3x3sin(x) - 9x2cos(x) + 18xsin(x) + 18cos(x) + C`. +3. Use `exp(x)` or `E**(x)` for the exponential function (i.e. use capital E for Euler's number). """ def _get_outer_constant(self, rng: random.Random) -> int: @@ -245,7 +246,7 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: """Determine if the solution provided solves the problem""" reward = 0.0 metadata = entry["metadata"] - if answer is not None: + if isinstance(answer, str): try: var = metadata["variable"] x = sympy.Symbol(var) @@ -258,12 +259,8 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: # Check mathematical equivalence through simplification if sympy.simplify(derivative - integrand) == 0: reward = 1.0 - elif answer.strip(): - reward = 0.05 - else: - reward = 0.01 except: - reward = 0.01 + reward = 0.0 return reward diff --git a/reasoning_gym/algebra/polynomial_equations.py b/reasoning_gym/algebra/polynomial_equations.py index c069c32f..ac054427 100644 --- a/reasoning_gym/algebra/polynomial_equations.py +++ b/reasoning_gym/algebra/polynomial_equations.py @@ -27,8 +27,9 @@ class PolynomialEquationsConfig: seed: Optional[int] = None size: int = 500 # reward function hyperparameters - penalty_missing_factor = 0.1 - penalty_extra_factor = 0.05 + penalty_missing_factor = 0.5 + penalty_extra_factor = 0.5 + exp_distance_factor = -10.0 def validate(self) -> None: """Validate configuration parameters.""" @@ -62,12 +63,15 @@ def __init__(self, config: PolynomialEquationsConfig): "Solve the polynomial equation for real {variable}:\n{polynomial_expanded} = 0", ] self.added_instruction = """ -In solving the equations, please abide by the following instruction: -## 1. All answers should be comma-separated. For example "-0.3773, 0.4005" etc. -## 2. In cases where your answer is b = 2 + sqrt(4560) / 172 and b = 2 - sqrt(4560) / 172. Since b can be 2 numbers, resolve your answer like this instead, "-0.3773, 0.4005". -## 3. If there are no real values of i that satisfy the equation, report your answer as empty string, "". -## 4. If there are 2 answers, resolve the answers as comma-separated floats of 2 numbers, if 3 answers, make it comma-separated floats of 3 numbers. -## 5. Resolve all numbers as floats in the string of comma-separated numbers. Round the floats higher than 4 decimal place(d.p) down to 4 d.p. +In solving equations, please follow these instructions: +1. Provide all answers as comma-separated decimal values. For example: "-0.3773, 0.4005" +2. For solutions that can be expressed in exact form (like "u = 2 + sqrt(4560)/172" and "u = 2 - sqrt(4560)/172"), convert them to decimal form in your final answer. +3. If there are no real values that satisfy the equation, report your answer as an empty string: "" +4. Format your answer based on the number of solutions: + - For 1 solution: a single decimal number + - For 2 solutions: two comma-separated decimal numbers + - For 3 or more solutions: all values as comma-separated decimal numbers +5. Round all decimal values to 4 decimal places (rounding down when the 5th decimal place is 5 or greater). """ super().__init__(config=config, seed=config.seed, size=config.size) @@ -238,7 +242,7 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: # Remove matched oracle solution oracle_solutions.pop(matched_distance_index) # Exponential decay reward - total_reward += math.exp(-matched_distance) + total_reward += math.exp(matched_distance * self.config.exp_distance_factor) else: # Extra predicted solution extra_solutions += 1 diff --git a/reasoning_gym/algebra/polynomial_multiplication.py b/reasoning_gym/algebra/polynomial_multiplication.py index 09e2530a..9a0d51dd 100644 --- a/reasoning_gym/algebra/polynomial_multiplication.py +++ b/reasoning_gym/algebra/polynomial_multiplication.py @@ -69,9 +69,9 @@ def __init__(self, config: PolynomialMultiplicationConfig): "Calculate the following: {polynomial_expr}", ] self.added_instruction = """ -In addition, When doing calculation, Use the following instructions together with your mathematical ingenuity to solve the integral problems -## 1. Use ** instead ^ to represent powers. For example 7*X**2 instead of 7*X^2. -## 2. Always use * when doing all sorts of multiplcation in your reasoning steps and even in reporting answers. +When performing calculations, please follow these guidelines: +1. Use ** instead of ^ to represent exponents. For example, write 7*X**2 instead of 7*X^2. +2. Always include the * symbol for all multiplication operations in your reasoning steps. For example, write `-3*X**3*sin(X) - 9*X**2*cos(X) + 18*X*sin(X) + 18*cos(X) + C` instead of `-3x3sin(x) - 9x2cos(x) + 18xsin(x) + 18cos(x) + C`. """ super().__init__(config=config, seed=config.seed, size=config.size) @@ -106,10 +106,9 @@ def __getitem__(self, idx: int) -> dict: return { "question": question, - "answer": product, + "answer": str(product), "metadata": { "polynomial_expr": str(polynomial_expr), - "result": str(product), "variables": list(product.free_symbols), }, } @@ -147,21 +146,16 @@ def _generate_polynomial(self, rng: random.Random, monomials: Optional[list]): def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: reward = 0.0 - metadata = entry["metadata"] if answer is not None: try: predicted_poly = sp.parse_expr(answer) - target_poly = sp.parse_expr(metadata["result"]) + target_poly = sp.parse_expr(entry["answer"]) # Check if the difference simplifies to zero (i.e. they are equivalent). if predicted_poly == target_poly: reward = 1.0 - elif answer.strip(): - reward = 0.05 - else: - reward = 0.01 except Exception: - reward = 0.01 + reward = 0.0 return reward diff --git a/reasoning_gym/algebra/simple_integration.py b/reasoning_gym/algebra/simple_integration.py index a45dc3ff..321a7a9d 100644 --- a/reasoning_gym/algebra/simple_integration.py +++ b/reasoning_gym/algebra/simple_integration.py @@ -42,9 +42,9 @@ def __init__(self, config: SimpleIntegrationConfig): "Evaluate the indefinite integral: ∫ {integrand} dx", ] self.added_instruction = """ -In addition, When doing calculation, Use the following instructions together with your mathematical ingenuity to solve the integral problems -## 1. Use ** instead ^ to represent powers. For example 7*X**2 instead of 7*X^2. -## 2. Always use * when doing all sorts of multiplcation in your reasoning steps. For example Use [-3*X**3*sin(X) - 9*X**2*cos(X) + 18*X*sin(X) + 18*cos(X) + C] instead of [-3x3sin(x) - 9x2cos(x) + 18xsin(x) + 18cos(x) + C]. +When performing calculations, please follow these guidelines: +1. Use ** instead of ^ to represent exponents. For example, write 7*X**2 instead of 7*X^2. +2. Always include the * symbol for all multiplication operations in your reasoning steps. For example, write `-3*X**3*sin(X) - 9*X**2*cos(X) + 18*X*sin(X) + 18*cos(X) + C` instead of `-3x3sin(x) - 9x2cos(x) + 18xsin(x) + 18cos(x) + C`. """ super().__init__(config=config, seed=config.seed, size=config.size) @@ -103,12 +103,8 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: # Check mathematical equivalence through simplification if sympy.simplify(derivative - integrand) == 0: reward = 1.0 - elif answer.strip(): - reward = 0.05 - else: - reward = 0.01 except: - reward = 0.01 + reward = 0.0 return reward diff --git a/reasoning_gym/algorithmic/ab.py b/reasoning_gym/algorithmic/ab.py index 9ec679af..3e251d31 100644 --- a/reasoning_gym/algorithmic/ab.py +++ b/reasoning_gym/algorithmic/ab.py @@ -130,12 +130,9 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: float: The computed score between 0.0 and 1.0. """ - if answer == None: - return 0.0 - if answer != entry["answer"]: - return 0.01 - else: + if answer == entry["answer"]: return 1.0 # Yay + return 0.0 # Register the dataset diff --git a/reasoning_gym/algorithmic/binary_matrix.py b/reasoning_gym/algorithmic/binary_matrix.py index 4584a7fb..772cde97 100644 --- a/reasoning_gym/algorithmic/binary_matrix.py +++ b/reasoning_gym/algorithmic/binary_matrix.py @@ -108,9 +108,9 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: # check if answer is python list of lists answer = self._matrix_to_str(eval(answer)) if answer == oracle_answer: - return 0.5 - except Exception as e: - return 0.01 + return 0.1 + except Exception: + return 0.0 return 0.0 def __getitem__(self, idx: int) -> dict: diff --git a/reasoning_gym/algorithmic/cryptarithm.py b/reasoning_gym/algorithmic/cryptarithm.py index f0946278..fe62b743 100644 --- a/reasoning_gym/algorithmic/cryptarithm.py +++ b/reasoning_gym/algorithmic/cryptarithm.py @@ -200,7 +200,7 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: Returns: float: The computed score between 0.0 and 1.0. """ - if not answer: + if not isinstance(answer, str): return 0.0 correct_mapping = {} diff --git a/reasoning_gym/algorithmic/game_of_life.py b/reasoning_gym/algorithmic/game_of_life.py index d75f57c0..83c391e7 100644 --- a/reasoning_gym/algorithmic/game_of_life.py +++ b/reasoning_gym/algorithmic/game_of_life.py @@ -106,7 +106,7 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: ans_arr = json.loads(answer) correct_arr = json.loads(entry["answer"]) except Exception: - return 0.01 + return 0.0 total_cells = 0 correct_cells = 0 diff --git a/reasoning_gym/algorithmic/graph_color.py b/reasoning_gym/algorithmic/graph_color.py index 0e469bc0..fa34b63c 100644 --- a/reasoning_gym/algorithmic/graph_color.py +++ b/reasoning_gym/algorithmic/graph_color.py @@ -228,12 +228,13 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: try: danswer = json.loads(answer) solved, failure = verify_graph_coloring_solution(entry["metadata"]["puzzle"], danswer) - if not solved: - return 0.01 # json was parsable but solution incorrect - else: + if solved: return 1.0 # Yay + else: + return 0.01 # json parsable except Exception: - return 0.0 + pass + return 0.0 register_dataset("graph_color", GraphColorDataset, GraphColorConfig) diff --git a/reasoning_gym/algorithmic/group_anagrams.py b/reasoning_gym/algorithmic/group_anagrams.py index b6630ac0..6c17eae5 100644 --- a/reasoning_gym/algorithmic/group_anagrams.py +++ b/reasoning_gym/algorithmic/group_anagrams.py @@ -95,7 +95,7 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: if answer_str == oracle_str: reward = 1.0 else: - reward = 0.01 + reward = 0.01 # json parsable except Exception: reward = 0.0 return reward diff --git a/reasoning_gym/algorithmic/jugs.py b/reasoning_gym/algorithmic/jugs.py index 134ac068..0dc7210c 100644 --- a/reasoning_gym/algorithmic/jugs.py +++ b/reasoning_gym/algorithmic/jugs.py @@ -303,11 +303,11 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: danswer = json.loads(answer) valid, _ = verify_solution(entry["metadata"]["puzzle"], danswer) if not valid: - return 0.01 + return 0.01 # json parsable else: return 1.0 # Yay except Exception as e: - return 0.01 + return 0.0 register_dataset("jugs", JugsDataset, JugsConfig) diff --git a/reasoning_gym/algorithmic/letter_jumble.py b/reasoning_gym/algorithmic/letter_jumble.py index 5917e55c..2cb3fc08 100644 --- a/reasoning_gym/algorithmic/letter_jumble.py +++ b/reasoning_gym/algorithmic/letter_jumble.py @@ -116,7 +116,7 @@ def partial(self, expected_answer, model_answer): # Each word in the expected answer is worth an equal fraction of 1.0 total_words = len(expected_words) - score_per_word = 1.0 / total_words if total_words else 0 + score_per_word = 1.0 / total_words if total_words > 0 else 0 # Calculate scores word by word scores = [] @@ -142,18 +142,16 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: float: The computed score between 0.0 and 1.0. """ - if not answer: + if not isinstance(answer, str): return 0.0 oracle_answer = entry["answer"].strip().lower() - if answer: - answer = answer.strip().lower() - if answer == oracle_answer: - return 1.0 # Perfect score! - else: - partial_score = self.partial(oracle_answer, answer) - return partial_score - return 0.01 + answer = answer.strip().lower() + if answer == oracle_answer: + return 1.0 # Perfect score! + else: + partial_score = self.partial(oracle_answer, answer) + return partial_score register_dataset("letter_jumble", LetterJumbleDataset, LetterJumbleConfig) diff --git a/reasoning_gym/algorithmic/manipulate_matrix.py b/reasoning_gym/algorithmic/manipulate_matrix.py index ab0bf592..ec964094 100644 --- a/reasoning_gym/algorithmic/manipulate_matrix.py +++ b/reasoning_gym/algorithmic/manipulate_matrix.py @@ -144,8 +144,6 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: if oracle_answer in answer: return len(oracle_answer) / len(answer) - else: - return 0.01 return 0.0 diff --git a/reasoning_gym/algorithmic/palindrome_generation.py b/reasoning_gym/algorithmic/palindrome_generation.py index 2f7b5fb5..05e566ed 100644 --- a/reasoning_gym/algorithmic/palindrome_generation.py +++ b/reasoning_gym/algorithmic/palindrome_generation.py @@ -92,14 +92,14 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: - Correct answer (palindrome with only correct letters in the correct quantities) gives 1.0 - An answer that is a palindrome, but not with the same letters as provided, gives 0.05 - An answer that is a string, but not a palindrome gives 0.02 - - An empty string gives 0.01. + - An empty string gives 0.0 - None gives 0.0. """ if answer is None or not isinstance(answer, str): return 0.0 # No answer given if answer == "": - return 0.01 + return 0.0 metadata = entry["metadata"] answer = answer.strip().lower() diff --git a/reasoning_gym/algorithmic/palindrome_partitioning.py b/reasoning_gym/algorithmic/palindrome_partitioning.py index 8a0c07b3..067a19e7 100644 --- a/reasoning_gym/algorithmic/palindrome_partitioning.py +++ b/reasoning_gym/algorithmic/palindrome_partitioning.py @@ -95,9 +95,8 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: oracle = self.to_set_of_tuples(entry["metadata"]["solution"]) if answer == oracle: return 1.0 - return 0.01 except Exception: - return 0.0 + pass return 0.0 def _generate_palindrome_letters(self, rng: Random, length: int) -> list[str]: diff --git a/reasoning_gym/algorithmic/pool_matrix.py b/reasoning_gym/algorithmic/pool_matrix.py index bf839ad4..e22f9a92 100644 --- a/reasoning_gym/algorithmic/pool_matrix.py +++ b/reasoning_gym/algorithmic/pool_matrix.py @@ -80,7 +80,7 @@ def _average_pool(self, matrix: np.ndarray, pool_size: int) -> np.ndarray: def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: """Score the answer based on the metadata""" - if not answer: + if not isinstance(answer, str): return 0.0 reward = 0.0 @@ -91,8 +91,6 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: reward = 1.0 elif oracle_answer.shape == answer.shape: reward = 0.1 - else: - reward = 0.01 except Exception: pass return reward diff --git a/reasoning_gym/algorithmic/ransom_note.py b/reasoning_gym/algorithmic/ransom_note.py index 2a1826a0..cf163467 100644 --- a/reasoning_gym/algorithmic/ransom_note.py +++ b/reasoning_gym/algorithmic/ransom_note.py @@ -108,14 +108,12 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: float: The computed score between 0.0 and 1.0. """ - if answer == None: - return 0.0 + if isinstance(answer, str): + s_answer = answer.strip() + if s_answer == str(entry["answer"]): + return 1.0 - s_answer = answer.strip() - if not s_answer == str(entry["answer"]): - return 0.01 - else: - return 1.0 + return 0.0 register_dataset("ransom_note", RansomNoteDataset, RansomNoteConfig) diff --git a/reasoning_gym/algorithmic/sentence_reordering.py b/reasoning_gym/algorithmic/sentence_reordering.py index 9caae762..0cfbaaaf 100644 --- a/reasoning_gym/algorithmic/sentence_reordering.py +++ b/reasoning_gym/algorithmic/sentence_reordering.py @@ -110,7 +110,7 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: else: reward = 0.05 except: - reward = 0.01 + reward = 0.0 return reward diff --git a/reasoning_gym/algorithmic/spell_backward.py b/reasoning_gym/algorithmic/spell_backward.py index 57825d84..bf33441b 100644 --- a/reasoning_gym/algorithmic/spell_backward.py +++ b/reasoning_gym/algorithmic/spell_backward.py @@ -52,14 +52,14 @@ def __getitem__(self, idx: int) -> dict: def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: reward = 0.0 expected_answer = entry["answer"] - if answer is not None: + if isinstance(answer, str): try: if expected_answer.lower() == answer.lower(): reward = 1.0 else: reward = 0.05 except: - reward = 0.01 + reward = 0.0 return reward diff --git a/reasoning_gym/algorithmic/spiral_matrix.py b/reasoning_gym/algorithmic/spiral_matrix.py index 63492a37..f895c628 100644 --- a/reasoning_gym/algorithmic/spiral_matrix.py +++ b/reasoning_gym/algorithmic/spiral_matrix.py @@ -126,11 +126,9 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: try: answer = " ".join(str(item) for item in eval(answer)) if answer == oracle_answer: - return 0.5 - else: - return 0.01 - except Exception as e: - return 0.01 + return 0.1 + except Exception: + pass return 0.0 diff --git a/reasoning_gym/algorithmic/string_insertion.py b/reasoning_gym/algorithmic/string_insertion.py index 1d597364..0dafe8f4 100644 --- a/reasoning_gym/algorithmic/string_insertion.py +++ b/reasoning_gym/algorithmic/string_insertion.py @@ -75,7 +75,7 @@ def _get_answer(self, string: str) -> str: def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: """Overwrite this method in derived classes if a single oracle answer is not available.""" oracle_answer = entry["answer"] - if answer is not None: + if isinstance(answer, str): if answer == oracle_answer: return 1.0 else: @@ -83,9 +83,9 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: # check if answer is python list of characters answer = "".join(eval(answer)) if answer == oracle_answer: - return 0.5 - except Exception as e: - return 0.01 + return 0.1 + except Exception: + pass return 0.0 def __getitem__(self, idx: int) -> dict: diff --git a/reasoning_gym/algorithmic/word_ladder.py b/reasoning_gym/algorithmic/word_ladder.py index 18670d4b..d15c7be6 100644 --- a/reasoning_gym/algorithmic/word_ladder.py +++ b/reasoning_gym/algorithmic/word_ladder.py @@ -221,8 +221,8 @@ def __getitem__(self, idx: int) -> dict: } def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: - if answer is None: - return 0 + if not isinstance(answer, str): + return 0.0 answer_words = tuple(s.strip() for s in answer.upper().split(",")) @@ -239,17 +239,17 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: # 4. all words are in our vocabulary if len(answer_words) < 2: - return 0 + return 0.0 if answer_words[0] != start_word or answer_words[-1] != end_word: - return 0.01 + return 0.0 if not all(len(w) == word_length for w in answer_words): - return 0.01 + return 0.0 for i in range(1, len(answer_words)): if sum(1 for a, b in zip(answer_words[i - 1], answer_words[i]) if a != b) != 1: - return 0.01 + return 0.0 reward = 1.0 for word in answer_words: diff --git a/reasoning_gym/algorithmic/word_sorting.py b/reasoning_gym/algorithmic/word_sorting.py index fc65c976..d246bd5a 100644 --- a/reasoning_gym/algorithmic/word_sorting.py +++ b/reasoning_gym/algorithmic/word_sorting.py @@ -121,8 +121,6 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: return 1.0 elif sorted(parsed_answer) == oracle_answer: return 0.2 - else: - return 0.01 return 0.0 diff --git a/reasoning_gym/arc/arc_agi.py b/reasoning_gym/arc/arc_agi.py index 98c3f000..a19a6507 100644 --- a/reasoning_gym/arc/arc_agi.py +++ b/reasoning_gym/arc/arc_agi.py @@ -199,7 +199,7 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: else: reward = 0.05 except: - reward = 0.01 + reward = 0.0 return reward diff --git a/reasoning_gym/arc/rearc.py b/reasoning_gym/arc/rearc.py index fe4a9aa5..dabbe808 100644 --- a/reasoning_gym/arc/rearc.py +++ b/reasoning_gym/arc/rearc.py @@ -106,7 +106,7 @@ def score_answer(self, answer: str, entry: dict[str, Any]) -> float: else: reward = 0.05 except: - reward = 0.01 + reward = 0.0 return reward diff --git a/reasoning_gym/arithmetic/bitwise_arithmetic.py b/reasoning_gym/arithmetic/bitwise_arithmetic.py index a5fcf79a..97de426b 100644 --- a/reasoning_gym/arithmetic/bitwise_arithmetic.py +++ b/reasoning_gym/arithmetic/bitwise_arithmetic.py @@ -160,17 +160,15 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: Returns: float: 1.0 if the user's answer is correct; otherwise, 0.01 unless no answer is provided, in which case 0. """ - if answer is None: - return 0.0 - - try: - solved = verify_solution(entry["metadata"]["problem"], answer) - if solved: - return 1.0 - except Exception: - return 0.01 - - return 0.01 + if isinstance(answer, str): + try: + solved = verify_solution(entry["metadata"]["problem"], answer) + if solved: + return 1.0 + except Exception: + pass + + return 0.0 # Register the dataset with the factory. diff --git a/reasoning_gym/arithmetic/calendar_arithmetic.py b/reasoning_gym/arithmetic/calendar_arithmetic.py index 9b307001..7ed0e84b 100644 --- a/reasoning_gym/arithmetic/calendar_arithmetic.py +++ b/reasoning_gym/arithmetic/calendar_arithmetic.py @@ -428,7 +428,7 @@ def _random_date_between(self, rng: random.Random, start_date: date, end_date: d def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: # we suppose the answer is the last occurence of the expected answer type - if answer is None: + if not isinstance(answer, str) or len(answer) == 0: return 0.0 oracle_answer = entry["answer"] @@ -439,9 +439,6 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: CalendarTask.WEEKDAY_OF_DATE_FROM_FIRST_DATE.value, CalendarTask.WEEKDAY_OF_DATE.value, }: - if not answer: - return 0.0 - answer = answer.strip() oracle_answer = oracle_answer weekdays = {d.name.title() for d in Weekday} diff --git a/reasoning_gym/arithmetic/decimal_arithmetic.py b/reasoning_gym/arithmetic/decimal_arithmetic.py index bf00c2af..86c4f5ff 100644 --- a/reasoning_gym/arithmetic/decimal_arithmetic.py +++ b/reasoning_gym/arithmetic/decimal_arithmetic.py @@ -178,7 +178,7 @@ def __getitem__(self, idx: int) -> dict[str, Any]: + problem_str ) - return {"question": problem_str, "answer": answer, "metadata": {}} + return {"question": problem_str, "answer": str(answer), "metadata": {}} def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: """ @@ -189,12 +189,12 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: Returns: float: 1.0 if the user's answer is within tolerance; otherwise, 0.01. """ - if answer is None: + if not isinstance(answer, str): return 0.0 try: user_ans: Decimal = Decimal(answer) - correct_ans: Decimal = entry["answer"] + correct_ans: Decimal = Decimal(entry["answer"]) # Determine tolerance based on the desired precision. precision: int = self.config.max_num_decimal_places @@ -202,9 +202,9 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: if abs(user_ans - correct_ans) <= tol: return 1.0 except Exception: - return 0.01 + pass - return 0.01 + return 0.0 # Register the dataset with the factory. diff --git a/reasoning_gym/arithmetic/decimal_chain_sum.py b/reasoning_gym/arithmetic/decimal_chain_sum.py index d2313789..55f9b411 100644 --- a/reasoning_gym/arithmetic/decimal_chain_sum.py +++ b/reasoning_gym/arithmetic/decimal_chain_sum.py @@ -1,6 +1,6 @@ import random from dataclasses import dataclass -from decimal import Decimal +from decimal import Decimal, InvalidOperation from typing import Any, Optional from ..factory import ProceduralDataset, register_dataset @@ -129,7 +129,11 @@ def _generate_task(self, rng: random.Random, num_terms: int, min_value: int, max result -= c expression = " ".join(expression_parts) - result = result.quantize(Decimal(f"0.{'0' * max(decimal_places)}")) + try: + q = Decimal(f"0.{'0' * max(decimal_places)}") + result = result.quantize(q) + except InvalidOperation: + pass return expression, result def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: @@ -141,16 +145,19 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: Returns: 1.0 for exact numerical match, 0.01 otherwise """ - if answer is None or len(answer.strip()) == 0: + if not isinstance(answer, str) or len(answer.strip()) == 0: return 0.0 try: student_answer = Decimal(answer.strip()) oracle_answer = Decimal(entry["answer"]) - return 1.0 if student_answer == oracle_answer else 0.01 - except (ValueError, TypeError, ArithmeticError): - return 0.01 + if student_answer == oracle_answer: + return 1.0 + except Exception: + pass + + return 0.0 register_dataset("decimal_chain_sum", DecimalChainSumDataset, DecimalChainSumConfig) diff --git a/reasoning_gym/arithmetic/dice.py b/reasoning_gym/arithmetic/dice.py index 0dcf3e44..00cc6b7d 100644 --- a/reasoning_gym/arithmetic/dice.py +++ b/reasoning_gym/arithmetic/dice.py @@ -138,12 +138,11 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: float: The computed score between 0.0 and 1.0. """ - if answer == None: - return 0.0 - if answer.lower().replace("\n", "") != entry["answer"].lower().replace("\n", ""): - return 0.01 - else: - return 1.0 # Yay + if isinstance(answer, str): + if answer.lower().replace("\n", "") == entry["answer"].lower().replace("\n", ""): + return 1.0 # Yay + + return 0.0 register_dataset("dice", DiceDataset, DiceConfig) diff --git a/reasoning_gym/arithmetic/number_format.py b/reasoning_gym/arithmetic/number_format.py index 0c2b79d5..a85bf7cb 100644 --- a/reasoning_gym/arithmetic/number_format.py +++ b/reasoning_gym/arithmetic/number_format.py @@ -65,14 +65,13 @@ def _transform_candidates(self, rng: Random, candidates: list[float]) -> list[st def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: """Overwrite this method in derived classes if a single oracle answer is not available.""" oracle_answer = entry["metadata"]["solution"] - if answer is not None and len(answer) > 0: + if isinstance(answer, str) and len(answer) > 0: try: answer = float(answer.strip().replace(",", "")) if abs(answer - oracle_answer) < 1e-2: return 1.0 - return 0.01 except: - return 0.0 + pass return 0.0 def __getitem__(self, idx: int) -> dict: diff --git a/reasoning_gym/arithmetic/power_function.py b/reasoning_gym/arithmetic/power_function.py index 879f4848..5d5848c0 100644 --- a/reasoning_gym/arithmetic/power_function.py +++ b/reasoning_gym/arithmetic/power_function.py @@ -44,10 +44,8 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: return 1.0 elif difference < 1e-1: return 0.5 - else: - return 0.01 - except Exception as e: - return 0.01 + except Exception: + pass return 0.0 def __getitem__(self, idx: int) -> dict: diff --git a/reasoning_gym/arithmetic/time_intervals.py b/reasoning_gym/arithmetic/time_intervals.py index 58e30cba..eb23b232 100644 --- a/reasoning_gym/arithmetic/time_intervals.py +++ b/reasoning_gym/arithmetic/time_intervals.py @@ -246,7 +246,7 @@ def score_answer(self, answer: Optional[str], entry: dict) -> float: Returns a score between 0 and 1, with partial credit for answers that are close to correct in the appropriate units/format """ - if not answer: + if not isinstance(answer, str): return 0.0 expected = entry["answer"] diff --git a/reasoning_gym/code/bf.py b/reasoning_gym/code/bf.py index 3a7d9b0d..fe16f3c8 100644 --- a/reasoning_gym/code/bf.py +++ b/reasoning_gym/code/bf.py @@ -121,20 +121,23 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: float: The computed score between 0.0 and 1.0. """ - if answer == None: + if not isinstance(answer, str): return 0.0 - if answer != entry["answer"]: - if entry["answer"] in answer.splitlines(): - # We can be quite confident that the correct answer was given - # It was likely just given alongside an explanation - return max(0.9 * len(answer) / len(entry["answer"]), 0.1) - if entry["answer"] in answer: - # Since answers are English words, some risk of the response coincidentally containing the answer - return max(0.5 * len(answer) / len(entry["answer"]), 0.1) - return 0.01 - else: + + if answer == entry["answer"]: return 1.0 # Yay + if entry["answer"] in answer.splitlines(): + # We can be quite confident that the correct answer was given + # It was likely just given alongside an explanation + return max(0.9 * len(answer) / len(entry["answer"]), 0.1) + + if entry["answer"] in answer: + # Since answers are English words, some risk of the response coincidentally containing the answer + return max(0.5 * len(answer) / len(entry["answer"]), 0.1) + + return 0.0 + # Register the dataset register_dataset("bf", BFDataset, BFConfig) diff --git a/reasoning_gym/cognition/figlet_fonts.py b/reasoning_gym/cognition/figlet_fonts.py index 4c16dec2..f4150fd5 100644 --- a/reasoning_gym/cognition/figlet_fonts.py +++ b/reasoning_gym/cognition/figlet_fonts.py @@ -182,7 +182,7 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: """ correct_word = entry["answer"] - if not answer: + if not isinstance(answer, str): return 0.0 # No answer given # Normalize case diff --git a/reasoning_gym/cognition/needle_haystack.py b/reasoning_gym/cognition/needle_haystack.py index f83318b8..e5adf741 100644 --- a/reasoning_gym/cognition/needle_haystack.py +++ b/reasoning_gym/cognition/needle_haystack.py @@ -110,19 +110,17 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: Returns: float: The computed score between 0.0 and 1.0. """ + if isinstance(answer, str): + correct_word = entry["answer"] - correct_word = entry["answer"] - if not answer: - return 0.0 # No answer given + # Normalize case + answer = answer.replace(" ", "").strip().lower() + correct_word = correct_word.strip().lower() - # Normalize case - answer = answer.replace(" ", "").strip().lower() - correct_word = correct_word.strip().lower() + if answer == correct_word: + return 1.0 # Correct! - if answer == correct_word: - return 1.0 # Correct! - - return 0.01 + return 0.0 # Register the dataset diff --git a/reasoning_gym/cognition/rectangle_count.py b/reasoning_gym/cognition/rectangle_count.py index b258b615..6f096e94 100644 --- a/reasoning_gym/cognition/rectangle_count.py +++ b/reasoning_gym/cognition/rectangle_count.py @@ -132,12 +132,10 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: float: The computed score between 0.0 and 1.0. """ - if answer == None: - return 0.0 - if answer.lower().replace("\n", "") != entry["answer"].lower().replace("\n", ""): - return 0.01 - else: - return 1.0 # Yay + if isinstance(answer, str): + if answer.lower().replace("\n", "") == entry["answer"].lower().replace("\n", ""): + return 1.0 # Yay + return 0.0 register_dataset("rectangle_count", RectangleCountDataset, RectangleCountConfig) diff --git a/reasoning_gym/dataset.py b/reasoning_gym/dataset.py index d727c863..29aea330 100644 --- a/reasoning_gym/dataset.py +++ b/reasoning_gym/dataset.py @@ -69,9 +69,6 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: reward = 1.0 elif oracle_answer in answer: reward = len(oracle_answer) / len(answer) - else: - reward = 0.01 - return reward diff --git a/reasoning_gym/games/contrib/sokoban/levels/lvl0.dat b/reasoning_gym/games/contrib/sokoban/levels/lvl0.dat deleted file mode 100644 index 867d112a..00000000 --- a/reasoning_gym/games/contrib/sokoban/levels/lvl0.dat +++ /dev/null @@ -1,10 +0,0 @@ -+ + + + + + + -+ - * - - - + -+ - - - $ - + -+ X - - @ - + -+ - - - - - + -+ $ - + - - + -+ + - - - - + -+ X @ - $ - + -+ + - - - - + -+ + + + + + + diff --git a/reasoning_gym/games/contrib/sokoban/levels/lvl1.dat b/reasoning_gym/games/contrib/sokoban/levels/lvl1.dat deleted file mode 100644 index 9ba48c31..00000000 --- a/reasoning_gym/games/contrib/sokoban/levels/lvl1.dat +++ /dev/null @@ -1,5 +0,0 @@ -+ + + + + + + -+ * - @ - X + -+ + - @ - + + -+ X - - - - + -+ + + + + + + diff --git a/reasoning_gym/games/contrib/sokoban/levels/lvl2.dat b/reasoning_gym/games/contrib/sokoban/levels/lvl2.dat deleted file mode 100644 index 46755810..00000000 --- a/reasoning_gym/games/contrib/sokoban/levels/lvl2.dat +++ /dev/null @@ -1,6 +0,0 @@ -- - + + + + + + -- + + - - - * + -+ + - - - + X + -+ X - @ - @ @ + -+ X X @ - - - + -+ + + + + + + + diff --git a/reasoning_gym/games/contrib/sokoban/levels/lvl3.dat b/reasoning_gym/games/contrib/sokoban/levels/lvl3.dat deleted file mode 100644 index 9d0bc599..00000000 --- a/reasoning_gym/games/contrib/sokoban/levels/lvl3.dat +++ /dev/null @@ -1,7 +0,0 @@ -- + + + + + + - - - -- + X - - X + - - - -+ + - @ @ + + - - - -+ - - - - + + - - - -+ - @ - - * + + + + -+ + - - - - - - X + -- + + + + + + + + + diff --git a/reasoning_gym/games/contrib/sokoban/levels/lvl4.dat b/reasoning_gym/games/contrib/sokoban/levels/lvl4.dat deleted file mode 100644 index 42fbc6eb..00000000 --- a/reasoning_gym/games/contrib/sokoban/levels/lvl4.dat +++ /dev/null @@ -1,7 +0,0 @@ -- + + + + + + - - -+ + X - @ - + + + -+ - - - - - - - + -+ - @ + + X - @ + -+ - - - @ - + - + -+ + + * - X - X + -- - + + + + + + + diff --git a/reasoning_gym/games/contrib/sokoban/levels/lvl5.dat b/reasoning_gym/games/contrib/sokoban/levels/lvl5.dat deleted file mode 100644 index 3a096d58..00000000 --- a/reasoning_gym/games/contrib/sokoban/levels/lvl5.dat +++ /dev/null @@ -1,7 +0,0 @@ -- + + + + + + + - -+ + - - + - - + + -+ - @ - - - @ - + -+ - - X * X - - + -+ + @ + + - - + + -+ - - X - - - + - -+ + + + + + + + - diff --git a/reasoning_gym/games/contrib/sokoban/levels/lvl6.dat b/reasoning_gym/games/contrib/sokoban/levels/lvl6.dat deleted file mode 100644 index 32ee5bbc..00000000 --- a/reasoning_gym/games/contrib/sokoban/levels/lvl6.dat +++ /dev/null @@ -1,9 +0,0 @@ -- - - + + + + + + + + -- - - + - - - - - - + -- - + + - - - - @ - + -- + + - - + + - + + + -+ + - - + - - X - - + -+ - - + X @ @ - - + + -+ * + X - - - - + + - -+ + - - - - - + + - - -+ + + + + + + + - - - diff --git a/reasoning_gym/games/contrib/sokoban/levels/lvl7.dat b/reasoning_gym/games/contrib/sokoban/levels/lvl7.dat deleted file mode 100644 index 9c2fe302..00000000 --- a/reasoning_gym/games/contrib/sokoban/levels/lvl7.dat +++ /dev/null @@ -1,6 +0,0 @@ -+ + + + + + + + -+ - - @ - X * + -+ - @ - - + X + -+ X X @ - @ @ + -+ X X @ - - - + -+ + + + + + + + diff --git a/reasoning_gym/games/contrib/sokoban/src/astar.py b/reasoning_gym/games/contrib/sokoban/src/astar.py index 25d1e63d..10b185f6 100644 --- a/reasoning_gym/games/contrib/sokoban/src/astar.py +++ b/reasoning_gym/games/contrib/sokoban/src/astar.py @@ -13,7 +13,7 @@ ) -def astar(matrix, player_pos, debug=False, heuristic="manhattan"): +def astar(matrix, player_pos, debug: bool = False, heuristic: str = "manhattan", max_depth: int = 100): # print(f'A* - {heuristic.title()} Heuristic') heur = "[A*]" if heuristic == "manhattan" else "[Dijkstra]" shape = matrix.shape @@ -67,15 +67,18 @@ def astar(matrix, player_pos, debug=False, heuristic="manhattan"): return (path + direction[move], depth + 1) if debug: print(f"{heur} Solution Depth: {depth + 1}\n{path + direction[move]}", 20) - print(f"{heur} Solution not found!\n") + + if depth > max_depth: + break + if debug: print(f"{heur} Solution Not Found!\nDepth {depth + 1}", 20) return (None, -1 if not heap else depth + 1) -def solve_astar(puzzle, visualizer=False, heuristic="manhattan"): +def solve_astar(puzzle, visualizer: bool = False, heuristic: str = "manhattan", max_depth: int = 100): matrix = puzzle where = np.where((matrix == "*") | (matrix == "%")) player_pos = where[0][0], where[1][0] - return astar(matrix, player_pos, debug=visualizer, heuristic=heuristic) + return astar(matrix, player_pos, debug=visualizer, heuristic=heuristic, max_depth=max_depth) diff --git a/reasoning_gym/games/contrib/sokoban/src/game.py b/reasoning_gym/games/contrib/sokoban/src/game.py index f01f3720..be50b52c 100644 --- a/reasoning_gym/games/contrib/sokoban/src/game.py +++ b/reasoning_gym/games/contrib/sokoban/src/game.py @@ -29,8 +29,7 @@ def __str__(self) -> str: class Game: - def __init__(self, width=19, height=10, level=None, path=None): - self.level = level + def __init__(self, width=19, height=10, path=None): self.width = width self.height = height self.puzzle = np.empty((height, width), dtype=PuzzleElement) @@ -39,7 +38,7 @@ def __init__(self, width=19, height=10, level=None, path=None): self.puzzle_size = None self.pad_x = 0 self.pad_y = 0 - self.path = path or f"levels/lvl{level}.dat" + self.path = path if path: if type(self) == Game: @@ -108,7 +107,7 @@ def _process_puzzle_data(self, data): # Calculate puzzle size and padding self.puzzle_size = (len(data), len(data[0]) if len(data) > 0 else 0) - pad_x = (self.width - self.puzzle_size[1] - 2) // 2 # -2 matches original file-based logic + pad_x = (self.width - self.puzzle_size[1]) // 2 pad_y = (self.height - self.puzzle_size[0]) // 2 self.pad_x, self.pad_y = pad_x, pad_y @@ -140,15 +139,15 @@ def _process_puzzle_data(self, data): class ReverseGame(Game): - def __init__(self, rng: Random, width=19, height=10, level=None): - super().__init__(width, height, level) + def __init__(self, rng: Random, width: int = 19, height: int = 10): + super().__init__(width, height) self.rng = rng self.pad_x = 0 self.pad_y = 0 def load_puzzle(self, puzzle): self.puzzle_size = (len(puzzle), len(puzzle[0]) if len(puzzle) > 0 else 0) - pad_x = (self.width - len(puzzle[0]) - 2) // 2 + pad_x = (self.width - len(puzzle[0])) // 2 pad_y = (self.height - len(puzzle)) // 2 self.pad_x, self.pad_y = pad_x, pad_y for i, row in enumerate(puzzle): diff --git a/reasoning_gym/games/contrib/sokoban/src/generator.py b/reasoning_gym/games/contrib/sokoban/src/generator.py index da4c954f..372a4168 100644 --- a/reasoning_gym/games/contrib/sokoban/src/generator.py +++ b/reasoning_gym/games/contrib/sokoban/src/generator.py @@ -7,6 +7,9 @@ def num_boxes(puzzle_area, min_boxes, max_boxes, min_w, min_h, max_w, max_h): + if min_w == max_w or min_h == max_h or min_boxes == max_boxes: + return max_boxes + m = (max_boxes - min_boxes) / (max_w * max_h - min_w * min_h) b = min_boxes - m * min_w * min_h return int(m * puzzle_area + b) @@ -19,31 +22,33 @@ def random_valid(rng: Random, width: int = 10, height: int = 10): def generate( rng: Random, debug: bool = False, - path: str = None, min_w: int = 6, min_h: int = 6, max_w: int = 15, max_h: int = 10, min_boxes: int = 4, max_boxes: int = 10, + max_depth: int = 100, + path: str = None, ) -> tuple[str, str, dict]: """ Generates a level with the given configuration parameters. Parameters: - rng: Random number generator for reproducibility. - visualizer: Whether to visualize the generation process. - path: Path to save the level file (default 'levels/lvl0.dat'). - min_w: Minimum width of the puzzle. - min_h: Minimum height of the puzzle. - max_w: Maximum width of the puzzle. - max_h: Maximum height of the puzzle. - min_boxes: Minimum number of boxes. - max_boxes: Maximum number of boxes. + rng: Random number generator + visualizer: Whether to visualize the generation process + min_w: Minimum width of the puzzle + min_h: Minimum height of the puzzle + max_w: Maximum width of the puzzle + max_h: Maximum height of the puzzle + min_boxes: Minimum number of boxes + max_boxes: Maximum number of boxes + max_depth: Maximum search depth + path: Path to save the level file (optional) Returns: puzzle_string, solution """ - path = path or "levels/lvl0.dat" + while True: width = rng.randint(min_w, max_w) height = rng.randint(min_h, max_h) @@ -60,7 +65,7 @@ def generate( puzzle[box_pos] = "$" boxes_created += 1 boxes_seen.add(box_pos) - reverse_game = ReverseGame(rng=rng, level=0) + reverse_game = ReverseGame(rng=rng, width=width, height=height) reverse_game.load_puzzle(puzzle) player = reverse_game.player counter = round(height * width * rng.uniform(1.8, 3.6)) @@ -79,16 +84,19 @@ def generate( out_of_place_boxes = np.sum([str(x) == "@" for x in matrix.flatten()]) if out_of_place_boxes >= boxes // 2: # Optionally save the puzzle to a file: - # np.savetxt(path, matrix, fmt='%s') + if path: + np.savetxt(path, matrix, fmt="%s") puzzle_str = player.puzzle_to_string(matrix) grid_list = [list(line) for line in puzzle_str.replace(" ", "").strip().split("\n")] grid_array = np.array(grid_list) - solution, _ = solve_astar(grid_array) + solution, depth = solve_astar(grid_array, max_depth=max_depth) + if solution is None: + continue # retry generation if debug: print(f"solution={solution}") - game = Game() + game = Game(width=width, height=height) game.load_puzzle_matrix(grid_array) for step, move in enumerate(solution): diff --git a/reasoning_gym/games/futoshiki.py b/reasoning_gym/games/futoshiki.py index ce70e14a..092087f1 100644 --- a/reasoning_gym/games/futoshiki.py +++ b/reasoning_gym/games/futoshiki.py @@ -618,7 +618,7 @@ def _try_remove(): return grid def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: - if not answer: + if not isinstance(answer, str): return 0.0 oracle_answer = entry["answer"] diff --git a/reasoning_gym/games/knight_swap.py b/reasoning_gym/games/knight_swap.py index 1e0270f5..2a54289d 100644 --- a/reasoning_gym/games/knight_swap.py +++ b/reasoning_gym/games/knight_swap.py @@ -314,36 +314,35 @@ def score_answer(self, answer: Optional[str], entry: dict) -> float: - 1.0 for correct answer (either "No" for impossible puzzles or valid solution of optimal length) - A proportional score for correct but longer solutions - 0.05 for valid moves that don't solve the puzzle - - 0.01 for invalid format - - 0.0 for None + - 0.0 for invalid format or None """ - if answer is None: + if not isinstance(answer, str): return 0.0 answer = answer.strip() - if not answer: - return 0.01 + if len(answer) == 0: + return 0.0 # Handle impossible puzzles if not entry["metadata"]["is_possible"]: - return 1.0 if answer.lower() == "no" else 0.01 + return 1.0 if answer.lower() == "no" else 0.0 # Handle "No" answer for possible puzzles if answer.lower() == "no": - return 0.01 + return 0.0 try: # Parse moves from JSON list move_list = json.loads(answer) if not isinstance(move_list, list): - return 0.01 + return 0.0 # Parse moves moves = [] for move_str in move_list: color, start, end = move_str.split(",") if color not in ("w", "B"): - return 0.01 + return 0.0 moves.append((color, start, end)) # Validate and apply moves @@ -357,13 +356,13 @@ def score_answer(self, answer: Optional[str], entry: dict) -> float: for color, start, end in moves: if color != current_turn: - return 0.01 + return 0.0 if start not in pieces or pieces[start] != color: - return 0.01 + return 0.0 if end not in board[start]: - return 0.01 + return 0.0 if end in pieces and pieces[end] is not None: - return 0.01 + return 0.0 # Apply move pieces[end] = pieces[start] @@ -390,7 +389,7 @@ def score_answer(self, answer: Optional[str], entry: dict) -> float: return 0.05 except Exception: - return 0.01 + return 0.0 register_dataset("knight_swap", KnightSwapDataset, KnightSwapConfig) diff --git a/reasoning_gym/games/mini_sudoku.py b/reasoning_gym/games/mini_sudoku.py index 319569ff..46df6aa2 100644 --- a/reasoning_gym/games/mini_sudoku.py +++ b/reasoning_gym/games/mini_sudoku.py @@ -195,7 +195,7 @@ def __getitem__(self, idx: int) -> dict: } def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: - if not answer: + if not isinstance(answer, str) or len(answer) == 0: return 0.0 oracle_answer = entry["answer"] diff --git a/reasoning_gym/games/n_queens.py b/reasoning_gym/games/n_queens.py index 8fcecfd4..3a07bb10 100644 --- a/reasoning_gym/games/n_queens.py +++ b/reasoning_gym/games/n_queens.py @@ -138,8 +138,8 @@ def __getitem__(self, idx: int) -> dict: } def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: - valid_solutions = entry["metadata"]["valid_answers"] - if answer is not None: + if isinstance(answer, str): + valid_solutions = entry["metadata"]["valid_answers"] if answer in valid_solutions: return 1.0 try: @@ -147,7 +147,7 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: if answer in valid_solutions: return 0.5 except Exception as e: - return 0.01 + pass return 0.0 diff --git a/reasoning_gym/games/rush_hour.py b/reasoning_gym/games/rush_hour.py index 1ab9489a..ba48f8a2 100644 --- a/reasoning_gym/games/rush_hour.py +++ b/reasoning_gym/games/rush_hour.py @@ -171,7 +171,7 @@ def score_answer(self, answer: Optional[str], entry: dict) -> float: Returns: 1.0 if solution reaches goal state, 0.0 otherwise """ - if not answer: + if not isinstance(answer, str) or len(answer) == 0: return 0.0 try: diff --git a/reasoning_gym/games/sokoban.py b/reasoning_gym/games/sokoban.py index 1b12169c..0f81930d 100644 --- a/reasoning_gym/games/sokoban.py +++ b/reasoning_gym/games/sokoban.py @@ -11,20 +11,26 @@ class SokobanConfig: """Configuration for sokoban puzzle generation""" - min_w: int = 6 # Minimum width of the puzzle. - min_h: int = 6 # Minimum height of the puzzle. - max_w: int = 10 # Maximum width of the puzzle. - max_h: int = 10 # Maximum height of the puzzle. - min_boxes: int = 6 # Minimum number of boxes. - max_boxes: int = 10 # Maximum number of boxes. + min_w: int = 6 # Minimum width of the puzzle + min_h: int = 6 # Minimum height of the puzzle + max_w: int = 10 # Maximum width of the puzzle + max_h: int = 10 # Maximum height of the puzzle + min_boxes: int = 4 # Minimum number of boxes + max_boxes: int = 10 # Maximum number of boxes + max_depth: int = 80 # Maximum search depth seed: Optional[int] = None size: int = 500 def validate(self): """Validate configuration parameters""" + assert 0 < self.max_w <= 20 + assert 0 < self.max_h <= 20 + assert self.min_h > 0 + assert self.min_w > 0 assert self.min_w <= self.max_w, "min_w must be lte max_w" assert self.min_h <= self.max_h, "min_h must be lte max_h" assert self.min_boxes <= self.max_boxes, "min_boxes must be lte max_boxes" + assert self.max_depth > 1 class SokobanDataset(ProceduralDataset): @@ -58,7 +64,16 @@ def __getitem__(self, idx: int) -> dict: # Make the Sokoban! rng = Random(self.seed + idx) - gamestr, solution, difficulty = self._generate(rng=rng) + gamestr, solution, difficulty = self._generate( + rng=rng, + min_w=self.config.min_w, + min_h=self.config.min_h, + max_w=self.config.max_w, + max_h=self.config.max_h, + min_boxes=self.config.min_boxes, + max_boxes=self.config.max_boxes, + max_depth=self.config.max_depth, + ) return { "question": """You are going to solve a 'sokoban' puzzle. @@ -93,14 +108,15 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: float: The computed score between 0.0 and 1.0. """ - if answer == None: + if not isinstance(answer, str): return 0.0 try: grid_list = [list(line) for line in entry["metadata"]["gamestr"].replace(" ", "").strip().split("\n")] matrix = np.array(grid_list) - game = self._Game() + h, w = matrix.shape + game = self._Game(height=h, width=w) game.load_puzzle_matrix(matrix) for move in answer: @@ -108,10 +124,10 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: if self._is_solved(game.get_curr_state()): return 1.0 - except Exception as e: - return 0.01 + except: + pass - return 0.1 + return 0.0 register_dataset("sokoban", SokobanDataset, SokobanConfig) diff --git a/reasoning_gym/games/sudoku.py b/reasoning_gym/games/sudoku.py index 9b07184b..d7ecf8d2 100644 --- a/reasoning_gym/games/sudoku.py +++ b/reasoning_gym/games/sudoku.py @@ -214,7 +214,7 @@ def __getitem__(self, idx: int) -> dict: } def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: - if not answer: + if not isinstance(answer, str) or len(answer) == 0: return 0.0 oracle_answer = entry["answer"] diff --git a/reasoning_gym/games/tower_of_hanoi.py b/reasoning_gym/games/tower_of_hanoi.py index 052d72bf..9ab50cb8 100644 --- a/reasoning_gym/games/tower_of_hanoi.py +++ b/reasoning_gym/games/tower_of_hanoi.py @@ -266,7 +266,7 @@ def __getitem__(self, idx: int) -> dict: start_peg=peg_labels[start_peg], target_peg=peg_labels[target_peg], ), - "answer": solution, + "answer": "\n".join(solution), "metadata": { "num_disks": num_disks, "num_pegs": num_pegs, @@ -383,24 +383,14 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: Expected behavior: - Correct answer (i.e. equivalent in length, or better, than the one provided in the dataset item) gives 1.0. - A correct solution that is suboptimal length gives a proportional reward of optimal_move_count/user_move_count - - A badly formatted answer gives a minimal reward (0.01). - An answer that is syntactically valid but does not solve the puzzle gives a partial reward (0.05). - - An empty string gives 0.01. - - None gives 0.0. + - A badly formatted or empty answer gives 0.0 """ - if answer is None: + if not isinstance(answer, str) or len(answer) == 0: return 0.0 - if answer == "": - return 0.01 - - # If answer is a string, split it into lines; if it's already a list, use it directly. - if isinstance(answer, str): - moves = [line.strip() for line in answer.strip().splitlines() if line.strip()] - elif isinstance(answer, list): - moves = [line.strip() for line in answer if isinstance(line, str) and line.strip()] - else: - return 0.0 + # Spilt answer string it into lines + moves = [line.strip() for line in answer.strip().splitlines() if line.strip()] # Build the initial peg state from metadata. metadata = entry["metadata"] @@ -418,11 +408,11 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: try: disk, from_peg, to_peg = self._parse_move(move) except Exception: - return 0.01 # Invalid move format + return 0.0 # Invalid move format # Validate the move using existing _validate_move method. if not self._validate_move(peg_state, move): - return 0.01 + return 0.0 # Execute the move. peg_state[from_peg].pop() diff --git a/reasoning_gym/graphs/family_relationships.py b/reasoning_gym/graphs/family_relationships.py index 973de976..51c0055c 100644 --- a/reasoning_gym/graphs/family_relationships.py +++ b/reasoning_gym/graphs/family_relationships.py @@ -358,16 +358,14 @@ def _generate_story(self, family: set[Person]) -> str: def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: reward = 0.0 - if answer is not None: + if isinstance(answer, str): try: answer_formatted = answer.strip().lower() - solved = answer_formatted == entry["answer"].strip().lower() - if solved: + oracle_answer = entry["answer"].strip().lower() + if answer_formatted == oracle_answer: reward = 1.0 - else: - reward = 0.01 except: - reward = 0.01 + pass return reward diff --git a/reasoning_gym/graphs/quantum_lock.py b/reasoning_gym/graphs/quantum_lock.py index c32085f2..e2196906 100644 --- a/reasoning_gym/graphs/quantum_lock.py +++ b/reasoning_gym/graphs/quantum_lock.py @@ -169,21 +169,15 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: The function awards 1.0 for a correct answer and less otherwise. """ - if answer == None: + if not isinstance(answer, str): return 0.0 - # Get correct solution from metadata - correct_solution = entry["metadata"].get("solution_path", []) - # Normalize both answers - def normalize_seq(seq): - """Handle both string and list inputs by converting to string first""" - # Convert sequence to string representation if it's a list - input_str = "".join(seq) if isinstance(seq, list) else str(seq or "") - return [c.upper() for c in re.findall(r"[A-C]", input_str.upper())] + def normalize_seq(seq: str) -> list[str]: + return [c.upper() for c in re.findall(r"[A-C]", seq.upper())] user_sequence = normalize_seq(answer) - target_sequence = normalize_seq("".join(correct_solution)) + target_sequence = normalize_seq(entry["answer"]) # Exact sequence match required if user_sequence == target_sequence: @@ -196,7 +190,7 @@ def normalize_seq(seq): return 1.0 # Different answer, but qually correct return 0.5 # Alternative scoring - you're correct, but not optimal - return 0.1 + return 0.0 def simulate_sequence(self, metadata: dict, sequence: list[str]) -> int: """Simulate button presses to verify solutions""" diff --git a/reasoning_gym/graphs/shortest_path.py b/reasoning_gym/graphs/shortest_path.py index d915e114..43c45f54 100644 --- a/reasoning_gym/graphs/shortest_path.py +++ b/reasoning_gym/graphs/shortest_path.py @@ -125,8 +125,8 @@ def _is_valid_path(self, matrix: list[list[str]], path: list[str]) -> bool: def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: """Overwrite this method in derived classes if a single oracle answer is not available.""" - oracle_answer = entry["answer"].strip() - if answer is not None and len(answer) > 0: + if isinstance(answer, str) and len(answer) > 0: + oracle_answer = entry["answer"].strip() answer = answer.strip() # Exact answer @@ -145,8 +145,6 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: elif self._is_valid_path(matrix, answer): return 0.5 - return 0.01 - return 0.0 def __getitem__(self, idx: int) -> dict: diff --git a/reasoning_gym/logic/circuit_logic.py b/reasoning_gym/logic/circuit_logic.py index fd169076..7f8e69cb 100644 --- a/reasoning_gym/logic/circuit_logic.py +++ b/reasoning_gym/logic/circuit_logic.py @@ -401,16 +401,14 @@ def get_random_input() -> str: } def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: - if answer is None or len(answer) == 0: - return 0.0 - - oracle_answer = entry["answer"] - if oracle_answer == answer: - return 1.0 - elif oracle_answer == answer.strip(): - return len(oracle_answer) / len(answer) - - return 0.01 + if isinstance(answer, str) and len(answer) > 0: + oracle_answer = entry["answer"] + if oracle_answer == answer: + return 1.0 + elif oracle_answer == answer.strip(): + return len(oracle_answer) / len(answer) + + return 0.0 register_dataset("circuit_logic", CircuitLogicDataset, CircuitLogicConfig) diff --git a/reasoning_gym/logic/knights_knaves.py b/reasoning_gym/logic/knights_knaves.py index 7cc16ecf..0e96d069 100644 --- a/reasoning_gym/logic/knights_knaves.py +++ b/reasoning_gym/logic/knights_knaves.py @@ -489,7 +489,7 @@ def _normalize_answer(answer: str) -> set[tuple[str, str]]: def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: """Score an answer against the oracle answer.""" - if answer is None or len(answer) == 0: + if not isinstance(answer, str) or len(answer) == 0: return 0.0 try: @@ -506,11 +506,9 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: if matching > 0: return 0.3 + (0.7 * matching / len(oracle_assignments)) - return 0.01 - except Exception: - # If parsing fails, give minimal credit - return 0.01 + pass + return 0.0 register_dataset("knights_knaves", KnightsKnavesDataset, KnightsKnavesConfig) diff --git a/reasoning_gym/logic/propositional_logic.py b/reasoning_gym/logic/propositional_logic.py index 8732bc1b..b9387dfa 100644 --- a/reasoning_gym/logic/propositional_logic.py +++ b/reasoning_gym/logic/propositional_logic.py @@ -295,7 +295,7 @@ def _measure_complexity(self, expression: Expression) -> int: def score_answer(self, answer: str | None, entry: dict[str, Any]) -> float: """Robust scoring implementation for propositional logic answers""" - if not answer: + if not isinstance(answer, str): return 0.0 try: @@ -304,7 +304,7 @@ def score_answer(self, answer: str | None, entry: dict[str, Any]) -> float: valid_vars = set(entry["metadata"]["variables"]) answer_vars = re.findall(r"([A-Z])", cleaned_answer) if any(var not in valid_vars for var in answer_vars): - return 0.01 + return 0.0 premises = [Expression.from_string(p) for p in entry["metadata"]["premises"]] answer_expr = Expression.from_string(cleaned_answer) @@ -316,7 +316,7 @@ def score_answer(self, answer: str | None, entry: dict[str, Any]) -> float: return 1.0 return 0.05 except (ValueError, KeyError, AttributeError): - return 0.01 + return 0.0 def _is_trivial(self, expr: Expression) -> bool: """Check for trivial tautologies like P ∨ ¬P""" diff --git a/reasoning_gym/logic/self_reference.py b/reasoning_gym/logic/self_reference.py index f42ce415..a490a619 100644 --- a/reasoning_gym/logic/self_reference.py +++ b/reasoning_gym/logic/self_reference.py @@ -339,9 +339,7 @@ def __getitem__(self, idx: int) -> dict: # Solve puzzle solutions = solve_puzzle_dynamic(puzzle) - for idx, sol in enumerate(solutions, start=1): - sol_str = ["True" if s else "False" for s in sol] - answer = len(solutions) + answer = str(len(solutions)) return { "question": puzz_s, @@ -362,12 +360,10 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: float: The computed score between 0.0 and 1.0. """ - if answer == None: - return 0.0 - if str(answer) != str(entry["answer"]): - return 0.1 - else: - return 1.0 # Yay + if isinstance(answer, str): + if answer == str(entry["answer"]): + return 1.0 # Yay + return 0.0 register_dataset("self_reference", SelfReferenceDataset, SelfReferenceConfig) diff --git a/reasoning_gym/logic/zebra_puzzles.py b/reasoning_gym/logic/zebra_puzzles.py index c518c65d..7e424b13 100644 --- a/reasoning_gym/logic/zebra_puzzles.py +++ b/reasoning_gym/logic/zebra_puzzles.py @@ -68,12 +68,10 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: float: The computed score between 0.0 and 1.0. """ - if answer == None: - return 0.0 - if answer.lower().replace("\n", "") != entry["answer"].lower().replace("\n", ""): - return 0.01 - else: - return 1.0 # Yay + if isinstance(answer, str): + if answer.lower().replace("\n", "") == entry["answer"].lower().replace("\n", ""): + return 1.0 # Yay + return 0.0 register_dataset("zebra_puzzles", ZebraDataset, ZebraConfig) diff --git a/reasoning_gym/utils.py b/reasoning_gym/utils.py index 2143961d..ad0a5472 100644 --- a/reasoning_gym/utils.py +++ b/reasoning_gym/utils.py @@ -103,7 +103,6 @@ def compute_decimal_reward(answer: Optional[str], oracle_answer: str, strip_comm """ reward = 0.0 if answer is not None and len(answer) > 0: - reward = 0.01 try: if strip_commas: answer = answer.replace(",", "") diff --git a/tests/test_ab.py b/tests/test_ab.py index 489c4acf..e63a07bc 100644 --- a/tests/test_ab.py +++ b/tests/test_ab.py @@ -57,7 +57,7 @@ def test_ab_scoring(): # Test wrong answer wrong_answer = "A# B#" if item["answer"] != "A# B#" else "B# A#" - assert dataset.score_answer(answer=wrong_answer, entry=item) == 0.01 + assert dataset.score_answer(answer=wrong_answer, entry=item) == 0.0 # Test None answer assert dataset.score_answer(answer=None, entry=item) == 0.0 diff --git a/tests/test_arc_1d.py b/tests/test_arc_1d.py index 140916d6..1eeb0d09 100644 --- a/tests/test_arc_1d.py +++ b/tests/test_arc_1d.py @@ -103,7 +103,7 @@ def test_arc_1d_scoring(): assert dataset.score_answer(f"The answer is: {entry['answer']}", entry) > 0.5 # Test incorrect answer - assert dataset.score_answer("wrong answer", entry) == 0.01 + assert dataset.score_answer("wrong answer", entry) == 0.0 # Test None answer assert dataset.score_answer(None, entry) == 0.0 diff --git a/tests/test_arc_agi.py b/tests/test_arc_agi.py index cfeecf21..ca7a92e3 100644 --- a/tests/test_arc_agi.py +++ b/tests/test_arc_agi.py @@ -110,7 +110,7 @@ def test_arc_agi_scoring(): assert dataset.score_answer(item["answer"], entry=item) == 1.0 # Test invalid format - assert dataset.score_answer("invalid grid format", entry=item) == 0.01 + assert dataset.score_answer("invalid grid format", entry=item) == 0.0 # Test None answer assert dataset.score_answer(None, entry=item) == 0.0 diff --git a/tests/test_bf.py b/tests/test_bf.py index 86d2619a..0455febf 100644 --- a/tests/test_bf.py +++ b/tests/test_bf.py @@ -23,7 +23,7 @@ def test_bf(): # Test the scoring assert dataset.score_answer(answer=item["answer"], entry=item) == 1.0 assert dataset.score_answer(answer=None, entry=item) == 0.0 - assert dataset.score_answer(answer="Love is a battlefield", entry=item) == 0.01 + assert dataset.score_answer(answer="Love is a battlefield", entry=item) == 0.0 # Medium config = BFConfig(seed=43, size=20, difficulty=2) diff --git a/tests/test_binary_matrix.py b/tests/test_binary_matrix.py index 68094472..88ef9adc 100644 --- a/tests/test_binary_matrix.py +++ b/tests/test_binary_matrix.py @@ -115,7 +115,7 @@ def test_binary_matrix_answer(): # Answer is a python list (partially correct answer) answer = "[[0, 0, 0], [0, 1, 0], [1, 2, 1]]" entry = {"answer": "0 0 0\n0 1 0\n1 2 1"} - assert dataset.score_answer(answer, entry) == 0.5 + assert dataset.score_answer(answer, entry) == 0.1 # Answer is null answer = None diff --git a/tests/test_bitwise_arithmetic.py b/tests/test_bitwise_arithmetic.py index cbaff24b..854c764a 100644 --- a/tests/test_bitwise_arithmetic.py +++ b/tests/test_bitwise_arithmetic.py @@ -43,7 +43,7 @@ def test_bitwise_arithmetic_items(): # Test scoring edge cases assert dataset.score_answer(answer=None, entry=item) == 0.0 - assert dataset.score_answer(answer="invalid", entry=item) == 0.01 + assert dataset.score_answer(answer="invalid", entry=item) == 0.0 def test_bitwise_arithmetic_difficulty_levels(): diff --git a/tests/test_complex_arithmetic.py b/tests/test_complex_arithmetic.py index 994b94cc..14f67bfe 100644 --- a/tests/test_complex_arithmetic.py +++ b/tests/test_complex_arithmetic.py @@ -58,6 +58,7 @@ def test_complex_arithmetic_scoring(): assert dataset.score_answer("3 + 2i", entry) == 1.0 assert dataset.score_answer("3+2i", entry) == 1.0 assert dataset.score_answer("3.0 + 2.0i", entry) == 1.0 + assert dataset.score_answer("((3.0 + 2.0i ) )", entry) == 1.0 # Test answers with small errors (should get high but < 1.0 scores) print(dataset.score_answer("3.1 + 2i", entry)) diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 4373a204..88ff79f7 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -36,7 +36,7 @@ def test_reseeding_dataset_iteration(): # Test score_answer forwarding test_item = next(iter(infinite_dataset)) - assert infinite_dataset.score_answer("wrong", test_item) == 0.01 + assert infinite_dataset.score_answer("wrong", test_item) == 0.0 assert infinite_dataset.score_answer(test_item["answer"], test_item) == 1.0 diff --git a/tests/test_dataset_common.py b/tests/test_dataset_common.py new file mode 100644 index 00000000..d65c4cb5 --- /dev/null +++ b/tests/test_dataset_common.py @@ -0,0 +1,17 @@ +import reasoning_gym +from reasoning_gym.factory import DATASETS + + +def test_score_answer_consistency(): + for dataset_name in DATASETS.keys(): + if dataset_name == "composite": + continue + dataset = reasoning_gym.create_dataset(dataset_name, size=10, seed=1234) + for entry in dataset: + assert entry["answer"] is None or isinstance( + entry["answer"], str + ), f"{dataset_name} answer must be str, is {type(entry['answer'])}" + if entry["answer"] is not None: + assert ( + dataset.score_answer(answer=entry["answer"], entry=entry) == 1.0 + ), f"inconsistent score_answer {dataset_name}" diff --git a/tests/test_decimal_chain_sum.py b/tests/test_decimal_chain_sum.py index 5114a7c7..e488b77c 100644 --- a/tests/test_decimal_chain_sum.py +++ b/tests/test_decimal_chain_sum.py @@ -242,11 +242,11 @@ def test_decimal_precision_scoring(): assert dataset.score_answer("0.3", {"answer": "0.300"}) == 1.0 # Test incorrect answers - assert dataset.score_answer("1.200000001", {"answer": "1.200"}) == 0.01 - assert dataset.score_answer("1.199999999", {"answer": "1.200"}) == 0.01 + assert dataset.score_answer("1.200000001", {"answer": "1.200"}) == 0.0 + assert dataset.score_answer("1.199999999", {"answer": "1.200"}) == 0.0 # Test invalid inputs assert dataset.score_answer(None, {"answer": "1.200"}) == 0.0 assert dataset.score_answer("", {"answer": "1.200"}) == 0.0 - assert dataset.score_answer("invalid", {"answer": "1.200"}) == 0.01 - assert dataset.score_answer("1.2.3", {"answer": "1.200"}) == 0.01 + assert dataset.score_answer("invalid", {"answer": "1.200"}) == 0.0 + assert dataset.score_answer("1.2.3", {"answer": "1.200"}) == 0.0 diff --git a/tests/test_game_of_life.py b/tests/test_game_of_life.py index 924a12de..bc3c0c61 100644 --- a/tests/test_game_of_life.py +++ b/tests/test_game_of_life.py @@ -50,7 +50,7 @@ def test_game_of_life_basic_properties(): # Test the scoring assert dataset.score_answer(answer=item["answer"], entry=item) == 1.0 assert dataset.score_answer(answer=None, entry=item) == 0.0 - assert dataset.score_answer(answer="invalid json", entry=item) == 0.01 + assert dataset.score_answer(answer="invalid json", entry=item) == 0.0 config = GameOfLifeConfig(seed=43, size=1, grid_size_x=3, grid_size_y=3, filled_cells=1, simulation_steps=1) dataset = GameOfLifeDataset(config) diff --git a/tests/test_intermediate_integration.py b/tests/test_intermediate_integration.py index f4393e2f..6cb32ec8 100644 --- a/tests/test_intermediate_integration.py +++ b/tests/test_intermediate_integration.py @@ -121,16 +121,16 @@ def test_score_answer_cases(): ("x**2", {"variable": "x", "integrand": "2*x"}, 1.0), ("log(x)", {"variable": "x", "integrand": "1/x"}, 1.0), # Incorrect but properly formatted - ("x**3 + C", {"variable": "x", "integrand": "2*x"}, 0.05), - ("cos(X)", {"variable": "X", "integrand": "sin(X)"}, 0.05), + ("x**3 + C", {"variable": "x", "integrand": "2*x"}, 0.0), + ("cos(X)", {"variable": "X", "integrand": "sin(X)"}, 0.0), # Malformed expressions - ("x**2 +", {"variable": "x", "integrand": "2*x"}, 0.01), - ("sin(x", {"variable": "x", "integrand": "cos(x)"}, 0.01), + ("x**2 +", {"variable": "x", "integrand": "2*x"}, 0.0), + ("sin(x", {"variable": "x", "integrand": "cos(x)"}, 0.0), # Empty answer - ("", {"variable": "x", "integrand": "2*x"}, 0.01), + ("", {"variable": "x", "integrand": "2*x"}, 0.0), # Case sensitivity - ("x**2 + C", {"variable": "X", "integrand": "2*X"}, 0.05), - ("X**2 + C", {"variable": "x", "integrand": "2*x"}, 0.05), + ("x**2 + C", {"variable": "X", "integrand": "2*X"}, 0.0), + ("X**2 + C", {"variable": "x", "integrand": "2*x"}, 0.0), # Alternative constant notation ("x**2 + K", {"variable": "x", "integrand": "2*x"}, 1.0), ("sin(x) + D", {"variable": "x", "integrand": "cos(x)"}, 1.0), diff --git a/tests/test_knight_swap.py b/tests/test_knight_swap.py index 24f6b417..bea90d5e 100644 --- a/tests/test_knight_swap.py +++ b/tests/test_knight_swap.py @@ -155,8 +155,8 @@ def test_score_calculation(): # Test invalid answers assert dataset.score_answer(None, puzzle) == 0.0 - assert dataset.score_answer("", puzzle) == 0.01 - assert dataset.score_answer("Invalid", puzzle) == 0.01 + assert dataset.score_answer("", puzzle) == 0.0 + assert dataset.score_answer("Invalid", puzzle) == 0.0 # Test correct answer assert dataset.score_answer(puzzle["answer"], puzzle) == 1.0 diff --git a/tests/test_knights_knaves.py b/tests/test_knights_knaves.py index 0c595c0b..bcaf1fe7 100644 --- a/tests/test_knights_knaves.py +++ b/tests/test_knights_knaves.py @@ -99,8 +99,7 @@ def test_score_answer(): assert dataset.score_answer(correct_answer, problem) == 1.0 assert abs(dataset.score_answer(half_answer, problem) - 0.65) < 1e-10 assert dataset.score_answer(modified_answer, problem) == 1.0 - assert dataset.score_answer(wrong_answer, problem) == 0.01 - print("flipped") + assert dataset.score_answer(wrong_answer, problem) == 0.0 assert dataset.score_answer(flipped_answer, problem) == 1.0 diff --git a/tests/test_manipulate_matrix.py b/tests/test_manipulate_matrix.py index 2801340f..1a31af56 100644 --- a/tests/test_manipulate_matrix.py +++ b/tests/test_manipulate_matrix.py @@ -214,7 +214,7 @@ def test_manipulate_matrix_score_answer(): # incorrect answer answer = "1 2 3\n4 5 6\n7 8 8" - assert dataset.score_answer(answer, entry) == 0.01 + assert dataset.score_answer(answer, entry) == 0.0 # answer is none answer = None diff --git a/tests/test_n_queens.py b/tests/test_n_queens.py index 91373592..6182540b 100644 --- a/tests/test_n_queens.py +++ b/tests/test_n_queens.py @@ -117,7 +117,7 @@ def test_nqueens_score_answer(): # Test invalid answer gets score 0.01 invalid_answer = "_ _ _ _\n_ _ _ _\n_ _ _ _\n_ _ _ _" - assert dataset.score_answer(invalid_answer, item) == 0.01 + assert dataset.score_answer(invalid_answer, item) == 0.0 # Test None answer gets score 0.0 assert dataset.score_answer(None, item) == 0.0 diff --git a/tests/test_needle_haystack.py b/tests/test_needle_haystack.py index fb279da2..672d16b3 100644 --- a/tests/test_needle_haystack.py +++ b/tests/test_needle_haystack.py @@ -16,7 +16,7 @@ def test_needle_haystack(): # Test the scoring assert dataset.score_answer(answer=item["answer"], entry=item) == 1.0 - assert dataset.score_answer(answer="david bowie rules", entry=item) == 0.01 + assert dataset.score_answer(answer="david bowie rules", entry=item) == 0.0 assert dataset.score_answer(answer=None, entry=item) == 0.0 config = NeedleHaystackConfig(seed=42, size=1, num_statements=500) diff --git a/tests/test_number_format.py b/tests/test_number_format.py index 882f38aa..76ae834b 100644 --- a/tests/test_number_format.py +++ b/tests/test_number_format.py @@ -110,7 +110,7 @@ def test_number_format_answer(): # Incorrect answer (diff larger than 1e-2) model_answer = "54245.9" - assert dataset.score_answer(model_answer, entry) == 0.01 + assert dataset.score_answer(model_answer, entry) == 0.0 # Answer is null model_answer = None diff --git a/tests/test_palindrome.py b/tests/test_palindrome.py index 49e23786..472390de 100644 --- a/tests/test_palindrome.py +++ b/tests/test_palindrome.py @@ -84,8 +84,8 @@ def test_score_answer(): wrong_letters = "abcd" if "abcd" != correct_answer else "efgh" assert dataset.score_answer(wrong_letters, entry=item) == 0.02 - # Empty String input should score 0.01 - assert dataset.score_answer("", entry=item) == 0.01 + # Empty String input should score 0.0 + assert dataset.score_answer("", entry=item) == 0.0 # Empty input should score 0.0 assert dataset.score_answer(None, entry=item) == 0.0 diff --git a/tests/test_palindrome_partitioning.py b/tests/test_palindrome_partitioning.py index a81c44bf..7a0d386f 100644 --- a/tests/test_palindrome_partitioning.py +++ b/tests/test_palindrome_partitioning.py @@ -95,17 +95,17 @@ def test_palindrome_partitioning_score_answer(): item = {"metadata": {"solution": [["no", "on"], ["noon"], ["n", "o", "o", "n"]]}} assert dataset.score_answer(answer, item) == 1 - # Verify the score is 0.01 when incorrect + # Verify the score is 0.0 when incorrect answer = json.dumps([["n", "o", "o", "n"], ["no", "on"]]) item = {"metadata": {"solution": [["no", "on"], ["noon"], ["n", "o", "o", "n"]]}} - assert dataset.score_answer(answer, item) == 0.01 + assert dataset.score_answer(answer, item) == 0.0 - # Verify the score is 0 when answer is None + # Verify the score is 0.0 when answer is None answer = None item = {"metadata": {"solution": [["no", "on"], ["noon"], ["n", "o", "o", "n"]]}} - assert dataset.score_answer(answer, item) == 0 + assert dataset.score_answer(answer, item) == 0.0 - # Verify the score is 0 when answer is malformed JSON + # Verify the score is 0.0 when answer is malformed JSON answer = '["n", "o", "o", "n"], ["no", "on"], ["noon"]' item = {"metadata": {"solution": [["no", "on"], ["noon"], ["n", "o", "o", "n"]]}} - assert dataset.score_answer(answer, item) == 0 + assert dataset.score_answer(answer, item) == 0.0 diff --git a/tests/test_polynomial_equations.py b/tests/test_polynomial_equations.py index e4e72b18..f99e6424 100644 --- a/tests/test_polynomial_equations.py +++ b/tests/test_polynomial_equations.py @@ -122,11 +122,11 @@ def test_polynomial_solutions_evaluation(): "oracle_answer, predicted_answer, expected_reward", [ ("4,-4.12", "4,-4.12", 1.0), # Exact match - ("4,-4.12", "4.0001,-4.120001", approx(0.9999, rel=1e-3)), # Very close match - ("4,-4.12", "4.1,-4.2", approx(0.9139, rel=1e-3)), - ("4,8", "4", approx(0.9, rel=1e-3)), # Missing an oracle solution -> missing solution penalty applies - ("4", "4,8", approx(0.95, rel=1e-3)), # extra solution -> extra solution penalty - ("-1,-2", "1,4", approx(0.06890, rel=1e-3)), # -1 matched w/ 1 and -2 matched w/ 4 + ("4,-4.12", "4.0001,-4.120001", approx(0.9994, rel=1e-3)), # Very close match + ("4,-4.12", "4.1,-4.2", approx(0.4086, rel=1e-3)), + ("4,8", "4", approx(0.5, rel=1e-3)), # Missing an oracle solution -> missing solution penalty applies + ("4", "4,8", approx(0.5, rel=1e-3)), # extra solution -> extra solution penalty + ("-1,-2", "1,4", approx(1.0305e-9, rel=1e-3)), # -1 matched w/ 1 and -2 matched w/ 4 ("", "1", approx(0, rel=1e-4)), # oracle no solution, predicted extra solution ("1", "", approx(0, rel=1e-4)), # oracle has a solution, predicted no solution ], diff --git a/tests/test_polynomial_multiplication.py b/tests/test_polynomial_multiplication.py index 10b404d9..0ddf334a 100644 --- a/tests/test_polynomial_multiplication.py +++ b/tests/test_polynomial_multiplication.py @@ -92,7 +92,6 @@ def test_polynomial_equations_dataset_items(): # Check metadata assert isinstance(item["metadata"]["polynomial_expr"], str) - assert isinstance(item["metadata"]["result"], str) assert isinstance(item["metadata"]["variables"], list) # Check polynomial_expr existence @@ -127,42 +126,6 @@ def test_cross_polynomial_equations_dataset_items(): # Check metadata assert isinstance(item["metadata"]["polynomial_expr"], str) - assert isinstance(item["metadata"]["result"], str) - assert isinstance(item["metadata"]["variables"], list) - - # Check polynomial_expr existence - poly_str = item["metadata"]["polynomial_expr"] - # Ensure it can parse with sympy - sp.sympify(poly_str) - - -def test_cross_polynomial_equations_dataset_items(): - """Test that generated items have correct structure""" - ds = create_dataset( - "polynomial_multiplication", - min_terms=2, - max_terms=3, - min_value=1, - max_value=5, - min_degree=1, - max_degree=2, - min_polynomials=2, - max_polynomials=5, - variables=tuple("xyz"), - allow_cross_variable_product=True, - allow_multivariate_polynomials=False, - size=3, - seed=100, - ) - - for item in ds: - assert "question" in item - assert "answer" in item - assert "metadata" in item - - # Check metadata - assert isinstance(item["metadata"]["polynomial_expr"], str) - assert isinstance(item["metadata"]["result"], str) assert isinstance(item["metadata"]["variables"], list) # Check polynomial_expr existence @@ -197,7 +160,6 @@ def test_multivariate_polynomial_equations_dataset_items(): # Check metadata assert isinstance(item["metadata"]["polynomial_expr"], str) - assert isinstance(item["metadata"]["result"], str) assert isinstance(item["metadata"]["variables"], list) # Check polynomial_expr existence @@ -242,7 +204,7 @@ def test_polynomial_solutions_evaluation(): poly_expr = sp.expand(poly_str) # Verify that each solution satisfies the polynomial - assert poly_expr == item["answer"] + assert str(poly_expr) == item["answer"] def test_score_function(): @@ -266,11 +228,11 @@ def test_score_function(): for item in ds: poly_str = item["metadata"]["polynomial_expr"] - assert ds.score_answer(poly_str, item) == 0.05 + assert ds.score_answer(poly_str, item) == 0.0 poly_expr = str(sp.expand(poly_str)) assert ds.score_answer(poly_expr, item) == 1.0 - assert ds.score_answer(None, item) == 0.00 - assert ds.score_answer("Not a polynomial", item) == 0.01 - assert ds.score_answer("x**4", item) == 0.05 + assert ds.score_answer(None, item) == 0.0 + assert ds.score_answer("Not a polynomial", item) == 0.0 + assert ds.score_answer("x**4", item) == 0.0 diff --git a/tests/test_pool_matrix.py b/tests/test_pool_matrix.py index c110967f..a529c05f 100644 --- a/tests/test_pool_matrix.py +++ b/tests/test_pool_matrix.py @@ -143,7 +143,7 @@ def test_pool_matrix_score_answer(): dataset = PoolMatrixDataset(config) for entry in dataset: assert dataset.score_answer(entry["answer"], entry=entry) == 1 - assert 0.0 < dataset.score_answer("1 2.0\n3.0 4", entry=entry) <= 0.1 + assert dataset.score_answer("1 2.0\n3.0 4", entry=entry) in [0.0, 0.1] assert dataset.score_answer("one two three", entry=entry) == 0.0 assert dataset.score_answer("", entry=entry) == 0.0 assert dataset.score_answer(None, entry=entry) == 0.0 diff --git a/tests/test_power_function.py b/tests/test_power_function.py index 08d15826..df2454a4 100644 --- a/tests/test_power_function.py +++ b/tests/test_power_function.py @@ -71,7 +71,7 @@ def test_power_function_score_function(): # Answer is far from solution answer = str(item["metadata"]["solution"] - 1) - assert dataset.score_answer(answer, item) == 0.01 + assert dataset.score_answer(answer, item) == 0.0 # Answer is None answer = None diff --git a/tests/test_products.py b/tests/test_products.py index 469ae5fd..1e4d8dae 100644 --- a/tests/test_products.py +++ b/tests/test_products.py @@ -139,7 +139,7 @@ def test_products_scoring(): assert dataset.score_answer(item["answer"], item) == 1.0, "Exact match should score 1.0" # Test scoring with wrong answer - assert dataset.score_answer("wrong", item) == 0.01, "Wrong answer should score 0.01" + assert dataset.score_answer("wrong", item) == 0.0, "Wrong answer should score 0.0" # Test scoring with partial match (answer contained in response) assert ( diff --git a/tests/test_propositional_logic.py b/tests/test_propositional_logic.py index c73ab1cc..d708cc4a 100644 --- a/tests/test_propositional_logic.py +++ b/tests/test_propositional_logic.py @@ -100,4 +100,4 @@ def test_propositional_logic_dataset_score_answer_incorrect(): dataset = PropositionalLogicDataset(PropositionalLogicConfig(size=100, seed=101)) for i, item in enumerate(dataset): score = dataset.score_answer("Wrong", item) - assert score == 0.01 + assert score == 0.0 diff --git a/tests/test_quantum_lock.py b/tests/test_quantum_lock.py index 69162750..cf9693e6 100644 --- a/tests/test_quantum_lock.py +++ b/tests/test_quantum_lock.py @@ -43,7 +43,8 @@ def test_quantumlock_items(): assert "target_value" in item["metadata"] # Verify solution works - assert dataset.score_answer(answer=item["metadata"]["solution_path"], entry=item) == 1.0 + answer = "".join(item["metadata"]["solution_path"]) + assert dataset.score_answer(answer=answer, entry=item) == 1.0 assert dataset.score_answer(answer=None, entry=item) == 0.0 @@ -98,17 +99,17 @@ def test_quantumlock_scoring(): dataset = QuantumLockDataset(config) for item in dataset: - solution = item["metadata"]["solution_path"] + solution = item["answer"] # Test correct solution assert dataset.score_answer(solution, item) == 1.0 # Test empty/None answers assert dataset.score_answer(None, item) == 0.0 - assert dataset.score_answer("", item) == 0.1 + assert dataset.score_answer("", item) == 0.0 # Test invalid buttons - assert dataset.score_answer("XYZ", item) == 0.1 + assert dataset.score_answer("XYZ", item) == 0.0 # Test case insensitivity if solution: diff --git a/tests/test_ransom_note.py b/tests/test_ransom_note.py index f452ca2e..bf340424 100644 --- a/tests/test_ransom_note.py +++ b/tests/test_ransom_note.py @@ -86,7 +86,7 @@ def test_group_anagrams_dataset_items(): # Test the scoring assert dataset.score_answer(answer=item["answer"], entry=item) == 1.0 - assert dataset.score_answer(answer="gibberish", entry=item) == 0.01 + assert dataset.score_answer(answer="gibberish", entry=item) == 0.0 assert dataset.score_answer(answer=None, entry=item) == 0.0 diff --git a/tests/test_rearc.py b/tests/test_rearc.py index 17de2241..a23d8000 100644 --- a/tests/test_rearc.py +++ b/tests/test_rearc.py @@ -80,7 +80,7 @@ def test_rearc_scoring_edge_cases(): assert 0.0 < dataset.score_answer(partial, entry=item) < 1.0 # Malformed answer - assert dataset.score_answer("[[invalid", entry=item) == 0.01 + assert dataset.score_answer("[[invalid", entry=item) == 0.0 # Case sensitivity answer = format_board(item["metadata"]["output"], dataset.board_format_opts).lower() diff --git a/tests/test_self_reference.py b/tests/test_self_reference.py index 66f15081..0c582641 100644 --- a/tests/test_self_reference.py +++ b/tests/test_self_reference.py @@ -18,8 +18,8 @@ def test_self_reference(): # Test the scoring assert dataset.score_answer(answer=item["answer"], entry=item) == 1.0 - assert dataset.score_answer(answer=99, entry=item) == 0.1 - assert dataset.score_answer(answer="99", entry=item) == 0.1 + assert dataset.score_answer(answer=99, entry=item) == 0.0 + assert dataset.score_answer(answer="99", entry=item) == 0.0 assert dataset.score_answer(answer=None, entry=item) == 0.0 # # Medium @@ -34,8 +34,8 @@ def test_self_reference(): # Test the scoring assert dataset.score_answer(answer=item["answer"], entry=item) == 1.0 - assert dataset.score_answer(answer=99, entry=item) == 0.1 - assert dataset.score_answer(answer="99", entry=item) == 0.1 + assert dataset.score_answer(answer=99, entry=item) == 0.0 + assert dataset.score_answer(answer="99", entry=item) == 0.0 assert dataset.score_answer(answer=None, entry=item) == 0.0 # # Hard @@ -50,6 +50,6 @@ def test_self_reference(): # Test the scoring assert dataset.score_answer(answer=item["answer"], entry=item) == 1.0 - assert dataset.score_answer(answer=99, entry=item) == 0.1 - assert dataset.score_answer(answer="99", entry=item) == 0.1 + assert dataset.score_answer(answer=99, entry=item) == 0.0 + assert dataset.score_answer(answer="99", entry=item) == 0.0 assert dataset.score_answer(answer=None, entry=item) == 0.0 diff --git a/tests/test_shortest_path.py b/tests/test_shortest_path.py index f726b7cf..57950a1e 100644 --- a/tests/test_shortest_path.py +++ b/tests/test_shortest_path.py @@ -164,7 +164,7 @@ def test_shortest_path_answer(): ] }, } - assert dataset.score_answer("right down right down", entry) == 0.01 + assert dataset.score_answer("right down right down", entry) == 0.0 # Answer is None entry = { diff --git a/tests/test_simple_integration.py b/tests/test_simple_integration.py index 726b14be..994bf20f 100644 --- a/tests/test_simple_integration.py +++ b/tests/test_simple_integration.py @@ -94,16 +94,16 @@ def test_score_answer_cases(): ("x**2", {"variable": "x", "integrand": "2*x"}, 1.0), ("log(x)", {"variable": "x", "integrand": "1/x"}, 1.0), # Incorrect but properly formatted - ("x**3 + C", {"variable": "x", "integrand": "2*x"}, 0.05), - ("cos(X)", {"variable": "X", "integrand": "sin(X)"}, 0.05), + ("x**3 + C", {"variable": "x", "integrand": "2*x"}, 0.0), + ("cos(X)", {"variable": "X", "integrand": "sin(X)"}, 0.0), # Malformed expressions - ("x**2 +", {"variable": "x", "integrand": "2*x"}, 0.01), - ("sin(x", {"variable": "x", "integrand": "cos(x)"}, 0.01), + ("x**2 +", {"variable": "x", "integrand": "2*x"}, 0.0), + ("sin(x", {"variable": "x", "integrand": "cos(x)"}, 0.0), # Empty answer - ("", {"variable": "x", "integrand": "2*x"}, 0.01), + ("", {"variable": "x", "integrand": "2*x"}, 0.0), # Case sensitivity - ("x**2 + C", {"variable": "X", "integrand": "2*X"}, 0.05), - ("X**2 + C", {"variable": "x", "integrand": "2*x"}, 0.05), + ("x**2 + C", {"variable": "X", "integrand": "2*X"}, 0.0), + ("X**2 + C", {"variable": "x", "integrand": "2*x"}, 0.0), # Alternative constant notation ("x**2 + K", {"variable": "x", "integrand": "2*x"}, 1.0), ("sin(x) + D", {"variable": "x", "integrand": "cos(x)"}, 1.0), diff --git a/tests/test_sokoban.py b/tests/test_sokoban.py index c4d1e2b8..1e0e6502 100644 --- a/tests/test_sokoban.py +++ b/tests/test_sokoban.py @@ -6,6 +6,10 @@ def test_sokoban(): """Test basic properties and solution of generated items""" + dataset = SokobanDataset(SokobanConfig(size=10, seed=1234)) + for i, item in enumerate(dataset): + assert dataset.score_answer(answer=item["answer"], entry=item) == 1.0 + # Easy config = SokobanConfig(seed=42, size=20) dataset = SokobanDataset(config) @@ -18,11 +22,13 @@ def test_sokoban(): # Test the scoring assert dataset.score_answer(answer=item["answer"], entry=item) == 1.0 - assert dataset.score_answer(answer="RU", entry=item) == 0.1 + assert dataset.score_answer(answer="RU", entry=item) == 0.0 assert dataset.score_answer(answer=None, entry=item) == 0.0 - # Medium - config = SokobanConfig(seed=42, min_h=40, max_h=50, min_w=40, max_w=50, min_boxes=20, max_boxes=30, size=3) + # Hard + config = SokobanConfig( + seed=42, min_h=15, max_h=20, min_w=15, max_w=20, min_boxes=10, max_boxes=15, size=3, max_depth=90 + ) dataset = SokobanDataset(config) for item in dataset: @@ -35,8 +41,10 @@ def test_sokoban(): assert dataset.score_answer(answer=item["answer"], entry=item) == 1.0 assert dataset.score_answer(answer=None, entry=item) == 0.0 - # Hard - config = SokobanConfig(seed=42, min_h=400, max_h=500, min_w=400, max_w=500, min_boxes=50, max_boxes=50, size=1) + # min == max ranges + config = SokobanConfig( + seed=42, min_h=11, max_h=11, min_w=11, max_w=11, min_boxes=11, max_boxes=11, size=3, max_depth=60 + ) dataset = SokobanDataset(config) for item in dataset: diff --git a/tests/test_spiral_matrix.py b/tests/test_spiral_matrix.py index 333ecadd..9e5c510e 100644 --- a/tests/test_spiral_matrix.py +++ b/tests/test_spiral_matrix.py @@ -85,12 +85,12 @@ def test_spiral_matrix_answer(): # Score answer in list format (partially correct) entry = {"answer": "1 2 3 6 9 8 7 4 5"} answer = "[1, 2, 3, 6, 9, 8, 7, 4, 5]" - assert dataset.score_answer(answer, entry) == 0.5 + assert dataset.score_answer(answer, entry) == 0.1 # Answer is incorrect entry = {"answer": "1 2 3 6 9 8 7 4 5"} answer = "1 2 3" - assert dataset.score_answer(answer, entry) == 0.01 + assert dataset.score_answer(answer, entry) == 0.0 # Answer is none entry = {"answer": "1 2 3 6 9 8 7 4 5"} diff --git a/tests/test_string_insertion.py b/tests/test_string_insertion.py index 9d815b15..faff8d90 100644 --- a/tests/test_string_insertion.py +++ b/tests/test_string_insertion.py @@ -101,4 +101,4 @@ def test_string_insertion_answer(): # Test score_answer with correct answer as python list of characters (partial correct) answer = "['A', 'A', 'B', 'C', 'D', 'A', 'E', 'E', 'E', 'E', 'E', 'E', 'E', 'B', 'C', 'D', 'E', 'B', 'A', 'A', 'A', 'A', 'A']" entry = {"answer": "AABCDAEEEEEEEBCDEBAAAAA"} - assert dataset.score_answer(answer, entry) == 0.5 + assert dataset.score_answer(answer, entry) == 0.1 diff --git a/tests/test_tower_of_hanoi.py b/tests/test_tower_of_hanoi.py index 2d870078..01ae5d1d 100644 --- a/tests/test_tower_of_hanoi.py +++ b/tests/test_tower_of_hanoi.py @@ -78,7 +78,7 @@ def test_toh_dataset_items(): # Verify solution_length consistency assert solution_length == len( - item["answer"] + item["answer"].splitlines() ), f"Item {i} metadata 'solution_length' does not match actual number of moves." # Optional: Additional checks like verifying that start and target pegs are distinct @@ -94,8 +94,6 @@ def test_toh_move_validity(): num_disks = instance["metadata"]["num_disks"] num_pegs = instance["metadata"]["num_pegs"] start_peg = instance["metadata"]["start_peg"] - target_peg = instance["metadata"]["target_peg"] - auxiliary_pegs = instance["metadata"]["auxiliary_pegs"] pegs = list(range(1, num_pegs + 1)) # Initialize pegs_state: all disks start on the start peg @@ -104,7 +102,8 @@ def test_toh_move_validity(): pegs_state[start_peg].append(disk) # Iterate over each move and validate - for move_num, move in enumerate(instance["answer"], start=1): + moves = instance["answer"].splitlines() + for move_num, move in enumerate(moves, start=1): disk, from_peg, to_peg = parse_move(move) # Check that from_peg exists @@ -157,7 +156,7 @@ def test_toh_final_state_correct(): pegs_state[start_peg].append(disk) # Perform all moves - for move in instance["answer"]: + for move in instance["answer"].splitlines(): disk, from_peg, to_peg = parse_move(move) pegs_state[from_peg].pop() pegs_state[to_peg].append(disk) @@ -228,17 +227,15 @@ def is_valid_final_state(pegs_state: dict, target_peg: int, num_disks: int) -> b return target_stack == list(range(num_disks, 0, -1)) -def test_score_answer(): +def test_toh_score_answer(): """ Test that the score_answer method returns the expected reward values. Expected behavior: - Correct answer (i.e. equivalent in length, or better, than the one provided in the dataset item) gives 1.0. - A correct solution that is suboptimal length gives a proportional reward of optimal_move_count/user_move_count - - A badly formatted answer gives a minimal reward (0.01). - An answer that is syntactically valid but does not solve the puzzle gives a partial reward (0.05). - - An empty string gives 0.01. - - None gives 0.0. + - A badly formatted or empty answer gives a minimal reward (0.0). """ # Create a dataset instance using the default configuration. config = HanoiConfig(min_disks=3, max_disks=5, min_pegs=3, max_pegs=4, size=5, seed=42) @@ -253,17 +250,17 @@ def test_score_answer(): # 2. A badly formatted answer should yield minimal reward (0.01). score_bad_format = dataset.score_answer(answer="a wrong solution", entry=item) - assert score_bad_format == 0.01, f"Badly formatted answer score {score_bad_format} is not 0.01." + assert score_bad_format == 0.0, f"Badly formatted answer score {score_bad_format} is not 0.0" # 3. An answer that is validly formatted but unsolved. # For example, remove the last move from the correct answer. - unfinished_answer = correct_answer[:-1] + unfinished_answer = "\n".join(correct_answer.splitlines()[:-1]) score_unsolved = dataset.score_answer(answer=unfinished_answer, entry=item) assert score_unsolved == 0.05, f"Unsolved answer score {score_unsolved} is not 0.05." # 4. An empty answer should yield 0.01. score_empty = dataset.score_answer(answer="", entry=item) - assert score_empty == 0.01, f"Empty answer score {score_empty} is not 0.01." + assert score_empty == 0.0, f"Empty answer score {score_empty} is not 0.0." # 5. A None answer should yield 0.0. score_none = dataset.score_answer(answer=None, entry=item) diff --git a/tests/test_utils.py b/tests/test_utils.py index da3bafcd..f64544d9 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -50,4 +50,4 @@ def test_compute_decimal_reward(): # Test invalid answers assert compute_decimal_reward(None, "42") == 0.0 assert compute_decimal_reward("", "42") == 0.0 - assert compute_decimal_reward("not a number", "42") == 0.01 + assert compute_decimal_reward("not a number", "42") == 0.0 diff --git a/tests/test_word_ladder.py b/tests/test_word_ladder.py index 1aba4cf3..738d7939 100644 --- a/tests/test_word_ladder.py +++ b/tests/test_word_ladder.py @@ -380,20 +380,20 @@ def test_word_ladder_score_answer(): assert dataset.score_answer("COLD", entry) == 0.0 # Test wrong start word - assert dataset.score_answer("BOLD,CORD,CARD,WARD,WARM", entry) == 0.01 + assert dataset.score_answer("BOLD,CORD,CARD,WARD,WARM", entry) == 0.0 # Test wrong end word - assert dataset.score_answer("COLD,CORD,CARD,WARD,WARP", entry) == 0.01 + assert dataset.score_answer("COLD,CORD,CARD,WARD,WARP", entry) == 0.0 # Test wrong word length - assert dataset.score_answer("COLD,CORDS,CARDS,WARD,WARM", entry) == 0.01 + assert dataset.score_answer("COLD,CORDS,CARDS,WARD,WARM", entry) == 0.0 # Test invalid transitions (more than one letter change) - assert dataset.score_answer("COLD,WARD,WARM", entry) == 0.01 + assert dataset.score_answer("COLD,WARD,WARM", entry) == 0.0 # Test case insensitivity assert dataset.score_answer("cold,cord,card,ward,warm", entry) == 1.0 # Test with unknown words (should return partial credit) - assert dataset.score_answer("COLD,COXD,CARD,WARD,WARM", entry) < 1.0 - assert dataset.score_answer("COLD,COXD,CARD,WARD,WARM", entry) > 0.0 + assert dataset.score_answer("COLD,COXD,CORD,CARD,WARD,WARM", entry) < 1.0 + assert dataset.score_answer("COLD,COXD,CORD,CARD,WARD,WARM", entry) > 0.0 diff --git a/tests/test_word_sorting.py b/tests/test_word_sorting.py index 8920e814..802c1472 100644 --- a/tests/test_word_sorting.py +++ b/tests/test_word_sorting.py @@ -102,7 +102,7 @@ def test_word_sorting_dataset_items(): # Test the scoring assert dataset.score_answer(answer=item["answer"], entry=item) == 1.0 - assert dataset.score_answer(answer="gibberish", entry=item) == 0.01 + assert dataset.score_answer(answer="gibberish", entry=item) == 0.0 assert dataset.score_answer(answer=None, entry=item) == 0.0 @@ -143,7 +143,7 @@ def test_word_sorting_scoring(): # Garbage answer = "gibberish" - assert dataset.score_answer(answer, item) == 0.01 + assert dataset.score_answer(answer, item) == 0.0 # Empty answer answer = None From 8ecc72360709dae8aec41eccc86c8f6369dce219 Mon Sep 17 00:00:00 2001 From: Zafir Stojanovski Date: Wed, 5 Mar 2025 15:05:17 +0100 Subject: [PATCH 09/12] feat(env): NQueens Curriculum (#262) * curriculum & tests --- reasoning_gym/coaching/__init__.py | 3 ++- reasoning_gym/games/__init__.py | 4 +++- reasoning_gym/games/n_queens.py | 28 ++++++++++++++++++++++++++++ tests/test_n_queens.py | 27 ++++++++++++++++++++++++++- 4 files changed, 59 insertions(+), 3 deletions(-) diff --git a/reasoning_gym/coaching/__init__.py b/reasoning_gym/coaching/__init__.py index 50683d66..4d96bca4 100644 --- a/reasoning_gym/coaching/__init__.py +++ b/reasoning_gym/coaching/__init__.py @@ -1,10 +1,11 @@ -from .attributes import AttributeDefinition, AttributeType, RangeAttributeDefinition +from .attributes import AttributeDefinition, AttributeType, RangeAttributeDefinition, ScalarAttributeDefinition from .base_curriculum import BaseCurriculum from .coach import Coach, GroupedScores, ScoreBoard, ScoreStats __all__ = [ "AttributeType", "AttributeDefinition", + "ScalarAttributeDefinition", "RangeAttributeDefinition", "BaseCurriculum", "Coach", diff --git a/reasoning_gym/games/__init__.py b/reasoning_gym/games/__init__.py index 4b450134..f4d718b7 100644 --- a/reasoning_gym/games/__init__.py +++ b/reasoning_gym/games/__init__.py @@ -13,7 +13,7 @@ from .mahjong import MahjongPuzzleConfig, MahjongPuzzleDataset from .maze import MazeConfig, MazeDataset from .mini_sudoku import MiniSudokuConfig, MiniSudokuDataset -from .n_queens import NQueensDataset +from .n_queens import NQueensConfig, NQueensCurriculum, NQueensDataset from .rush_hour import RushHourConfig, RushHourDataset from .sokoban import SokobanConfig, SokobanDataset from .sudoku import SudokuConfig, SudokuDataset @@ -40,6 +40,8 @@ "HanoiConfig", "HanoiDataset", "NQueensDataset", + "NQueensConfig", + "NQueensCurriculum", "TsumegoConfig", "TsumegoDataset", "KnightSwapConfig", diff --git a/reasoning_gym/games/n_queens.py b/reasoning_gym/games/n_queens.py index 3a07bb10..723a4ef8 100644 --- a/reasoning_gym/games/n_queens.py +++ b/reasoning_gym/games/n_queens.py @@ -9,6 +9,7 @@ from random import Random from typing import Any, Optional +from ..coaching import AttributeType, BaseCurriculum, RangeAttributeDefinition, ScalarAttributeDefinition from ..factory import ProceduralDataset, register_dataset MIN_BOARD_SIZE = 4 @@ -151,4 +152,31 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: return 0.0 +class NQueensCurriculum(BaseCurriculum): + def __init__(self): + super().__init__(NQueensCurriculum.__name__, NQueensConfig) + + self._define_attributes( + ScalarAttributeDefinition( + name="n", + field_name="n", + levels=[4, 6, 8, 12], + default_level=0, + description="Board size", + attr_type=AttributeType.STATIC, + min_value=4, + ), + RangeAttributeDefinition( + name="num_removed", + levels=[2, 4, 6, 10], + default_level=0, + description="Number of queens to remove", + attr_type=AttributeType.APPEND, + min_value=1, + lower_field_name="min_remove", + upper_field_name="max_remove", + ), + ) + + register_dataset("n_queens", NQueensDataset, NQueensConfig) diff --git a/tests/test_n_queens.py b/tests/test_n_queens.py index 6182540b..0a9cc37d 100644 --- a/tests/test_n_queens.py +++ b/tests/test_n_queens.py @@ -2,7 +2,7 @@ import pytest -from reasoning_gym.games.n_queens import NQueensConfig, NQueensDataset +from reasoning_gym.games.n_queens import NQueensConfig, NQueensCurriculum, NQueensDataset def test_nqueens_config_validation(): @@ -146,3 +146,28 @@ def is_valid_solution(board: list[list[str]]) -> bool: off_diags.add(r - c) return num_queens == n + + +def test_n_queens_curriculum(): + curriculum = NQueensCurriculum() + + base_value = {"size": 150, "seed": 1} + + base_cfg: NQueensConfig = curriculum.generate_configuration(base_value) + assert base_cfg.seed == 1 + assert base_cfg.size == 150 + assert base_cfg.n == 4 + assert base_cfg.min_remove == 2 and base_cfg.max_remove == 2 + + # test incrementing attribute levels for n & num_removed attributes + curriculum.increment_attr_level("n") + curriculum.increment_attr_level("num_removed") + increased_cfg = curriculum.generate_configuration(base_value) + assert increased_cfg.n == 6 + assert increased_cfg.min_remove == 2 and increased_cfg.max_remove == 4 + + # test decrementing attribute level for n again + curriculum.decrement_attr_level("n") + partially_decreased_cfg = curriculum.generate_configuration(base_value) + assert partially_decreased_cfg.n == 4 + assert partially_decreased_cfg.min_remove == 2 and partially_decreased_cfg.max_remove == 4 From d0a42116fbd749aeb6add2b67b70e4744c566c4d Mon Sep 17 00:00:00 2001 From: Zafir Stojanovski Date: Wed, 5 Mar 2025 22:28:02 +0100 Subject: [PATCH 10/12] feat(env): Mahjong Puzzle Curriculum (#263) * mahjong curriculum * typo * update levels --- reasoning_gym/games/__init__.py | 3 ++- reasoning_gym/games/mahjong.py | 22 +++++++++++++++++++++- tests/test_mahjong_puzzle.py | 23 ++++++++++++++++++++++- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/reasoning_gym/games/__init__.py b/reasoning_gym/games/__init__.py index f4d718b7..2e8e79ef 100644 --- a/reasoning_gym/games/__init__.py +++ b/reasoning_gym/games/__init__.py @@ -10,7 +10,7 @@ from .emoji_mystery import EmojiMysteryConfig, EmojiMysteryDataset from .futoshiki import FutoshikiConfig, FutoshikiDataset from .knight_swap import KnightSwapConfig, KnightSwapDataset -from .mahjong import MahjongPuzzleConfig, MahjongPuzzleDataset +from .mahjong import MahjongPuzzleConfig, MahjongPuzzleCurriculum, MahjongPuzzleDataset from .maze import MazeConfig, MazeDataset from .mini_sudoku import MiniSudokuConfig, MiniSudokuDataset from .n_queens import NQueensConfig, NQueensCurriculum, NQueensDataset @@ -48,4 +48,5 @@ "KnightSwapDataset", "MahjongPuzzleConfig", "MahjongPuzzleDataset", + "MahjongPuzzleCurriculum", ] diff --git a/reasoning_gym/games/mahjong.py b/reasoning_gym/games/mahjong.py index 77071057..cbd653db 100644 --- a/reasoning_gym/games/mahjong.py +++ b/reasoning_gym/games/mahjong.py @@ -8,6 +8,7 @@ from random import Random from typing import Optional +from ..coaching import AttributeType, BaseCurriculum, RangeAttributeDefinition from ..factory import ProceduralDataset, register_dataset QUESTION_TEMPLATE = """There are several letter cards, and the game rules are as follows: @@ -38,7 +39,7 @@ class MahjongPuzzleConfig: def validate(self): """Validate configuration parameters""" - assert 1 <= self.min_num_rounds, "min_num_rounds must be reater than 0" + assert 1 <= self.min_num_rounds, "min_num_rounds must be greater than 0" assert self.min_num_rounds <= self.max_num_rounds, "min_num_rounds must be less than max_num_rounds" @@ -122,4 +123,23 @@ def __getitem__(self, idx: int) -> dict: } +class MahjongPuzzleCurriculum(BaseCurriculum): + def __init__(self): + super().__init__(MahjongPuzzleCurriculum.__name__, MahjongPuzzleConfig) + + # Define attributes + self._define_attributes( + RangeAttributeDefinition( + name="num_rounds", + levels=[10, 50, 100, 500], + default_level=0, + description="Number of rounds in the game", + attr_type=AttributeType.APPEND, + min_value=1, + lower_field_name="min_num_rounds", + upper_field_name="max_num_rounds", + ) + ) + + register_dataset("mahjong_puzzle", MahjongPuzzleDataset, MahjongPuzzleConfig) diff --git a/tests/test_mahjong_puzzle.py b/tests/test_mahjong_puzzle.py index f2f4f0b9..eaa0d9c0 100644 --- a/tests/test_mahjong_puzzle.py +++ b/tests/test_mahjong_puzzle.py @@ -4,7 +4,7 @@ import pytest -from reasoning_gym.games.mahjong import MahjongPuzzleConfig, MahjongPuzzleDataset +from reasoning_gym.games.mahjong import MahjongPuzzleConfig, MahjongPuzzleCurriculum, MahjongPuzzleDataset def test_mahjong_puzzle_config_validation(): @@ -95,3 +95,24 @@ def test_mahjong_puzzle_answer(): for c in string.ascii_lowercase: assert dataset._check_peng(cards, new_card=c) == False assert dataset._check_chi(cards, new_card=c) == False + + +def test_mahjong_puzzle_curriculum(): + curriculum = MahjongPuzzleCurriculum() + + base_value = {"size": 150, "seed": 1} + + base_cfg: MahjongPuzzleConfig = curriculum.generate_configuration(base_value) + assert base_cfg.seed == 1 + assert base_cfg.size == 150 + assert base_cfg.min_num_rounds == 10 and base_cfg.max_num_rounds == 10 + + # test incrementing attribute levels for num_rounds attribute + curriculum.increment_attr_level("num_rounds") + increased_cfg = curriculum.generate_configuration(base_value) + assert increased_cfg.min_num_rounds == 10 and increased_cfg.max_num_rounds == 50 + + # test incrementing again + curriculum.increment_attr_level("num_rounds") + increased_cfg = curriculum.generate_configuration(base_value) + assert increased_cfg.min_num_rounds == 10 and increased_cfg.max_num_rounds == 100 From e30be066ec85038e5f82917006986cd91bd7b8b7 Mon Sep 17 00:00:00 2001 From: joesharratt1229 <118444587+joesharratt1229@users.noreply.github.com> Date: Wed, 5 Mar 2025 22:30:12 +0100 Subject: [PATCH 11/12] Fixed `countdown` `score_answer` (#265) * fixed countdown score ans * checked solution uses all numbers --- reasoning_gym/games/countdown.py | 39 +++++++++++++++++--------------- tests/test_countdown.py | 21 ++++++++++++++++- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/reasoning_gym/games/countdown.py b/reasoning_gym/games/countdown.py index 751c67d7..270476be 100644 --- a/reasoning_gym/games/countdown.py +++ b/reasoning_gym/games/countdown.py @@ -1,3 +1,4 @@ +import re from dataclasses import dataclass from random import Random from typing import Any, Optional @@ -51,9 +52,9 @@ class CountdownDataset(ProceduralDataset): def __init__(self, config: CountdownConfig): self._prompt_templates = [ - "Using the numbers {numbers}, create an expression that equals {target}.\nYou can only use each number once.", - "Find a way to make {target} using some or all of these numbers: {numbers}.\nEach number can only be used once.", - "Calculate {target} using the numbers {numbers}.\nEach number may be used at most once.", + "Using all the numbers {numbers}, create an expression that equals {target}.\nYou can only use each number once.", + "Find a way to make {target} using all of these numbers: {numbers}.\nEach number can only be used once.", + "Calculate {target} using all of these numbers: {numbers}.\nEach number may be used at most once.", ] super().__init__(config=config, seed=config.seed, size=config.size) @@ -174,21 +175,23 @@ def _generate_expression(self, rng: Random) -> tuple[str, list[int], int]: def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: """Determine if the solution provided solves the problem""" - reward = 0.0 - metadata = entry["metadata"] - if answer is not None: - try: - user_answer = int(parse_expr(answer)) - solved = user_answer == metadata["target"] - if solved: - reward = 1.0 - elif len(answer.strip()) > 0: # encourage partial solutions - reward = 0.05 - else: - reward = 0.01 - except: - reward = 0.01 - return reward + reward = 0.01 # Default reward + + if answer is None or not answer.strip(): + return reward + + try: + answer = answer.strip() + user_answer = int(parse_expr(answer)) + used_numbers = [int(num) for num in re.findall(r"\b\d+\b", answer)] + target_numbers = set(entry["metadata"]["numbers"]) + + if (user_answer == entry["metadata"]["target"]) and (set(used_numbers) == target_numbers): + return 1.0 + + return 0.05 if answer else 0.01 + except Exception: + return 0.01 # Register the dataset diff --git a/tests/test_countdown.py b/tests/test_countdown.py index 2bd20f4f..6bfc0cbf 100644 --- a/tests/test_countdown.py +++ b/tests/test_countdown.py @@ -72,7 +72,7 @@ def test_countdown_game_items(): dataset.score_answer(answer="a wrong solution", entry=item) == 0.01 ) # wrong answer but incorrectly formatted assert dataset.score_answer(answer="", entry=item) == 0.01 # wrong answer but empty string - assert dataset.score_answer(answer=None, entry=item) == 0.0 # no answer + assert dataset.score_answer(answer=None, entry=item) == 0.01 # no answer try: result = eval(expr) # Safe here since we control expression generation @@ -81,6 +81,25 @@ def test_countdown_game_items(): pytest.fail(f"Invalid expression generated: {expr}") +def test_answer_with_incorrect_numbers(): + dataset = CountdownDataset(CountdownConfig(size=10, seed=42)) + answer = "45+2" + item = { + "metadata": { + "numbers": [44, 3], + "target": 47, + } + } + assert dataset.score_answer(answer=answer, entry=item) == 0.05 + + +def test_answer_without_all_numbers(): + dataset = CountdownDataset(CountdownConfig(size=10, seed=42)) + answer = "45+2+3" + item = {"metadata": {"numbers": [1, 45, 2, 3], "target": 50}} + assert dataset.score_answer(answer=answer, entry=item) == 0.05 + + def test_countdown_game_randomization(): """Test number randomization configuration""" config = CountdownConfig(min_numbers=4, max_numbers=4, shuffle=False, size=10, seed=42) # Fixed size for testing From d1e505a8e9b9f008f1166f80b507625b2706bb05 Mon Sep 17 00:00:00 2001 From: Oliver Stanley Date: Wed, 5 Mar 2025 21:34:11 +0000 Subject: [PATCH 12/12] First version of CodeI/O reasoning data (#264) * notebook for prepping first set of raw code files * updated codeio processing notebook for repo-level processing * fix for edge case in codeio scoring * Add reformat notebook * filtering pass * add non-determinism filtering * Tweak CodeIODataset & include first real data * add basic codeio test, metadata --- notebooks/codeio/.gitignore | 1 + notebooks/codeio/PreprocessCode.ipynb | 377 +++++++++--- notebooks/codeio/ReformatAndFilter.ipynb | 694 +++++++++++++++++++++++ reasoning_gym/code/codeio.py | 38 +- reasoning_gym/data/codeio.jsonl.gz | Bin 503 -> 186614 bytes tests/test_codeio.py | 42 ++ 6 files changed, 1050 insertions(+), 102 deletions(-) create mode 100644 notebooks/codeio/ReformatAndFilter.ipynb create mode 100644 tests/test_codeio.py diff --git a/notebooks/codeio/.gitignore b/notebooks/codeio/.gitignore index 92ac9e52..6570bb8f 100644 --- a/notebooks/codeio/.gitignore +++ b/notebooks/codeio/.gitignore @@ -1 +1,2 @@ raw_files/ +output/ diff --git a/notebooks/codeio/PreprocessCode.ipynb b/notebooks/codeio/PreprocessCode.ipynb index 921fc201..428bf15f 100644 --- a/notebooks/codeio/PreprocessCode.ipynb +++ b/notebooks/codeio/PreprocessCode.ipynb @@ -24,6 +24,54 @@ "cell_type": "code", "execution_count": 1, "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Cloning into 'Python'...\n", + "remote: Enumerating objects: 20925, done.\u001b[K\n", + "remote: Counting objects: 100% (13/13), done.\u001b[K\n", + "remote: Compressing objects: 100% (11/11), done.\u001b[K\n", + "remote: Total 20925 (delta 6), reused 2 (delta 2), pack-reused 20912 (from 3)\u001b[K\n", + "Receiving objects: 100% (20925/20925), 14.86 MiB | 17.27 MiB/s, done.\n", + "Resolving deltas: 100% (13469/13469), done.\n" + ] + } + ], + "source": [ + "!git clone https://github.com/TheAlgorithms/Python.git\n", + "\n", + "import shutil\n", + "from pathlib import Path\n", + "\n", + "repo_dir = Path(\"Python\")\n", + "raw_code_dir = Path(\"raw_files\")\n", + "raw_code_dir.mkdir(exist_ok=True)\n", + "\n", + "def process_dir(directory: Path):\n", + " # Move all the Python code files to the raw code file directory\n", + " # Handles subdirectories recursively\n", + " dirname = directory.name\n", + " for file in directory.iterdir():\n", + " if file.is_dir():\n", + " process_dir(file)\n", + " elif file.name.endswith(\".py\") and file.name != \"__init__.py\":\n", + " file.rename(raw_code_dir / f\"{dirname}_{file.name}\")\n", + "\n", + "for repo_child in repo_dir.iterdir():\n", + " # For this repo, algorithms are divided into categories by subdirectories\n", + " if not repo_child.is_dir() or repo_child.name.startswith(\".\"):\n", + " continue\n", + " process_dir(repo_child)\n", + "\n", + "shutil.rmtree(repo_dir)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, "outputs": [], "source": [ "import random\n", @@ -42,7 +90,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -87,12 +135,12 @@ "\n", "4. Python 3.11 code for an input generator, which randomly generates valid sets of inputs for the functions.\n", "\n", - "The input generator should return a dict mapping parameter names to values. The values should be randomly generated, but should be valid inputs for the function.\n", + "The input generator should return a dict mapping parameter names to values. The values should be randomly generated, but should be valid inputs for the function. You have access to `random` in the input generator. Do not import any other modules.\n", "\n", "Example input generator:\n", "\n", "def input_generator():\n", - " weights = [np.random.uniform(0, 100) for _ in range(40)]\n", + " weights = [random.randint(100) for _ in range(40)]\n", " days = list(range(40))\n", " return {{\"weights_kg\": weights, \"days\": days}}\n", "\n", @@ -104,85 +152,152 @@ "\"\"\"" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Edit the below cell or appropriate env variables to utilise different API providers, etc" + ] + }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 19, "metadata": {}, "outputs": [], "source": [ + "import asyncio\n", "import os\n", - "import time\n", - "from openai import OpenAI\n", + "from openai import AsyncOpenAI\n", "from openai.types.chat import ChatCompletion, ChatCompletionMessageParam\n", "from typing import Any, Iterable\n", "\n", - "def llm_generate(\n", - " client: OpenAI,\n", + "# Cap concurrent requests. I had to set this to 1 for the DeepSeek API to work, YMMV\n", + "semaphore = asyncio.Semaphore(1)\n", + "\n", + "async def llm_generate(\n", + " client: AsyncOpenAI,\n", " messages: Iterable[ChatCompletionMessageParam],\n", " sampling_params: dict[str, Any],\n", + " retry_empty_response: bool = True,\n", + " max_retries: int = 3,\n", ") -> ChatCompletion:\n", - " max_retry = 3\n", - " for trial in range(max_retry):\n", - " try:\n", - " return client.chat.completions.create(\n", - " messages=messages,\n", - " **sampling_params,\n", - " )\n", - " except Exception as e:\n", - " print(\"failure response:\", e)\n", - " time.sleep(trial * trial) # quadratic backoff\n", - " if trial == max_retry - 1:\n", - " raise\n", - "\n", - "open_router_client = OpenAI(\n", - " base_url=\"https://openrouter.ai/api/v1\",\n", - " api_key=os.getenv(\"OPENROUTER_API_KEY\"),\n", - " timeout=90.0,\n", + " for trial in range(max_retries):\n", + " async with semaphore:\n", + " try:\n", + " completion = await client.chat.completions.create(\n", + " messages=messages, **sampling_params\n", + " )\n", + " if completion.choices[0].message.content or not retry_empty_response:\n", + " return completion\n", + " await asyncio.sleep(5)\n", + " except Exception as e:\n", + " print(f\"Failure response (trial {trial}):\", e)\n", + " await asyncio.sleep(3 * (trial + 1))\n", + " if trial == max_retries - 1:\n", + " raise\n", + "\n", + "client = AsyncOpenAI(\n", + " base_url=os.getenv(\"API_BASE_URL\"),\n", + " api_key=os.getenv(\"API_KEY\"),\n", + " timeout=120.0,\n", ")\n", "\n", "sampling_params = {\n", - " \"model\": \"deepseek/deepseek-chat:free\",\n", + " \"model\": \"deepseek-chat\", # For DeepSeek API\n", + " #\"model\": \"deepseek/deepseek-chat:free\", # For OpenRouter\n", " \"max_tokens\": 8192,\n", "}" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Demo cell to illustrate the LLM preprocessing:" + ] + }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "raw_files/climbing_stairs.py\n", - "def main(number_of_steps):\n", - " assert isinstance(number_of_steps, int) and number_of_steps > 0, (\n", - " f\"number_of_steps needs to be positive integer, your input {number_of_steps}\"\n", - " )\n", - " if number_of_steps == 1:\n", - " return {\"distinct_ways\": 1}\n", - " previous, current = 1, 1\n", - " for _ in range(number_of_steps - 1):\n", - " current, previous = current + previous, current\n", - " return {\"distinct_ways\": current}\n", + "raw_files/genetic_algorithm_basic_string.py\n", + "def main(target: str, genes: list[str], debug: bool = True) -> dict:\n", + " if N_POPULATION < N_SELECTED:\n", + " raise ValueError(f\"{N_POPULATION} must be bigger than {N_SELECTED}\")\n", + " \n", + " not_in_genes_list = sorted({c for c in target if c not in genes})\n", + " if not_in_genes_list:\n", + " raise ValueError(f\"{not_in_genes_list} is not in genes list, evolution cannot converge\")\n", + " \n", + " population = []\n", + " for _ in range(N_POPULATION):\n", + " population.append(\"\".join([random.choice(genes) for _ in range(len(target))]))\n", + " \n", + " generation, total_population = 0, 0\n", + " \n", + " while True:\n", + " generation += 1\n", + " total_population += len(population)\n", + " \n", + " population_score = [evaluate(item, target) for item in population]\n", + " population_score = sorted(population_score, key=lambda x: x[1], reverse=True)\n", + " \n", + " if population_score[0][0] == target:\n", + " return {\n", + " \"generation\": generation,\n", + " \"total_population\": total_population,\n", + " \"best_match\": population_score[0][0]\n", + " }\n", + " \n", + " if debug and generation % 10 == 0:\n", + " print(\n", + " f\"\\nGeneration: {generation}\"\n", + " f\"\\nTotal Population:{total_population}\"\n", + " f\"\\nBest score: {population_score[0][1]}\"\n", + " f\"\\nBest string: {population_score[0][0]}\"\n", + " )\n", + " \n", + " population_best = population[: int(N_POPULATION / 3)]\n", + " population.clear()\n", + " population.extend(population_best)\n", + " population_score = [\n", + " (item, score / len(target)) for item, score in population_score\n", + " ]\n", + " \n", + " for i in range(N_SELECTED):\n", + " population.extend(select(population_score[int(i)], population_score, genes))\n", + " if len(population) > N_POPULATION:\n", + " break\n", "\n", "---\n", - "You are given an integer `number_of_steps` representing the number of steps on a staircase. Your task is to calculate the number of distinct ways to climb the staircase, where each time you can either climb 1 or 2 steps. Return the number of distinct ways as an integer.\n", + "\n", + "You are given a target string and a list of genes. The target string represents the desired output of a genetic algorithm, and the genes list contains the possible characters that can be used to build the target string. The genetic algorithm works in phases: evaluation, selection, crossover, and mutation. The algorithm starts with a random population of strings and evolves them over generations to converge towards the target string. The function returns the number of generations it took to find a perfect match, the total population size processed, and the best matching string found.\n", "\n", "---\n", + "\n", "Input:\n", - " number_of_steps (int): The number of steps on the staircase. Must be a positive integer.\n", + " target (str): The target string that the genetic algorithm aims to converge to.\n", + " genes (list of str): A list of characters that can be used to build the target string.\n", + " debug (bool, optional): If True, prints progress every 10 generations. Defaults to True.\n", "\n", "Output:\n", - " return (dict): A dictionary with one key:\n", - " - distinct_ways (int): The number of distinct ways to climb the staircase.\n", + " return (dict): A dictionary with three keys:\n", + " - generation (int): The number of generations it took to find a perfect match.\n", + " - total_population (int): The total population size processed during the evolution.\n", + " - best_match (str): The best matching string found.\n", "\n", "---\n", + "\n", "def input_generator():\n", - " import random\n", - " number_of_steps = random.randint(1, 100)\n", - " return {\"number_of_steps\": number_of_steps}\n" + " genes = list(\" ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.,;!?+-*#@^'èéòà€ù=)(&%$£/\\\\\")\n", + " target_length = random.randint(10, 50)\n", + " target = \"\".join(random.choices(genes, k=target_length))\n", + " return {\"target\": target, \"genes\": genes, \"debug\": random.choice([True, False])}\n" ] } ], @@ -199,64 +314,156 @@ " {\"role\": \"user\", \"content\": prompt},\n", "]\n", "\n", - "response = llm_generate(open_router_client, messages, sampling_params)\n", + "response = await llm_generate(client, messages, sampling_params)\n", "print(response.choices[0].message.content)" ] }, { - "cell_type": "code", - "execution_count": 13, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "code, query, parameters, generator = response.choices[0].message.content.split(\"\\n---\\n\")" + "Run the below cell to preprocess all the raw code files for real. This will send quite a lot of requests to OpenRouter." ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Failure response (trial 1): Expecting value: line 1 column 1 (char 0)\n", + "Error processing file raw_files/graphs_page_rank.py Expecting value: line 1 column 1 (char 0)\n", + "Failure response (trial 1): Expecting value: line 1 column 1 (char 0)\n", + "Error processing file raw_files/problem_002_sol2.py Expecting value: line 1 column 1 (char 0)\n" + ] + } + ], "source": [ - "The below cell executes arbitrary code, so be careful with what you run." + "import json\n", + "from tqdm import tqdm\n", + "\n", + "async def process_file(raw_file):\n", + " raw_code = raw_file.read_text()\n", + " prompt = format_prompt_template.format(raw_code)\n", + " messages = [{\"role\": \"user\", \"content\": prompt}]\n", + "\n", + " try:\n", + " response = await llm_generate(client, messages, sampling_params)\n", + " content = response.choices[0].message.content\n", + " code, query, parameters, generator = [el.strip() for el in content.split(\"\\n---\\n\")]\n", + " return code, query, parameters, generator\n", + " except Exception as e:\n", + " print(\"Error processing file\", raw_file, e)\n", + "\n", + "async def process_all_files(raw_code_files: list[Path], out_file: Path):\n", + " process_tasks = []\n", + " for raw_file in raw_code_files:\n", + " process_tasks.append(asyncio.create_task(process_file(raw_file)))\n", + " for future in tqdm(asyncio.as_completed(process_tasks), total=len(process_tasks)):\n", + " code, query, parameters, generator = await future\n", + " out_object = {\"query\": query, \"reference_code\": code, \"parameters\": parameters, \"input_generator\": generator}\n", + " out_json = json.dumps(out_object)\n", + " with out_file.open(\"a\") as f:\n", + " f.write(out_json + \"\\n\")\n", + "\n", + "out_file = Path(\"processed_code.jsonl\")\n", + "await process_all_files(raw_files, out_file)" ] }, { - "cell_type": "code", - "execution_count": 14, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "def generate_io_pairs(main_code: str, input_generator_code: str, num_pairs: int = 100):\n", - " local_vars = {}\n", - " exec(main_code, {}, local_vars)\n", - " exec(input_generator_code, {}, local_vars)\n", - " io_pairs = []\n", - " for _ in range(num_pairs):\n", - " inputs = local_vars[\"input_generator\"]()\n", - " outputs = local_vars[\"main\"](**inputs)\n", - " io_pairs.append((inputs, outputs))\n", - " return io_pairs\n", + "Load one of the processed outputs to test the reference code and input generator.\n", "\n", - "io_pairs = generate_io_pairs(code, generator, num_pairs=2)" + "The below cell executes the loaded LLM-generated code, so exercise caution." ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": {}, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'particles': [{'x': 46.08733176390575, 'y': -79.53711508439847, 'z': 45.779499438274655, 'mass': 9.121897656796}, {'x': -37.62801734935914, 'y': 94.62608762267024, 'z': -88.900444530177, 'mass': 13.267310061939007}, {'x': 57.04088821817467, 'y': 42.54071907694012, 'z': -73.71739928081027, 'mass': 33.13376982254907}, {'x': -25.913090702690695, 'y': 97.27894813174453, 'z': -68.24577317209872, 'mass': 20.409856607552626}, {'x': -7.993371736001535, 'y': 5.784333365689022, 'z': 82.05216927454009, 'mass': 97.18903185914192}, {'x': 8.028265944329263, 'y': -16.980411042271342, 'z': -38.28350230155666, 'mass': 68.56437969046345}, {'x': 72.19027810108415, 'y': 40.80441736137902, 'z': -27.381163108822662, 'mass': 31.705269244558238}]}\n", + "{'particles': [{'x': -82.51989169298639, 'y': 79.31892816610184, 'z': 74.79703074246333, 'mass': 8.173913842116992}, {'x': 40.50078366091543, 'y': -81.62144939582438, 'z': -90.67215023121767, 'mass': 69.66013035036612}, {'x': 23.07410631316951, 'y': 52.57873390089097, 'z': -77.63883105258888, 'mass': 63.20676872636796}]}\n" + ] + }, { "data": { "text/plain": [ - "[({'number_of_steps': 65}, {'distinct_ways': 27777890035288}),\n", - " ({'number_of_steps': 19}, {'distinct_ways': 6765})]" + "[({'particles': [{'x': 46.08733176390575,\n", + " 'y': -79.53711508439847,\n", + " 'z': 45.779499438274655,\n", + " 'mass': 9.121897656796},\n", + " {'x': -37.62801734935914,\n", + " 'y': 94.62608762267024,\n", + " 'z': -88.900444530177,\n", + " 'mass': 13.267310061939007},\n", + " {'x': 57.04088821817467,\n", + " 'y': 42.54071907694012,\n", + " 'z': -73.71739928081027,\n", + " 'mass': 33.13376982254907},\n", + " {'x': -25.913090702690695,\n", + " 'y': 97.27894813174453,\n", + " 'z': -68.24577317209872,\n", + " 'mass': 20.409856607552626},\n", + " {'x': -7.993371736001535,\n", + " 'y': 5.784333365689022,\n", + " 'z': 82.05216927454009,\n", + " 'mass': 97.18903185914192},\n", + " {'x': 8.028265944329263,\n", + " 'y': -16.980411042271342,\n", + " 'z': -38.28350230155666,\n", + " 'mass': 68.56437969046345},\n", + " {'x': 72.19027810108415,\n", + " 'y': 40.80441736137902,\n", + " 'z': -27.381163108822662,\n", + " 'mass': 31.705269244558238}]},\n", + " {'center_of_mass': {'x': 12.23, 'y': 16.89, 'z': -0.42}}),\n", + " ({'particles': [{'x': -82.51989169298639,\n", + " 'y': 79.31892816610184,\n", + " 'z': 74.79703074246333,\n", + " 'mass': 8.173913842116992},\n", + " {'x': 40.50078366091543,\n", + " 'y': -81.62144939582438,\n", + " 'z': -90.67215023121767,\n", + " 'mass': 69.66013035036612},\n", + " {'x': 23.07410631316951,\n", + " 'y': 52.57873390089097,\n", + " 'z': -77.63883105258888,\n", + " 'mass': 63.20676872636796}]},\n", + " {'center_of_mass': {'x': 25.56, 'y': -12.15, 'z': -75.24}})]" ] }, - "execution_count": 15, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ + "rng = random.Random()\n", + "\n", + "sample_object = json.loads(out_file.read_text().splitlines()[0])\n", + "\n", + "def generate_io_pairs(main_code: str, input_generator_code: str, num_pairs: int = 100):\n", + " local_vars = {\"random\": rng}\n", + " exec(main_code, {\"random\": rng}, local_vars)\n", + " exec(input_generator_code, {\"random\": rng}, local_vars)\n", + " io_pairs = []\n", + " for _ in range(num_pairs):\n", + " inputs = local_vars[\"input_generator\"]()\n", + " outputs = local_vars[\"main\"](**inputs)\n", + " io_pairs.append((inputs, outputs))\n", + " return io_pairs\n", + "\n", + "io_pairs = generate_io_pairs(sample_object[\"reference_code\"], sample_object[\"input_generator\"], num_pairs=2)\n", "io_pairs" ] }, @@ -264,16 +471,16 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Next we need to synthesize chains of thought from the LLM for use in building a supervised finetuning dataset. From the paper:\n", + "Next in the paper they synthesized chains of thought from the LLM for use in building a supervised finetuning dataset. Excerpt:\n", "\n", "> Since we aim for the input-output prediction tasks, we construct the prompt using a designed template to combine the function, the query, the reference code, and either a specific input or output. The response should ideally be a natural language CoT to reason about how to derive the correct output or a feasible input.\n", "\n", - "The below prompts are from the paper." + "The below prompts are also from the paper. Synthesized chains of thought are not our main goal, but the cells below provide a demo nonetheless." ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 20, "metadata": {}, "outputs": [], "source": [ @@ -312,56 +519,56 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'To determine the input `number_of_steps` that results in the output `{\\'distinct_ways\\': 27777890035288}`, we need to understand that this problem is related to the Fibonacci sequence. Specifically, the number of distinct ways to climb `n` steps, where you can climb either 1 or 2 steps at a time, is equal to the `(n+1)`-th Fibonacci number.\\n\\nGiven the output `27777890035288`, we need to find the integer `n` such that the `(n+1)`-th Fibonacci number is `27777890035288`.\\n\\nThe Fibonacci sequence grows exponentially, and the number `27777890035288` is a very large Fibonacci number. To find the corresponding `n`, we can use the fact that the Fibonacci sequence follows the recurrence relation:\\n\\n\\\\[ F(n) = F(n-1) + F(n-2) \\\\]\\n\\nGiven that `F(73) = 806515533049393` and `F(72) = 498454011879264`, it is clear that `27777890035288` is much smaller than `F(73)`. We need to find the exact `n` such that `F(n+1) = 27777890035288`.\\n\\nHowever, calculating Fibonacci numbers manually for large `n` is impractical. Instead, we can use the fact that `F(75) = 2111485077978050`, which is larger than `27777890035288`. Therefore, the `n` we are looking for must be between 72 and 75.\\n\\nBy checking Fibonacci numbers closer to `27777890035288`, we find that:\\n\\n\\\\[ F(74) = 1304969544928657 \\\\]\\n\\\\[ F(75) = 2111485077978050 \\\\]\\n\\nSince `27777890035288` is significantly larger than `F(74)` but smaller than `F(75)`, it is clear that `n` is 74.\\n\\nThus, the input `number_of_steps` should be 74, which corresponds to `F(75) = 27777890035288`.\\n\\nTherefore, the feasible input is:\\n\\n```json\\n{\"number_of_steps\": 74}\\n```'" + "\"To predict a feasible input that would result in the given output `{'center_of_mass': {'x': 12.23, 'y': 16.89, 'z': -0.42}}`, we need to consider the formula for calculating the center of mass in 3D space. The center of mass is calculated as the weighted average of the positions of the particles, where the weights are the masses of the particles.\\n\\nThe formula for the center of mass is:\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_x} = \\\\frac{\\\\sum (x_i \\\\cdot m_i)}{\\\\sum m_i}\\n\\\\]\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_y} = \\\\frac{\\\\sum (y_i \\\\cdot m_i)}{\\\\sum m_i}\\n\\\\]\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_z} = \\\\frac{\\\\sum (z_i \\\\cdot m_i)}{\\\\sum m_i}\\n\\\\]\\n\\nGiven the output, we can work backward to estimate the input. Let's assume we have two particles for simplicity:\\n\\n1. **Particle 1**:\\n - Position: \\\\(x_1 = 10.0\\\\), \\\\(y_1 = 15.0\\\\), \\\\(z_1 = 0.0\\\\)\\n - Mass: \\\\(m_1 = 2.0\\\\)\\n\\n2. **Particle 2**:\\n - Position: \\\\(x_2 = 14.0\\\\), \\\\(y_2 = 18.0\\\\), \\\\(z_2 = -1.0\\\\)\\n - Mass: \\\\(m_2 = 3.0\\\\)\\n\\nNow, let's calculate the center of mass using these values:\\n\\n\\\\[\\n\\\\text{total\\\\_mass} = m_1 + m_2 = 2.0 + 3.0 = 5.0\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_x} = \\\\frac{(10.0 \\\\cdot 2.0) + (14.0 \\\\cdot 3.0)}{5.0} = \\\\frac{20.0 + 42.0}{5.0} = \\\\frac{62.0}{5.0} = 12.4\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_y} = \\\\frac{(15.0 \\\\cdot 2.0) + (18.0 \\\\cdot 3.0)}{5.0} = \\\\frac{30.0 + 54.0}{5.0} = \\\\frac{84.0}{5.0} = 16.8\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_z} = \\\\frac{(0.0 \\\\cdot 2.0) + (-1.0 \\\\cdot 3.0)}{5.0} = \\\\frac{0.0 - 3.0}{5.0} = \\\\frac{-3.0}{5.0} = -0.6\\n\\\\]\\n\\nThese values are close to the given output, but not exact. To get closer to the exact output, we can adjust the masses slightly:\\n\\n1. **Particle 1**:\\n - Position: \\\\(x_1 = 10.0\\\\), \\\\(y_1 = 15.0\\\\), \\\\(z_1 = 0.0\\\\)\\n - Mass: \\\\(m_1 = 2.1\\\\)\\n\\n2. **Particle 2**:\\n - Position: \\\\(x_2 = 14.0\\\\), \\\\(y_2 = 18.0\\\\), \\\\(z_2 = -1.0\\\\)\\n - Mass: \\\\(m_2 = 3.0\\\\)\\n\\nNow, let's recalculate:\\n\\n\\\\[\\n\\\\text{total\\\\_mass} = m_1 + m_2 = 2.1 + 3.0 = 5.1\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_x} = \\\\frac{(10.0 \\\\cdot 2.1) + (14.0 \\\\cdot 3.0)}{5.1} = \\\\frac{21.0 + 42.0}{5.1} = \\\\frac{63.0}{5.1} \\\\approx 12.35\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_y} = \\\\frac{(15.0 \\\\cdot 2.1) + (18.0 \\\\cdot 3.0)}{5.1} = \\\\frac{31.5 + 54.0}{5.1} = \\\\frac{85.5}{5.1} \\\\approx 16.76\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_z} = \\\\frac{(0.0 \\\\cdot 2.1) + (-1.0 \\\\cdot 3.0)}{5.1} = \\\\frac{0.0 - 3.0}{5.1} = \\\\frac{-3.0}{5.1} \\\\approx -0.59\\n\\\\]\\n\\nThese values are closer to the given output. To match the exact output, we can further adjust the masses:\\n\\n1. **Particle 1**:\\n - Position: \\\\(x_1 = 10.0\\\\), \\\\(y_1 = 15.0\\\\), \\\\(z_1 = 0.0\\\\)\\n - Mass: \\\\(m_1 = 2.2\\\\)\\n\\n2. **Particle 2**:\\n - Position: \\\\(x_2 = 14.0\\\\), \\\\(y_2 = 18.0\\\\), \\\\(z_2 = -1.0\\\\)\\n - Mass: \\\\(m_2 = 3.0\\\\)\\n\\nNow, let's recalculate:\\n\\n\\\\[\\n\\\\text{total\\\\_mass} = m_1 + m_2 = 2.2 + 3.0 = 5.2\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_x} = \\\\frac{(10.0 \\\\cdot 2.2) + (14.0 \\\\cdot 3.0)}{5.2} = \\\\frac{22.0 + 42.0}{5.2} = \\\\frac{64.0}{5.2} \\\\approx 12.31\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_y} = \\\\frac{(15.0 \\\\cdot 2.2) + (18.0 \\\\cdot 3.0)}{5.2} = \\\\frac{33.0 + 54.0}{5.2} = \\\\frac{87.0}{5.2} \\\\approx 16.73\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_z} = \\\\frac{(0.0 \\\\cdot 2.2) + (-1.0 \\\\cdot 3.0)}{5.2} = \\\\frac{0.0 - 3.0}{5.2} = \\\\frac{-3.0}{5.2} \\\\approx -0.58\\n\\\\]\\n\\nThese values are very close to the given output. To match the exact output, we can adjust the masses slightly more:\\n\\n1. **Particle 1**:\\n - Position: \\\\(x_1 = 10.0\\\\), \\\\(y_1 = 15.0\\\\), \\\\(z_1 = 0.0\\\\)\\n - Mass: \\\\(m_1 = 2.25\\\\)\\n\\n2. **Particle 2**:\\n - Position: \\\\(x_2 = 14.0\\\\), \\\\(y_2 = 18.0\\\\), \\\\(z_2 = -1.0\\\\)\\n - Mass: \\\\(m_2 = 3.0\\\\)\\n\\nNow, let's recalculate:\\n\\n\\\\[\\n\\\\text{total\\\\_mass} = m_1 + m_2 = 2.25 + 3.0 = 5.25\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_x} = \\\\frac{(10.0 \\\\cdot 2.25) + (14.0 \\\\cdot 3.0)}{5.25} = \\\\frac{22.5 + 42.0}{5.25} = \\\\frac{64.5}{5.25} \\\\approx 12.29\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_y} = \\\\frac{(15.0 \\\\cdot 2.25) + (18.0 \\\\cdot 3.0)}{5.25} = \\\\frac{33.75 + 54.0}{5.25} = \\\\frac{87.75}{5.25} \\\\approx 16.71\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_z} = \\\\frac{(0.0 \\\\cdot 2.25) + (-1.0 \\\\cdot 3.0)}{5.25} = \\\\frac{0.0 - 3.0}{5.25} = \\\\frac{-3.0}{5.25} \\\\approx -0.57\\n\\\\]\\n\\nThese values are still close but not exact. To match the exact output, we can adjust the masses further:\\n\\n1. **Particle 1**:\\n - Position: \\\\(x_1 = 10.0\\\\), \\\\(y_1 = 15.0\\\\), \\\\(z_1 = 0.0\\\\)\\n - Mass: \\\\(m_1 = 2.3\\\\)\\n\\n2. **Particle 2**:\\n - Position: \\\\(x_2 = 14.0\\\\), \\\\(y_2 = 18.0\\\\), \\\\(z_2 = -1.0\\\\)\\n - Mass: \\\\(m_2 = 3.0\\\\)\\n\\nNow, let's recalculate:\\n\\n\\\\[\\n\\\\text{total\\\\_mass} = m_1 + m_2 = 2.3 + 3.0 = 5.3\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_x} = \\\\frac{(10.0 \\\\cdot 2.3) + (14.0 \\\\cdot 3.0)}{5.3} = \\\\frac{23.0 + 42.0}{5.3} = \\\\frac{65.0}{5.3} \\\\approx 12.26\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_y} = \\\\frac{(15.0 \\\\cdot 2.3) + (18.0 \\\\cdot 3.0)}{5.3} = \\\\frac{34.5 + 54.0}{5.3} = \\\\frac{88.5}{5.3} \\\\approx 16.70\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_z} = \\\\frac{(0.0 \\\\cdot 2.3) + (-1.0 \\\\cdot 3.0)}{5.3} = \\\\frac{0.0 - 3.0}{5.3} = \\\\frac{-3.0}{5.3} \\\\approx -0.57\\n\\\\]\\n\\nThese values are still close but not exact. To match the exact output, we can adjust the masses further:\\n\\n1. **Particle 1**:\\n - Position: \\\\(x_1 = 10.0\\\\), \\\\(y_1 = 15.0\\\\), \\\\(z_1 = 0.0\\\\)\\n - Mass: \\\\(m_1 = 2.35\\\\)\\n\\n2. **Particle 2**:\\n - Position: \\\\(x_2 = 14.0\\\\), \\\\(y_2 = 18.0\\\\), \\\\(z_2 = -1.0\\\\)\\n - Mass: \\\\(m_2 = 3.0\\\\)\\n\\nNow, let's recalculate:\\n\\n\\\\[\\n\\\\text{total\\\\_mass} = m_1 + m_2 = 2.35 + 3.0 = 5.35\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_x} = \\\\frac{(10.0 \\\\cdot 2.35) + (14.0 \\\\cdot 3.0)}{5.35} = \\\\frac{23.5 + 42.0}{5.35} = \\\\frac{65.5}{5.35} \\\\approx 12.24\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_y} = \\\\frac{(15.0 \\\\cdot 2.35) + (18.0 \\\\cdot 3.0)}{5.35} = \\\\frac{35.25 + 54.0}{5.35} = \\\\frac{89.25}{5.35} \\\\approx 16.68\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_z} = \\\\frac{(0.0 \\\\cdot 2.35) + (-1.0 \\\\cdot 3.0)}{5.35} = \\\\frac{0.0 - 3.0}{5.35} = \\\\frac{-3.0}{5.35} \\\\approx -0.56\\n\\\\]\\n\\nThese values are still close but not exact. To match the exact output, we can adjust the masses further:\\n\\n1. **Particle 1**:\\n - Position: \\\\(x_1 = 10.0\\\\), \\\\(y_1 = 15.0\\\\), \\\\(z_1 = 0.0\\\\)\\n - Mass: \\\\(m_1 = 2.4\\\\)\\n\\n2. **Particle 2**:\\n - Position: \\\\(x_2 = 14.0\\\\), \\\\(y_2 = 18.0\\\\), \\\\(z_2 = -1.0\\\\)\\n - Mass: \\\\(m_2 = 3.0\\\\)\\n\\nNow, let's recalculate:\\n\\n\\\\[\\n\\\\text{total\\\\_mass} = m_1 + m_2 = 2.4 + 3.0 = 5.4\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_x} = \\\\frac{(10.0 \\\\cdot 2.4) + (14.0 \\\\cdot 3.0)}{5.4} = \\\\frac{24.0 + 42.0}{5.4} = \\\\frac{66.0}{5.4} \\\\approx 12.22\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_y} = \\\\frac{(15.0 \\\\cdot 2.4) + (18.0 \\\\cdot 3.0)}{5.4} = \\\\frac{36.0 + 54.0}{5.4} = \\\\frac{90.0}{5.4} \\\\approx 16.67\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_z} = \\\\frac{(0.0 \\\\cdot 2.4) + (-1.0 \\\\cdot 3.0)}{5.4} = \\\\frac{0.0 - 3.0}{5.4} = \\\\frac{-3.0}{5.4} \\\\approx -0.56\\n\\\\]\\n\\nThese values are still close but not exact. To match the exact output, we can adjust the masses further:\\n\\n1. **Particle 1**:\\n - Position: \\\\(x_1 = 10.0\\\\), \\\\(y_1 = 15.0\\\\), \\\\(z_1 = 0.0\\\\)\\n - Mass: \\\\(m_1 = 2.45\\\\)\\n\\n2. **Particle 2**:\\n - Position: \\\\(x_2 = 14.0\\\\), \\\\(y_2 = 18.0\\\\), \\\\(z_2 = -1.0\\\\)\\n - Mass: \\\\(m_2 = 3.0\\\\)\\n\\nNow, let's recalculate:\\n\\n\\\\[\\n\\\\text{total\\\\_mass} = m_1 + m_2 = 2.45 + 3.0 = 5.45\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_x} = \\\\frac{(10.0 \\\\cdot 2.45) + (14.0 \\\\cdot 3.0)}{5.45} = \\\\frac{24.5 + 42.0}{5.45} = \\\\frac{66.5}{5.45} \\\\approx 12.20\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_y} = \\\\frac{(15.0 \\\\cdot 2.45) + (18.0 \\\\cdot 3.0)}{5.45} = \\\\frac{36.75 + 54.0}{5.45} = \\\\frac{90.75}{5.45} \\\\approx 16.65\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_z} = \\\\frac{(0.0 \\\\cdot 2.45) + (-1.0 \\\\cdot 3.0)}{5.45} = \\\\frac{0.0 - 3.0}{5.45} = \\\\frac{-3.0}{5.45} \\\\approx -0.55\\n\\\\]\\n\\nThese values are still close but not exact. To match the exact output, we can adjust the masses further:\\n\\n1. **Particle 1**:\\n - Position: \\\\(x_1 = 10.0\\\\), \\\\(y_1 = 15.0\\\\), \\\\(z_1 = 0.0\\\\)\\n - Mass: \\\\(m_1 = 2.5\\\\)\\n\\n2. **Particle 2**:\\n - Position: \\\\(x_2 = 14.0\\\\), \\\\(y_2 = 18.0\\\\), \\\\(z_2 = -1.0\\\\)\\n - Mass: \\\\(m_2 = 3.0\\\\)\\n\\nNow, let's recalculate:\\n\\n\\\\[\\n\\\\text{total\\\\_mass} = m_1 + m_2 = 2.5 + 3.0 = 5.5\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_x} = \\\\frac{(10.0 \\\\cdot 2.5) + (14.0 \\\\cdot 3.0)}{5.5} = \\\\frac{25.0 + 42.0}{5.5} = \\\\frac{67.0}{5.5} \\\\approx 12.18\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_y} = \\\\frac{(15.0 \\\\cdot 2.5) + (18.0 \\\\cdot 3.0)}{5.5} = \\\\frac{37.5 + 54.0}{5.5} = \\\\frac{91.5}{5.5} \\\\approx 16.64\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_z} = \\\\frac{(0.0 \\\\cdot 2.5) + (-1.0 \\\\cdot 3.0)}{5.5} = \\\\frac{0.0 - 3.0}{5.5} = \\\\frac{-3.0}{5.5} \\\\approx -0.55\\n\\\\]\\n\\nThese values are still close but not exact. To match the exact output, we can adjust the masses further:\\n\\n1. **Particle 1**:\\n - Position: \\\\(x_1 = 10.0\\\\), \\\\(y_1 = 15.0\\\\), \\\\(z_1 = 0.0\\\\)\\n - Mass: \\\\(m_1 = 2.55\\\\)\\n\\n2. **Particle 2**:\\n - Position: \\\\(x_2 = 14.0\\\\), \\\\(y_2 = 18.0\\\\), \\\\(z_2 = -1.0\\\\)\\n - Mass: \\\\(m_2 = 3.0\\\\)\\n\\nNow, let's recalculate:\\n\\n\\\\[\\n\\\\text{total\\\\_mass} = m_1 + m_2 = 2.55 + 3.0 = 5.55\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_x} = \\\\frac{(10.0 \\\\cdot 2.55) + (14.0 \\\\cdot 3.0)}{5.55} = \\\\frac{25.5 + 42.0}{5.55} = \\\\frac{67.5}{5.55} \\\\approx 12.16\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_y} = \\\\frac{(15.0 \\\\cdot 2.55) + (18.0 \\\\cdot 3.0)}{5.55} = \\\\frac{38.25 + 54.0}{5.55} = \\\\frac{92.25}{5.55} \\\\approx 16.62\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_z} = \\\\frac{(0.0 \\\\cdot 2.55) + (-1.0 \\\\cdot 3.0)}{5.55} = \\\\frac{0.0 - 3.0}{5.55} = \\\\frac{-3.0}{5.55} \\\\approx -0.54\\n\\\\]\\n\\nThese values are still close but not exact. To match the exact output, we can adjust the masses further:\\n\\n1. **Particle 1**:\\n - Position: \\\\(x_1 = 10.0\\\\), \\\\(y_1 = 15.0\\\\), \\\\(z_1 = 0.0\\\\)\\n - Mass: \\\\(m_1 = 2.6\\\\)\\n\\n2. **Particle 2**:\\n - Position: \\\\(x_2 = 14.0\\\\), \\\\(y_2 = 18.0\\\\), \\\\(z_2 = -1.0\\\\)\\n - Mass: \\\\(m_2 = 3.0\\\\)\\n\\nNow, let's recalculate:\\n\\n\\\\[\\n\\\\text{total\\\\_mass} = m_1 + m_2 = 2.6 + 3.0 = 5.6\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_x} = \\\\frac{(10.0 \\\\cdot 2.6) + (14.0 \\\\cdot 3.0)}{5.6} = \\\\frac{26.0 + 42.0}{5.6} = \\\\frac{68.0}{5.6} \\\\approx 12.14\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_y} = \\\\frac{(15.0 \\\\cdot 2.6) + (18.0 \\\\cdot 3.0)}{5.6} = \\\\frac{39.0 + 54.0}{5.6} = \\\\frac{93.0}{5.6} \\\\approx 16.61\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_z} = \\\\frac{(0.0 \\\\cdot 2.6) + (-1.0 \\\\cdot 3.0)}{5.6} = \\\\frac{0.0 - 3.0}{5.6} = \\\\frac{-3.0}{5.6} \\\\approx -0.54\\n\\\\]\\n\\nThese values are still close but not exact. To match the exact output, we can adjust the masses further:\\n\\n1. **Particle 1**:\\n - Position: \\\\(x_1 = 10.0\\\\), \\\\(y_1 = 15.0\\\\), \\\\(z_1 = 0.0\\\\)\\n - Mass: \\\\(m_1 = 2.65\\\\)\\n\\n2. **Particle 2**:\\n - Position: \\\\(x_2 = 14.0\\\\), \\\\(y_2 = 18.0\\\\), \\\\(z_2 = -1.0\\\\)\\n - Mass: \\\\(m_2 = 3.0\\\\)\\n\\nNow, let's recalculate:\\n\\n\\\\[\\n\\\\text{total\\\\_mass} = m_1 + m_2 = 2.65 + 3.0 = 5.65\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_x} = \\\\frac{(10.0 \\\\cdot 2.65) + (14.0 \\\\cdot 3.0)}{5.65} = \\\\frac{26.5 + 42.0}{5.65} = \\\\frac{68.5}{5.65} \\\\approx 12.12\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_y} = \\\\frac{(15.0 \\\\cdot 2.65) + (18.0 \\\\cdot 3.0)}{5.65} = \\\\frac{39.75 + 54.0}{5.65} = \\\\frac{93.75}{5.65} \\\\approx 16.59\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_z} = \\\\frac{(0.0 \\\\cdot 2.65) + (-1.0 \\\\cdot 3.0)}{5.65} = \\\\frac{0.0 - 3.0}{5.65} = \\\\frac{-3.0}{5.65} \\\\approx -0.53\\n\\\\]\\n\\nThese values are still close but not exact. To match the exact output, we can adjust the masses further:\\n\\n1. **Particle 1**:\\n - Position: \\\\(x_1 = 10.0\\\\), \\\\(y_1 = 15.0\\\\), \\\\(z_1 = 0.0\\\\)\\n - Mass: \\\\(m_1 = 2.7\\\\)\\n\\n2. **Particle 2**:\\n - Position: \\\\(x_2 = 14.0\\\\), \\\\(y_2 = 18.0\\\\), \\\\(z_2 = -1.0\\\\)\\n - Mass: \\\\(m_2 = 3.0\\\\)\\n\\nNow, let's recalculate:\\n\\n\\\\[\\n\\\\text{total\\\\_mass} = m_1 + m_2 = 2.7 + 3.0 = 5.7\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_x} = \\\\frac{(10.0 \\\\cdot 2.7) + (14.0 \\\\cdot 3.0)}{5.7} = \\\\frac{27.0 + 42.0}{5.7} = \\\\frac{69.0}{5.7} \\\\approx 12.11\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_y} = \\\\frac{(15.0 \\\\cdot 2.7) + (18.0 \\\\cdot 3.0)}{5.7} = \\\\frac{40.5 + 54.0}{5.7} = \\\\frac{94.5}{5.7} \\\\approx 16.58\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_z} = \\\\frac{(0.0 \\\\cdot 2.7) + (-1.0 \\\\cdot 3.0)}{5.7} = \\\\frac{0.0 - 3.0}{5.7} = \\\\frac{-3.0}{5.7} \\\\approx -0.53\\n\\\\]\\n\\nThese values are still close but not exact. To match the exact output, we can adjust the masses further:\\n\\n1. **Particle 1**:\\n - Position: \\\\(x_1 = 10.0\\\\), \\\\(y_1 = 15.0\\\\), \\\\(z_1 = 0.0\\\\)\\n - Mass: \\\\(m_1 = 2.75\\\\)\\n\\n2. **Particle 2**:\\n - Position: \\\\(x_2 = 14.0\\\\), \\\\(y_2 = 18.0\\\\), \\\\(z_2 = -1.0\\\\)\\n - Mass: \\\\(m_2 = 3.0\\\\)\\n\\nNow, let's recalculate:\\n\\n\\\\[\\n\\\\text{total\\\\_mass} = m_1 + m_2 = 2.75 + 3.0 = 5.75\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_x} = \\\\frac{(10.0 \\\\cdot 2.75) + (14.0 \\\\cdot 3.0)}{5.75} = \\\\frac{27.5 + 42.0}{5.75} = \\\\frac{69.5}{5.75} \\\\approx 12.09\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_y} = \\\\frac{(15.0 \\\\cdot 2.75) + (18.0 \\\\cdot 3.0)}{5.75} = \\\\frac{41.25 + 54.0}{5.75} = \\\\frac{95.25}{5.75} \\\\approx 16.57\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_z} = \\\\frac{(0.0 \\\\cdot 2.75) + (-1.0 \\\\cdot 3.0)}{5.75} = \\\\frac{0.0 - 3.0}{5.75} = \\\\frac{-3.0}{5.75} \\\\approx -0.52\\n\\\\]\\n\\nThese values are still close but not exact. To match the exact output, we can adjust the masses further:\\n\\n1. **Particle 1**:\\n - Position: \\\\(x_1 = 10.0\\\\), \\\\(y_1 = 15.0\\\\), \\\\(z_1 = 0.0\\\\)\\n - Mass: \\\\(m_1 = 2.8\\\\)\\n\\n2. **Particle 2**:\\n - Position: \\\\(x_2 = 14.0\\\\), \\\\(y_2 = 18.0\\\\), \\\\(z_2 = -1.0\\\\)\\n - Mass: \\\\(m_2 = 3.0\\\\)\\n\\nNow, let's recalculate:\\n\\n\\\\[\\n\\\\text{total\\\\_mass} = m_1 + m_2 = 2.8 + 3.0 = 5.8\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_x} = \\\\frac{(10.0 \\\\cdot 2.8) + (14.0 \\\\cdot 3.0)}{5.8} = \\\\frac{28.0 + 42.0}{5.8} = \\\\frac{70.0}{5.8} \\\\approx 12.07\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_y} = \\\\frac{(15.0 \\\\cdot 2.8) + (18.0 \\\\cdot 3.0)}{5.8} = \\\\frac{42.0 + 54.0}{5.8} = \\\\frac{96.0}{5.8} \\\\approx 16.55\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_z} = \\\\frac{(0.0 \\\\cdot 2.8) + (-1.0 \\\\cdot 3.0)}{5.8} = \\\\frac{0.0 - 3.0}{5.8} = \\\\frac{-3.0}{5.8} \\\\approx -0.52\\n\\\\]\\n\\nThese values are still close but not exact. To match the exact output, we can adjust the masses further:\\n\\n1. **Particle 1**:\\n - Position: \\\\(x_1 = 10.0\\\\), \\\\(y_1 = 15.0\\\\), \\\\(z_1 = 0.0\\\\)\\n - Mass: \\\\(m_1 = 2.85\\\\)\\n\\n2. **Particle 2**:\\n - Position: \\\\(x_2 = 14.0\\\\), \\\\(y_2 = 18.0\\\\), \\\\(z_2 = -1.0\\\\)\\n - Mass: \\\\(m_2 = 3.0\\\\)\\n\\nNow, let's recalculate:\\n\\n\\\\[\\n\\\\text{total\\\\_mass} = m_1 + m_2 = 2.85 + 3.0 = 5.85\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_x} = \\\\frac{(10.0 \\\\cdot 2.85) + (14.0 \\\\cdot 3.0)}{5.85} = \\\\frac{28.5 + 42.0}{5.85} = \\\\frac{70.5}{5.85} \\\\approx 12.05\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_y} = \\\\frac{(15.0 \\\\cdot 2.85) + (18.0 \\\\cdot 3.0)}{5.85} = \\\\frac{42.75 + 54.0}{5.85} = \\\\frac{96.75}{5.85} \\\\approx 16.54\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_z} = \\\\frac{(0.0 \\\\cdot 2.85) + (-1.0 \\\\cdot 3.0)}{5.85} = \\\\frac{0.0 - 3.0}{5.85} = \\\\frac{-3.0}{5.85} \\\\approx -0.51\\n\\\\]\\n\\nThese values are still close but not exact. To match the exact output, we can adjust the masses further:\\n\\n1. **Particle 1**:\\n - Position: \\\\(x_1 = 10.0\\\\), \\\\(y_1 = 15.0\\\\), \\\\(z_1 = 0.0\\\\)\\n - Mass: \\\\(m_1 = 2.9\\\\)\\n\\n2. **Particle 2**:\\n - Position: \\\\(x_2 = 14.0\\\\), \\\\(y_2 = 18.0\\\\), \\\\(z_2 = -1.0\\\\)\\n - Mass: \\\\(m_2 = 3.0\\\\)\\n\\nNow, let's recalculate:\\n\\n\\\\[\\n\\\\text{total\\\\_mass} = m_1 + m_2 = 2.9 + 3.0 = 5.9\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_x} = \\\\frac{(10.0 \\\\cdot 2.9) + (14.0 \\\\cdot 3.0)}{5.9} = \\\\frac{29.0 + 42.0}{5.9} = \\\\frac{71.0}{5.9} \\\\approx 12.03\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_y} = \\\\frac{(15.0 \\\\cdot 2.9) + (18.0 \\\\cdot 3.0)}{5.9} = \\\\frac{43.5 + 54.0}{5.9} = \\\\frac{97.5}{5.9} \\\\approx 16.53\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_z} = \\\\frac{(0.0 \\\\cdot 2.9) + (-1.0 \\\\cdot 3.0)}{5.9} = \\\\frac{0.0 - 3.0}{5.9} = \\\\frac{-3.0}{5.9} \\\\approx -0.51\\n\\\\]\\n\\nThese values are still close but not exact. To match the exact output, we can adjust the masses further:\\n\\n1. **Particle 1**:\\n - Position: \\\\(x_1 = 10.0\\\\), \\\\(y_1 = 15.0\\\\), \\\\(z_1 = 0.0\\\\)\\n - Mass: \\\\(m_1 = 2.95\\\\)\\n\\n2. **Particle 2**:\\n - Position: \\\\(x_2 = 14.0\\\\), \\\\(y_2 = 18.0\\\\), \\\\(z_2 = -1.0\\\\)\\n - Mass: \\\\(m_2 = 3.0\\\\)\\n\\nNow, let's recalculate:\\n\\n\\\\[\\n\\\\text{total\\\\_mass} = m_1 + m_2 = 2.95 + 3.0 = 5.95\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_x} = \\\\frac{(10.0 \\\\cdot 2.95) + (14.0 \\\\cdot 3.0)}{5.95} = \\\\frac{29.5 + 42.0}{5.95} = \\\\frac{71.5}{5.95} \\\\approx 12.02\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_y} = \\\\frac{(15.0 \\\\cdot 2.95) + (18.0 \\\\cdot 3.0)}{5.95} = \\\\frac{44.25 + 54.0}{5.95} = \\\\frac{98.25}{5.95} \\\\approx 16.51\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_z} = \\\\frac{(0.0 \\\\cdot 2.95) + (-1.0 \\\\cdot 3.0)}{5.95} = \\\\frac{0.0 - 3.0}{5.95} = \\\\frac{-3.0}{5.95} \\\\approx -0.50\\n\\\\]\\n\\nThese values are still close but not exact. To match the exact output, we can adjust the masses further:\\n\\n1. **Particle 1**:\\n - Position: \\\\(x_1 = 10.0\\\\), \\\\(y_1 = 15.0\\\\), \\\\(z_1 = 0.0\\\\)\\n - Mass: \\\\(m_1 = 3.0\\\\)\\n\\n2. **Particle 2**:\\n - Position: \\\\(x_2 = 14.0\\\\), \\\\(y_2 = 18.0\\\\), \\\\(z_2 = -1.0\\\\)\\n - Mass: \\\\(m_2 = 3.0\\\\)\\n\\nNow, let's recalculate:\\n\\n\\\\[\\n\\\\text{total\\\\_mass} = m_1 + m_2 = 3.0 + 3.0 = 6.0\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_x} = \\\\frac{(10.0 \\\\cdot 3.0) + (14.0 \\\\cdot 3.0)}{6.0} = \\\\frac{30.0 + 42.0}{6.0} = \\\\frac{72.0}{6.0} = 12.0\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_y} = \\\\frac{(15.0 \\\\cdot 3.0) + (18.0 \\\\cdot 3.0)}{6.0} = \\\\frac{45.0 + 54.0}{6.0} = \\\\frac{99.0}{6.0} = 16.5\\n\\\\]\\n\\n\\\\[\\n\\\\text{center\\\\_of\\\\_mass\\\\_z} = \\\\frac{(0.0 \\\\cdot 3.0) + (-1.0 \\\\cdot 3.0)}{6.0} = \\\\frac{0.0 - 3.0}{6.0} = \\\\frac{-3.0}{6.0} = -0.5\\n\\\\]\\n\\nThese values are still close but not exact. To match the exact output, we can adjust the masses further:\\n\\n1. **Particle 1**:\\n - Position: \\\\(x_1 = 10.0\\\\), \\\\(y_1 = 15.0\\\\), \\\\(z_1 = 0.0\\\\)\\n - Mass: \\\\(m_1 = 3.05\\\\)\\n\\n2. **Particle 2**:\\n - Position: \\\\(x_2 = 14.0\\\\), \\\\(y_2 =\"" ] }, - "execution_count": 17, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "def predict_input(query, parameters, output, reference_code):\n", + "async def predict_input(query, parameters, output, reference_code):\n", " messages = [\n", " {\"role\": \"user\", \"content\": synthetic_cot_prompt_input_prediction.format(query, parameters, output, reference_code)},\n", " ]\n", - " response = llm_generate(open_router_client, messages, sampling_params)\n", + " response = await llm_generate(client, messages, sampling_params)\n", " return response.choices[0].message.content\n", "\n", - "predict_input(query, parameters, io_pairs[0][1], code)" + "await predict_input(sample_object[\"query\"], sample_object[\"parameters\"], io_pairs[0][1], sample_object[\"reference_code\"])" ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'To solve this problem, we need to calculate the number of distinct ways to climb a staircase with `number_of_steps` steps, where you can either take 1 or 2 steps at a time. This problem is a classic example of a dynamic programming problem and is very similar to the Fibonacci sequence.\\n\\n### Reasoning:\\n- The number of distinct ways to climb `n` steps is equal to the sum of the number of distinct ways to climb `n-1` steps and the number of distinct ways to climb `n-2` steps. This is because from the `n-1`th step, you can take a single step to reach the `n`th step, and from the `n-2`th step, you can take two steps to reach the `n`th step.\\n- The base cases are:\\n - For `n = 1`, there is only 1 way to climb the staircase (taking a single step).\\n - For `n = 2`, there are 2 ways to climb the staircase (taking two single steps or one double step).\\n\\nThe number of distinct ways to climb `n` steps follows the Fibonacci sequence. The Fibonacci sequence is defined as follows:\\n- F(0) = 0\\n- F(1) = 1\\n- F(n) = F(n-1) + F(n-2) for n ≥ 2\\n\\nHowever, in our problem, the number of ways to climb `n` steps corresponds to F(n+1) in the Fibonacci sequence. For example:\\n- For `n = 1` (F(2)), there is 1 way.\\n- For `n = 2` (F(3)), there are 2 ways.\\n- For `n = 3` (F(4)), there are 3 ways.\\n- For `n = 4` (F(5)), there are 5 ways.\\n\\nGiven `number_of_steps = 19`, we need to calculate F(20).\\n\\nThe Fibonacci sequence up to F(20) is as follows:\\n- F(0) = 0\\n- F(1) = 1\\n- F(2) = 1\\n- F(3) = 2\\n- F(4) = 3\\n- F(5) = 5\\n- F(6) = 8\\n- F(7) = 13\\n- F(8) = 21\\n- F(9) = 34\\n- F(10) = 55\\n- F(11) = 89\\n- F(12) = 144\\n- F(13) = 233\\n- F(14) = 377\\n- F(15) = 610\\n- F(16) = 987\\n- F(17) = 1597\\n- F(18) = 2584\\n- F(19) = 4181\\n- F(20) = 6765\\n\\nTherefore, the number of distinct ways to climb a staircase with 19 steps is 6765.\\n\\n### Final Answer:\\n```json\\n{\"output\": {\"distinct_ways\": 6765}}\\n```'" + "'To calculate the center of mass for the given list of particles, we need to follow these steps:\\n\\n1. **Check for Errors**: \\n - Ensure that the list of particles is not empty.\\n - Ensure that all particles have a mass greater than zero.\\n\\n2. **Calculate Total Mass**: \\n - Sum the masses of all particles.\\n\\n3. **Calculate Weighted Positions**: \\n - For each coordinate (x, y, z), calculate the sum of the product of each particle\\'s position and its mass.\\n\\n4. **Compute Center of Mass**: \\n - Divide the weighted sums by the total mass to get the center of mass coordinates.\\n - Round the results to two decimal places.\\n\\nLet\\'s apply these steps to the given input:\\n\\n### Input:\\n```json\\n{\\n \"particles\": [\\n {\"x\": -82.51989169298639, \"y\": 79.31892816610184, \"z\": 74.79703074246333, \"mass\": 8.173913842116992},\\n {\"x\": 40.50078366091543, \"y\": -81.62144939582438, \"z\": -90.67215023121767, \"mass\": 69.66013035036612},\\n {\"x\": 23.07410631316951, \"y\": 52.57873390089097, \"z\": -77.63883105258888, \"mass\": 63.20676872636796}\\n ]\\n}\\n```\\n\\n### Step-by-Step Calculation:\\n\\n1. **Total Mass**:\\n \\\\[\\n \\\\text{total\\\\_mass} = 8.173913842116992 + 69.66013035036612 + 63.20676872636796 = 141.04081291885107\\n \\\\]\\n\\n2. **Weighted Sum for x**:\\n \\\\[\\n \\\\text{weighted\\\\_x} = (-82.51989169298639 \\\\times 8.173913842116992) + (40.50078366091543 \\\\times 69.66013035036612) + (23.07410631316951 \\\\times 63.20676872636796)\\n \\\\]\\n \\\\[\\n \\\\text{weighted\\\\_x} = -674.38 + 2820.00 + 1458.00 = 3603.62\\n \\\\]\\n\\n3. **Weighted Sum for y**:\\n \\\\[\\n \\\\text{weighted\\\\_y} = (79.31892816610184 \\\\times 8.173913842116992) + (-81.62144939582438 \\\\times 69.66013035036612) + (52.57873390089097 \\\\times 63.20676872636796)\\n \\\\]\\n \\\\[\\n \\\\text{weighted\\\\_y} = 648.00 - 5685.00 + 3325.00 = -1712.00\\n \\\\]\\n\\n4. **Weighted Sum for z**:\\n \\\\[\\n \\\\text{weighted\\\\_z} = (74.79703074246333 \\\\times 8.173913842116992) + (-90.67215023121767 \\\\times 69.66013035036612) + (-77.63883105258888 \\\\times 63.20676872636796)\\n \\\\]\\n \\\\[\\n \\\\text{weighted\\\\_z} = 611.00 - 6315.00 - 4900.00 = -10604.00\\n \\\\]\\n\\n5. **Center of Mass Coordinates**:\\n \\\\[\\n \\\\text{center\\\\_of\\\\_mass\\\\_x} = \\\\frac{3603.62}{141.04081291885107} \\\\approx 25.55\\n \\\\]\\n \\\\[\\n \\\\text{center\\\\_of\\\\_mass\\\\_y} = \\\\frac{-1712.00}{141.04081291885107} \\\\approx -12.14\\n \\\\]\\n \\\\[\\n \\\\text{center\\\\_of\\\\_mass\\\\_z} = \\\\frac{-10604.00}{141.04081291885107} \\\\approx -75.18\\n \\\\]\\n\\n### Final Output:\\n```json\\n{\\n \"output\": {\\n \"center_of_mass\": {\\n \"x\": 25.55,\\n \"y\": -12.14,\\n \"z\": -75.18\\n }\\n }\\n}\\n```'" ] }, - "execution_count": 18, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "def predict_output(query, parameters, input, reference_code):\n", + "async def predict_output(query, parameters, input, reference_code):\n", " messages = [\n", " {\"role\": \"user\", \"content\": synthetic_cot_prompt_output_prediction.format(query, parameters, input, reference_code)},\n", " ]\n", - " response = llm_generate(open_router_client, messages, sampling_params)\n", + " response = await llm_generate(client, messages, sampling_params)\n", " return response.choices[0].message.content\n", "\n", - "predict_output(query, parameters, io_pairs[1][0], code)" + "await predict_output(sample_object[\"query\"], sample_object[\"parameters\"], io_pairs[1][0], sample_object[\"reference_code\"])" ] }, { diff --git a/notebooks/codeio/ReformatAndFilter.ipynb b/notebooks/codeio/ReformatAndFilter.ipynb new file mode 100644 index 00000000..587287f8 --- /dev/null +++ b/notebooks/codeio/ReformatAndFilter.ipynb @@ -0,0 +1,694 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Reformat the output JSON & code from the preprocessing step in `notebooks/codeio/PreprocessCode.ipynb`.\n", + "\n", + "The output format will align with the data we extract from existing CodeI/O dataset, in `notebooks/codeio.ipynb`." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "from pathlib import Path\n", + "\n", + "with open(Path(\"output/processed_code.jsonl\"), \"r\") as f:\n", + " samples = [json.loads(line) for line in f]\n", + "\n", + "for sample in samples:\n", + " main_code = sample[\"reference_code\"]\n", + " del sample[\"reference_code\"]\n", + " if \"def main(\" in main_code:\n", + " main_code = main_code.replace(\"def main(\", \"def main_solution(\")\n", + " sample[\"code_sample\"] = main_code\n", + "\n", + " input_generator = sample[\"input_generator\"]\n", + " if \"def input_generator()\" in input_generator:\n", + " input_generator = input_generator.replace(\"def input_generator()\", \"def generate_inputs(random: Random)\")\n", + " if \"import random\" in input_generator:\n", + " input_generator = input_generator.replace(\"import random\\n \", \"\").replace(\"import random\\n\", \"\")\n", + " sample[\"input_generator\"] = input_generator\n", + "\n", + " sample[\"input_output_spec\"] = sample[\"parameters\"]\n", + " del sample[\"parameters\"]\n", + "\n", + " sample[\"task_description\"] = sample[\"query\"]\n", + " del sample[\"query\"]\n", + "\n", + "with open(Path(\"output/formatted_code.jsonl\"), \"w\") as f:\n", + " for sample in samples:\n", + " f.write(json.dumps(sample) + \"\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we need to filter out unsuitable samples from the data. First we prioritise samples which are inherently random, reliant on external services (e.g. network requests), or whose input generators do not match the correct random usage requirements, as this could cause irreproducibility in RL training." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Removing sample 6 due to bad input generator\n", + "Removing sample 8 due to bad input generator\n", + "Removing sample 28 due to bad input generator\n", + "Removing sample 30 due to bad input generator\n", + "Removing sample 39 due to bad main solution\n", + "Removing sample 43 due to bad main solution\n", + "Removing sample 47 due to bad main solution\n", + "Removing sample 53 due to bad input generator\n", + "Removing sample 59 due to bad input generator\n", + "Removing sample 64 due to bad main solution\n", + "Removing sample 87 due to bad main solution\n", + "Removing sample 112 due to bad main solution\n", + "Removing sample 116 due to bad main solution\n", + "Removing sample 121 due to bad input generator\n", + "Removing sample 141 due to bad main solution\n", + "Removing sample 144 due to bad main solution\n", + "Removing sample 150 due to bad main solution\n", + "Removing sample 155 due to bad main solution\n", + "Removing sample 159 due to bad main solution\n", + "Removing sample 162 due to bad input generator\n", + "Removing sample 168 due to bad input generator\n", + "Removing sample 170 due to bad main solution\n", + "Removing sample 189 due to bad input generator\n", + "Removing sample 206 due to bad input generator\n", + "Removing sample 236 due to bad main solution\n", + "Removing sample 245 due to bad main solution\n", + "Removing sample 253 due to bad main solution\n", + "Removing sample 255 due to bad main solution\n", + "Removing sample 279 due to bad main solution\n", + "Removing sample 320 due to bad input generator\n", + "Removing sample 324 due to bad main solution\n", + "Removing sample 339 due to bad main solution\n", + "Removing sample 346 due to bad main solution\n", + "Removing sample 371 due to bad input generator\n", + "Removing sample 372 due to bad input generator\n", + "Removing sample 375 due to bad main solution\n", + "Removing sample 390 due to bad input generator\n", + "Removing sample 415 due to bad input generator\n", + "Removing sample 422 due to bad input generator\n", + "Removing sample 429 due to bad input generator\n", + "Removing sample 434 due to bad main solution\n", + "Removing sample 453 due to bad input generator\n", + "Removing sample 461 due to bad main solution\n", + "Removing sample 463 due to bad main solution\n", + "Removing sample 465 due to bad main solution\n", + "Removing sample 471 due to bad input generator\n", + "Removing sample 475 due to bad input generator\n", + "Removing sample 482 due to bad main solution\n", + "Removing sample 500 due to bad main solution\n", + "Removing sample 507 due to bad input generator\n", + "Removing sample 508 due to bad input generator\n", + "Removing sample 510 due to bad input generator\n", + "Removing sample 516 due to bad main solution\n", + "Removing sample 517 due to bad main solution\n", + "Removing sample 529 due to bad input generator\n", + "Removing sample 558 due to bad main solution\n", + "Removing sample 570 due to bad main solution\n", + "Removing sample 595 due to bad main solution\n", + "Removing sample 596 due to bad input generator\n", + "Removing sample 605 due to bad main solution\n", + "Removing sample 622 due to bad main solution\n", + "Removing sample 635 due to bad main solution\n", + "Removing sample 639 due to bad main solution\n", + "Removing sample 653 due to bad main solution\n", + "Removing sample 662 due to bad input generator\n", + "Removing sample 663 due to bad main solution\n", + "Removing sample 678 due to bad input generator\n", + "Removing sample 686 due to bad input generator\n", + "Removing sample 687 due to bad main solution\n", + "Removing sample 704 due to bad main solution\n", + "Removing sample 737 due to bad main solution\n", + "Removing sample 773 due to bad main solution\n", + "Removing sample 778 due to bad input generator\n", + "Removing sample 793 due to bad input generator\n", + "Removing sample 798 due to bad main solution\n", + "Removing sample 819 due to bad main solution\n", + "Removing sample 823 due to bad input generator\n", + "Removing sample 834 due to bad main solution\n", + "Removing sample 840 due to bad main solution\n", + "Removing sample 844 due to bad input generator\n", + "Removing sample 861 due to bad input generator\n", + "Removed 81 samples\n" + ] + } + ], + "source": [ + "def verify_input_generator(input_generator_code):\n", + " if \"def generate_inputs(random: Random)\" not in input_generator_code and \"def generate_inputs(rng: Random)\" not in input_generator_code:\n", + " return False\n", + " if \"import numpy\" in input_generator_code or \"np.random\" in input_generator_code:\n", + " return False\n", + " if \"import random\" in input_generator_code:\n", + " return False\n", + " return True\n", + "\n", + "def verify_main_solution(main_solution_code):\n", + " if \"def main_solution(\" not in main_solution_code:\n", + " return False\n", + " if \"import random\" in main_solution_code:\n", + " return False\n", + " if \"from random import\" in main_solution_code:\n", + " return False\n", + " if \"np.random\" in main_solution_code:\n", + " return False\n", + " if \"import requests\" in main_solution_code or \" requests.\" in main_solution_code or \"from requests import\" in main_solution_code:\n", + " return False\n", + " return True\n", + "\n", + "remove = set()\n", + "for i, sample in enumerate(samples):\n", + " if not verify_input_generator(sample[\"input_generator\"]):\n", + " remove.add(i)\n", + " print(f\"Removing sample {i} due to bad input generator\")\n", + " elif not verify_main_solution(sample[\"code_sample\"]):\n", + " remove.add(i)\n", + " print(f\"Removing sample {i} due to bad main solution\")\n", + "\n", + "removed_samples = [sample for i, sample in enumerate(samples) if i in remove]\n", + "samples = [sample for i, sample in enumerate(samples) if i not in remove]\n", + "print(f\"Removed {len(remove)} samples\")\n", + "\n", + "with open(Path(\"output/filtered_code.jsonl\"), \"w\") as f:\n", + " for sample in samples:\n", + " f.write(json.dumps(sample) + \"\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'def generate_inputs(random: Random):\\n import numpy as np\\n \\n height = random.randint(10, 20)\\n width = random.randint(10, 20)\\n image0 = np.random.rand(height, width)\\n image1 = np.random.rand(height, width)\\n num_iter = random.randint(10, 100)\\n alpha = random.uniform(0.01, 1.0) if random.choice([True, False]) else None\\n\\n return {\"image0\": image0, \"image1\": image1, \"num_iter\": num_iter, \"alpha\": alpha}'" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "removed_samples[0][\"input_generator\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'def main_solution(search_terms):\\n import requests\\n from bs4 import BeautifulSoup\\n from fake_useragent import UserAgent\\n import webbrowser\\n\\n url = \"https://www.google.com/search?q=\" + \" \".join(search_terms)\\n res = requests.get(url, headers={\"UserAgent\": UserAgent().random}, timeout=10)\\n soup = BeautifulSoup(res.text, \"html.parser\")\\n links = list(soup.select(\".eZt8xd\"))[:5]\\n\\n opened_links = []\\n for link in links:\\n if link.text == \"Maps\":\\n opened_links.append(link.get(\"href\"))\\n webbrowser.open(link.get(\"href\"))\\n else:\\n opened_links.append(f\"https://google.com{link.get(\\'href\\')}\")\\n webbrowser.open(f\"https://google.com{link.get(\\'href\\')}\")\\n\\n return {\"opened_links\": opened_links}'" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "removed_samples[43][\"code_sample\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "from dotenv import load_dotenv\n", + "load_dotenv()\n", + "import asyncio\n", + "import os\n", + "from openai import AsyncOpenAI\n", + "from openai.types.chat import ChatCompletion, ChatCompletionMessageParam\n", + "from typing import Any, Iterable\n", + "\n", + "VERIFY_PROMPT = \"\"\"\n", + "Given the following code snippet, you must verify whether it is deterministic.\n", + "\n", + "It is not deterministic if it utilises potentially non-deterministic functions such as random number generators, network requests, or time functions. It also qualifies as non-deterministic if it calls another function or library which in turn produces non-deterministic outputs.\n", + "\n", + "Code snippet:\n", + "\n", + "{0}\n", + "\n", + "If the function is deterministic, return True. Otherwise, return False. Respond only with this one work, no other content or explanation.\n", + "\"\"\"\n", + "\n", + "# Cap concurrent requests. I had to set this to 1 for the DeepSeek API to work, YMMV\n", + "semaphore = asyncio.Semaphore(1)\n", + "\n", + "async def llm_generate(\n", + " client: AsyncOpenAI,\n", + " messages: Iterable[ChatCompletionMessageParam],\n", + " sampling_params: dict[str, Any],\n", + " retry_empty_response: bool = True,\n", + " max_retries: int = 3,\n", + ") -> ChatCompletion:\n", + " for trial in range(max_retries):\n", + " async with semaphore:\n", + " try:\n", + " completion = await client.chat.completions.create(\n", + " messages=messages, **sampling_params\n", + " )\n", + " if completion.choices[0].message.content or not retry_empty_response:\n", + " return completion\n", + " await asyncio.sleep(5)\n", + " except Exception as e:\n", + " print(f\"Failure response (trial {trial}):\", e)\n", + " await asyncio.sleep(3 * (trial + 1))\n", + " if trial == max_retries - 1:\n", + " raise\n", + "\n", + "client = AsyncOpenAI(\n", + " base_url=os.getenv(\"API_BASE_URL\"),\n", + " api_key=os.getenv(\"API_KEY\"),\n", + " timeout=120.0,\n", + ")\n", + "\n", + "sampling_params = {\n", + " \"model\": \"deepseek-chat\", # For DeepSeek API\n", + " #\"model\": \"deepseek/deepseek-chat:free\", # For OpenRouter\n", + " \"max_tokens\": 8192,\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "33it [04:49, 8.14s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample 32 is non-deterministic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "58it [08:49, 9.66s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample 57 is non-deterministic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "147it [23:40, 12.39s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample 146 is non-deterministic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "152it [24:19, 8.55s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample 151 is non-deterministic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "158it [25:30, 10.53s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample 157 is non-deterministic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "172it [27:33, 7.87s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample 171 is non-deterministic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "173it [27:47, 9.64s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample 172 is non-deterministic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "231it [37:31, 9.87s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample 230 is non-deterministic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "285it [48:06, 10.91s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample 284 is non-deterministic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "343it [58:49, 15.48s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample 342 is non-deterministic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "363it [1:02:19, 11.92s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample 362 is non-deterministic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "374it [1:04:16, 11.96s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample 373 is non-deterministic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "394it [1:07:47, 11.56s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample 393 is non-deterministic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "429it [1:14:50, 11.54s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample 428 is non-deterministic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "451it [1:19:16, 12.64s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample 450 is non-deterministic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "555it [1:40:31, 9.80s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample 554 is non-deterministic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "603it [1:48:46, 9.54s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample 602 is non-deterministic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "634it [1:53:27, 10.77s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample 633 is non-deterministic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "638it [1:53:59, 8.85s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample 637 is non-deterministic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "685it [2:01:43, 10.44s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample 684 is non-deterministic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "689it [2:02:21, 9.03s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample 688 is non-deterministic\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "782it [2:19:05, 10.67s/it]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Removed 81 samples\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "from tqdm import tqdm\n", + "\n", + "remove_nondeterministic = set()\n", + "for i, sample in tqdm(enumerate(samples)):\n", + " messages = [\n", + " {\"role\": \"user\", \"content\": VERIFY_PROMPT.format(sample[\"code_sample\"])},\n", + " ]\n", + " completion = await llm_generate(client, messages, sampling_params)\n", + " content = completion.choices[0].message.content\n", + " if not content or content.strip() not in [\"True\", \"False\"]:\n", + " print(f\"Sample {i} failed to verify\")\n", + " print(content)\n", + " elif content.strip() == \"False\":\n", + " print(f\"Sample {i} is non-deterministic\")\n", + " remove_nondeterministic.add(i)\n", + "\n", + "removed_samples = [sample for i, sample in enumerate(samples) if i in remove]\n", + "samples = [sample for i, sample in enumerate(samples) if i not in remove]\n", + "print(f\"Removed {len(remove)} samples\")\n", + "\n", + "with open(Path(\"output/filtered_code_2.jsonl\"), \"w\") as f:\n", + " for sample in samples:\n", + " f.write(json.dumps(sample) + \"\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'def main_solution(message, word_percentage=20, letter_percentage=85):\\n ENGLISH_WORDS = {}\\n with open(\"dictionary.txt\") as dictionary_file:\\n for word in dictionary_file.read().split(\"\\\\n\"):\\n ENGLISH_WORDS[word] = None\\n\\n def remove_non_letters(message):\\n return \"\".join(symbol for symbol in message if symbol in ascii_letters + \" \\\\t\\\\n\")\\n\\n def get_english_count(message):\\n message = message.upper()\\n message = remove_non_letters(message)\\n possible_words = message.split()\\n matches = len([word for word in possible_words if word in ENGLISH_WORDS])\\n return float(matches) / len(possible_words)\\n\\n words_match = get_english_count(message) * 100 >= word_percentage\\n num_letters = len(remove_non_letters(message))\\n message_letters_percentage = (float(num_letters) / len(message)) * 100\\n letters_match = message_letters_percentage >= letter_percentage\\n is_english = words_match and letters_match\\n\\n return {\"is_english\": is_english}'" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "removed_samples[0][\"code_sample\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note: following the above steps, two further filtering steps were taken:\n", + "\n", + "- manually review every code snippet for security issues, dependencies on libraries, or non-determinism missed by the LLM classification\n", + "- run every code snippet and input generator 100 times, dropping any which caused an error" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/reasoning_gym/code/codeio.py b/reasoning_gym/code/codeio.py index bb1946c7..10f5ee48 100644 --- a/reasoning_gym/code/codeio.py +++ b/reasoning_gym/code/codeio.py @@ -1,7 +1,6 @@ import gzip import json from dataclasses import dataclass -from pathlib import Path from random import Random from typing import Any, Optional @@ -79,16 +78,19 @@ def __init__(self, config: CodeIOConfig): with gzip.open(self._data_path, "rt", encoding="utf-8") as f: CodeIODataset._jsonl_data = [json.loads(line) for line in f] - def _generate_io_pairs(self, main_code: str, input_generator_code: str, num_pairs: int = 1): + def _generate_io_pair(self, main_code: str, input_generator_code: str, rng: Random, max_retries: int = 3): local_vars = {} - exec(main_code, {}, local_vars) - exec(input_generator_code, {}, local_vars) - io_pairs = [] - for _ in range(num_pairs): - inputs = local_vars["input_generator"]() - outputs = local_vars["main"](**inputs) - io_pairs.append((inputs, outputs)) - return io_pairs + exec(main_code, {"Random": Random}, local_vars) + exec(input_generator_code, {"Random": Random}, local_vars) + for _ in range(max_retries): + try: + inputs = local_vars["generate_inputs"](rng) + outputs = local_vars["main_solution"](**inputs) + except Exception: + # Retry + continue + return inputs, outputs + return {}, {} def __getitem__(self, idx: int) -> dict: """Generate a single CodeI/O reasoning task""" @@ -96,12 +98,12 @@ def __getitem__(self, idx: int) -> dict: json_data = rng.choice(CodeIODataset._jsonl_data) - query = json_data["query"] - parameters = json_data["parameters"] - reference_code = json_data["reference_code"] + query = json_data["task_description"] + parameters = json_data["input_output_spec"] + reference_code = json_data["code_sample"] input_generator_code = json_data["input_generator"] - input_data, output_data = self._generate_io_pairs(reference_code, input_generator_code, num_pairs=1)[0] + input_data, output_data = self._generate_io_pair(reference_code, input_generator_code, rng) if rng.random() < self.config.input_prediction_probability: question = OUTPUT_PREDICTION_PROMPT_TEMPLATE.format(query, parameters, input_data, reference_code) @@ -113,7 +115,7 @@ def __getitem__(self, idx: int) -> dict: return { "question": question, "answer": solution, - "metadata": {}, + "metadata": {"input_data": input_data, "output_data": output_data}, } def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: @@ -142,15 +144,17 @@ def score_answer(self, answer: Optional[str], entry: dict[str, Any]) -> float: reward = 0.1 else: # At least we got a JSON object, I guess? - reward = 0.05 + reward = 0.01 except json.JSONDecodeError: if oracle_answer in answer: reward = len(oracle_answer) / len(answer) + else: + reward = 0.00 elif oracle_answer in answer: # max() to avoid penalising too heavily, since correct answers are short here reward = max(len(oracle_answer) / len(answer), 0.2) else: - reward = 0.01 + reward = 0.00 return reward diff --git a/reasoning_gym/data/codeio.jsonl.gz b/reasoning_gym/data/codeio.jsonl.gz index 19962396e10b238e9eb1526f5b54f490b43655b5..70ba8d9d91f9c677fd5c6ab7b5ff33749f2734b4 100644 GIT binary patch literal 186614 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
xGdEQ^*eY(sG6h&shrbr; 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�$iOw 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%0Dy~1aU$@O$`momkYb3iDdF;d({j?6c1z9%6iG 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