mirror of
https://github.com/open-thought/reasoning-gym.git
synced 2026-04-19 12:58:07 +00:00
* init * fix tests * unify codeio * filtered for libraries not present in reasoning-gym * fix more bounds * puzzle24 * knight swap curriculum * fix number sorting * fix attributes * add validation of config in creation of dataset * dry run for instantiating and validating the datasets * remove unused imports * fix curriculum tests to reference newly updated attribute names
171 lines
6.9 KiB
Python
171 lines
6.9 KiB
Python
import random
|
|
from dataclasses import dataclass
|
|
from typing import Optional
|
|
|
|
from ..coaching import BaseCurriculum, RangeAttributeDefinition
|
|
from ..factory import ProceduralDataset, register_dataset
|
|
|
|
DATASET_NAME = "simple_geometry"
|
|
|
|
|
|
@dataclass
|
|
class SimpleGeometryConfig:
|
|
"""
|
|
Configuration for generating basic geometry (angle-finding) tasks.
|
|
Produces a random convex polygon with N sides, random angles
|
|
for the first (N-1) sides, and asks the solver to find the last angle.
|
|
"""
|
|
|
|
min_sides: int = 3 # Minimum number of sides (e.g. triangle)
|
|
max_sides: int = 6 # Maximum number of sides (e.g. hexagon)
|
|
min_angle: int = 10 # Minimum angle (in degrees) for each of the first (N-1) angles
|
|
max_angle: int = 170 # Maximum angle (in degrees) for each of the first (N-1) angles
|
|
seed: Optional[int] = None # Random seed
|
|
size: int = 100 # Number of geometry tasks to generate
|
|
|
|
def validate(self) -> None:
|
|
"""
|
|
Validate configuration parameters.
|
|
"""
|
|
assert self.min_sides >= 3, "min_sides must be at least 3 (triangle)."
|
|
assert self.max_sides >= self.min_sides, "max_sides must be >= min_sides."
|
|
assert 0 < self.min_angle < 180, "min_angle must be in (0, 180)."
|
|
assert self.max_angle <= 179, "max_angle should be less than 180."
|
|
assert self.max_angle >= self.min_angle, "max_angle must be >= min_angle."
|
|
|
|
|
|
class SimpleGeometryDataset(ProceduralDataset):
|
|
"""
|
|
A dataset for simple polygon angle-finding tasks.
|
|
We randomly choose the number of sides N within [min_sides, max_sides].
|
|
We then generate (N-1) random angles (in degrees), ensuring their sum is
|
|
strictly less than the total sum for an (N)-sided convex polygon (which is 180*(N-2)).
|
|
The question asks for the missing angle; the answer is computed by subtracting the
|
|
sum of known angles from 180*(N-2).
|
|
"""
|
|
|
|
def __init__(self, config: SimpleGeometryConfig):
|
|
self._prompt_templates = [
|
|
(
|
|
"Given a convex polygon with {n_sides} sides, its first {n_minus_1} interior angles "
|
|
"are: {angle_list}. What is the measure of the remaining interior angle (in degrees)?"
|
|
"Return only the angle as your answer."
|
|
"Do not give the units in your answer."
|
|
),
|
|
(
|
|
"A convex polygon has {n_sides} sides. The measures of "
|
|
"the first {n_minus_1} interior angles are: {angle_list}. "
|
|
"Find the measure of the last interior angle."
|
|
"Return only the angle as your answer."
|
|
"Do not give the units in your answer."
|
|
),
|
|
(
|
|
"Consider a convex {n_sides}-gon whose first {n_minus_1} interior angles "
|
|
"are: {angle_list}. Determine the measure of the remaining angle."
|
|
"Return only the angle as your answer."
|
|
"Do not give the units in your answer."
|
|
),
|
|
]
|
|
super().__init__(config=config, seed=config.seed, size=config.size)
|
|
|
|
def __getitem__(self, idx: int) -> dict:
|
|
"""
|
|
Generate a single geometry angle-finding item.
|
|
|
|
Returns:
|
|
A dict with:
|
|
- question: str
|
|
- answer: str (the missing angle, as an integer or float in degrees)
|
|
- metadata: dict (n_sides, angles, sum_of_known, missing_angle, etc.)
|
|
"""
|
|
rng = random.Random(self.seed + idx)
|
|
|
|
# Randomly pick the number of sides
|
|
n_sides = rng.randint(self.config.min_sides, self.config.max_sides)
|
|
|
|
# Total interior angle sum for a convex n_sides-gon
|
|
total_sum = 180 * (n_sides - 2)
|
|
|
|
# Generate (n_sides - 1) random angles, ensuring their sum < total_sum
|
|
known_angles = self._generate_valid_angles(rng, n_sides, total_sum)
|
|
|
|
# Missing angle
|
|
missing_angle = total_sum - sum(known_angles)
|
|
|
|
# Build the question string
|
|
angle_list_str = ", ".join(f"{a:.1f}°" for a in known_angles)
|
|
prompt = rng.choice(self._prompt_templates).format(
|
|
n_sides=n_sides, n_minus_1=n_sides - 1, angle_list=angle_list_str
|
|
)
|
|
|
|
# Round the missing angle to one decimal place or integer if it is very close to an integer
|
|
# so that the answer remains consistent and clean
|
|
missing_angle_rounded = round(missing_angle, 1)
|
|
if abs(missing_angle_rounded - round(missing_angle_rounded)) < 1e-6:
|
|
# If it is effectively an integer, keep it as int
|
|
missing_angle_rounded = int(missing_angle_rounded)
|
|
|
|
answer_str = str(missing_angle_rounded)
|
|
|
|
return {
|
|
"question": prompt,
|
|
"answer": answer_str,
|
|
"metadata": {
|
|
"source_dataset": DATASET_NAME,
|
|
"source_index": idx,
|
|
"n_sides": n_sides,
|
|
"known_angles": known_angles,
|
|
"sum_of_known_angles": sum(known_angles),
|
|
"missing_angle_raw": missing_angle,
|
|
"missing_angle_rounded": missing_angle_rounded,
|
|
"total_interior_sum": total_sum,
|
|
"difficulty": {
|
|
"sides": (self.config.min_sides, self.config.max_sides),
|
|
},
|
|
},
|
|
}
|
|
|
|
def _generate_valid_angles(self, rng: random.Random, n_sides: int, total_sum: int):
|
|
"""
|
|
Generate (n_sides - 1) random angles in [min_angle, max_angle],
|
|
ensuring the sum is strictly less than total_sum to keep a valid missing angle.
|
|
We keep retrying until we find a valid set or reach a max attempt limit.
|
|
"""
|
|
max_attempts = 100
|
|
for _ in range(max_attempts):
|
|
angles = []
|
|
# We choose angles one by one
|
|
for _ in range(n_sides - 1):
|
|
angle = rng.randint(self.config.min_angle, self.config.max_angle)
|
|
angles.append(float(angle))
|
|
|
|
# Check if the sum is strictly less than total_sum
|
|
if sum(angles) < total_sum:
|
|
return angles
|
|
|
|
# If we fail after max_attempts, raise an error
|
|
raise ValueError(
|
|
f"Could not generate valid angles for an {n_sides}-gon "
|
|
f"with total sum {total_sum} within {max_attempts} attempts."
|
|
)
|
|
|
|
|
|
class SimpleGeometryCurriculum(BaseCurriculum):
|
|
def __init__(self):
|
|
super().__init__(SimpleGeometryCurriculum.__name__, SimpleGeometryConfig)
|
|
|
|
# Define attributes
|
|
self._define_attributes(
|
|
RangeAttributeDefinition(
|
|
name="sides",
|
|
levels=[5, 10, 15, 30],
|
|
description="Number of sides in the polygon.",
|
|
lower_field_name="min_sides",
|
|
upper_field_name="max_sides",
|
|
ensure_interval=True,
|
|
)
|
|
)
|
|
|
|
|
|
# Register the dataset so it can be accessed similarly to the others
|
|
register_dataset(DATASET_NAME, SimpleGeometryDataset, SimpleGeometryConfig, SimpleGeometryCurriculum)
|