mirror of
https://github.com/open-thought/reasoning-gym.git
synced 2026-04-19 12:58:07 +00:00
* make sympy-based task entries json serializable * remove datetime objs from time_intervals metadata * make adv geometry json serializable * make futoshiki metadata json serializable * fixes * futoshiki tweaks * fix adv geometry * deal with fractions in str representations * fix * restore start_time, end_time as str
307 lines
12 KiB
Python
307 lines
12 KiB
Python
import random
|
|
from dataclasses import dataclass, field
|
|
from typing import Any, Optional
|
|
|
|
import numpy as np
|
|
import sympy
|
|
from sympy.geometry import Point
|
|
|
|
from ..coaching import BaseCurriculum, ScalarAttributeDefinition
|
|
from ..factory import ProceduralDataset, register_dataset
|
|
|
|
DATASET_NAME = "advanced_geometry"
|
|
|
|
|
|
@dataclass
|
|
class AdvancedGeometryConfig:
|
|
"""
|
|
Configuration for generating advanced geometry tasks.
|
|
"""
|
|
|
|
min_coord: int = -10 # Minimum x/y coordinate
|
|
max_coord: int = 10 # Maximum x/y coordinate
|
|
size: int = 50 # Number of problems to generate
|
|
seed: Optional[int] = None
|
|
|
|
# Probability or list of tasks we want to generate
|
|
# For demonstration, we have three categories:
|
|
task_types: list[str] = field(
|
|
default_factory=lambda: [
|
|
"orthocenter",
|
|
"incircle_radius",
|
|
"angle_measure",
|
|
]
|
|
)
|
|
|
|
def validate(self):
|
|
assert self.min_coord < self.max_coord, "min_coord must be < max_coord."
|
|
assert self.size > 0, "Size of dataset must be positive."
|
|
assert len(self.task_types) > 0, "Must specify at least one task type."
|
|
|
|
|
|
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):
|
|
"""
|
|
A dataset for advanced geometry tasks using coordinate geometry.
|
|
"""
|
|
|
|
def __init__(self, config: AdvancedGeometryConfig):
|
|
self._prompt_templates = {
|
|
"orthocenter": [
|
|
f"Given triangle ABC with coordinates A={{A}}, B={{B}}, and C={{C}}, find the coordinates of its orthocenter. {GEOMETRY_FORMAT_INSTRUCTIONS}",
|
|
f"For triangle with vertices A={{A}}, B={{B}}, and C={{C}}, determine the orthocenter (intersection of altitudes). {GEOMETRY_FORMAT_INSTRUCTIONS}",
|
|
],
|
|
"incircle_radius": [
|
|
f"Consider triangle ABC with coordinates A={{A}}, B={{B}}, and C={{C}}. Compute the radius of its incircle. {GEOMETRY_FORMAT_INSTRUCTIONS}",
|
|
f"Find the incircle radius of triangle ABC whose vertices are A={{A}}, B={{B}}, and C={{C}}. {GEOMETRY_FORMAT_INSTRUCTIONS}",
|
|
],
|
|
"angle_measure": [
|
|
f"In triangle ABC with coordinates A={{A}}, B={{B}}, and C={{C}}, find the measure (in degrees) of angle ABC. {GEOMETRY_FORMAT_INSTRUCTIONS}",
|
|
f"Given a triangle with vertices A={{A}}, B={{B}}, and C={{C}}, determine the angle at B in degrees. {GEOMETRY_FORMAT_INSTRUCTIONS}",
|
|
],
|
|
}
|
|
super().__init__(config=config, seed=config.seed, size=config.size)
|
|
|
|
def __getitem__(self, idx: int) -> dict:
|
|
"""
|
|
Generate a single advanced geometry item based on the config's task types.
|
|
"""
|
|
rng = random.Random(self.seed + idx)
|
|
task_type = rng.choice(self.config.task_types)
|
|
|
|
# Randomly generate coordinates for a triangle
|
|
A, B, C = self._generate_non_degenerate_triangle(rng)
|
|
|
|
# Build a question and compute the solution
|
|
if task_type == "orthocenter":
|
|
question, answer, metadata = self._build_orthocenter_task(rng, A, B, C)
|
|
elif task_type == "incircle_radius":
|
|
question, answer, metadata = self._build_incircle_radius_task(rng, A, B, C)
|
|
elif task_type == "angle_measure":
|
|
question, answer, metadata = self._build_angle_measure_task(rng, A, B, C)
|
|
else:
|
|
raise ValueError(f"Unknown task_type: {task_type}")
|
|
|
|
metadata["source_dataset"] = DATASET_NAME
|
|
metadata["source_index"] = idx
|
|
metadata["task_type"] = task_type
|
|
metadata["difficulty"] = {
|
|
"min_coord": self.config.min_coord,
|
|
"max_coord": self.config.max_coord,
|
|
}
|
|
|
|
return {
|
|
"question": question,
|
|
"answer": answer,
|
|
"metadata": metadata,
|
|
}
|
|
|
|
def _generate_non_degenerate_triangle(self, rng: random.Random):
|
|
"""
|
|
Generate a random non-degenerate triangle with integer coordinates
|
|
in [min_coord, max_coord] x [min_coord, max_coord].
|
|
"""
|
|
max_attempts = 100
|
|
for _ in range(max_attempts):
|
|
# Generate points with integer coordinates
|
|
points = []
|
|
for _ in range(3):
|
|
x = rng.randint(self.config.min_coord, self.config.max_coord)
|
|
y = rng.randint(self.config.min_coord, self.config.max_coord)
|
|
points.append(Point(x, y))
|
|
|
|
A, B, C = points
|
|
|
|
# Calculate signed area to check for non-degeneracy
|
|
# Using the formula: 1/2 * |x1(y2 - y3) + x2(y3 - y1) + x3(y1 - y2)|
|
|
area = abs(A.x * (B.y - C.y) + B.x * (C.y - A.y) + C.x * (A.y - B.y)) / 2
|
|
|
|
if area > 0:
|
|
return A, B, C
|
|
|
|
raise ValueError(f"Failed to generate a non-degenerate triangle after {max_attempts} attempts.")
|
|
|
|
def _build_orthocenter_task(self, rng: random.Random, A: Point, B: Point, C: Point):
|
|
"""
|
|
Build a question about finding the orthocenter of triangle ABC.
|
|
"""
|
|
# Convert segments to lines
|
|
BC_line = sympy.Line(B, C)
|
|
CA_line = sympy.Line(C, A)
|
|
|
|
# Calculate altitudes by creating lines perpendicular from each vertex
|
|
alt_A = BC_line.perpendicular_line(A)
|
|
alt_B = CA_line.perpendicular_line(B)
|
|
|
|
# Find orthocenter (intersection of any two altitudes, e.g. alt_A and alt_B)
|
|
ortho = alt_A.intersection(alt_B)[0]
|
|
|
|
x_ortho_approx = float(ortho.x.evalf())
|
|
y_ortho_approx = float(ortho.y.evalf())
|
|
|
|
question_template = rng.choice(self._prompt_templates["orthocenter"])
|
|
question = question_template.format(A=(A.x, A.y), B=(B.x, B.y), C=(C.x, C.y), a="a", b="b")
|
|
answer_str = f"({x_ortho_approx:.3f}, {y_ortho_approx:.3f})"
|
|
metadata = {
|
|
"A": (str(A.x), str(A.y)),
|
|
"B": (str(B.x), str(B.y)),
|
|
"C": (str(C.x), str(C.y)),
|
|
"ortho": (str(ortho.x), str(ortho.y)),
|
|
"orthocenter_approx": (x_ortho_approx, y_ortho_approx),
|
|
}
|
|
return question, answer_str, metadata
|
|
|
|
def _build_incircle_radius_task(self, rng: random.Random, A: Point, B: Point, C: Point):
|
|
"""
|
|
Build a question about finding the incircle radius of triangle ABC.
|
|
"""
|
|
# Calculate side lengths
|
|
a = B.distance(C)
|
|
b = C.distance(A)
|
|
c = A.distance(B)
|
|
|
|
# Semi-perimeter
|
|
s = (a + b + c) / 2
|
|
|
|
# Area using Heron's formula
|
|
area = sympy.sqrt(s * (s - a) * (s - b) * (s - c))
|
|
|
|
# Radius of incircle = Area / Semi-perimeter
|
|
radius = area / s
|
|
|
|
# Convert to float for final answer
|
|
radius_approx = float(radius.evalf())
|
|
|
|
question_template = rng.choice(self._prompt_templates["incircle_radius"])
|
|
question = question_template.format(A=(A.x, A.y), B=(B.x, B.y), C=(C.x, C.y))
|
|
answer_str = f"{radius_approx:.3f}"
|
|
|
|
metadata = {
|
|
"A": (str(A.x), str(A.y)),
|
|
"B": (str(B.x), str(B.y)),
|
|
"C": (str(C.x), str(C.y)),
|
|
"incircle_radius_exact": str(radius),
|
|
"incircle_radius_approx": radius_approx,
|
|
}
|
|
return question, answer_str, metadata
|
|
|
|
def _build_angle_measure_task(self, rng: random.Random, A: Point, B: Point, C: Point):
|
|
"""
|
|
Build a question about finding the measure of angle ABC in degrees.
|
|
"""
|
|
# Angle at B means the angle ∠ABC
|
|
# Vector BA = A - B, BC = C - B
|
|
BA = A - B
|
|
BC = C - B
|
|
|
|
# Use vector dot product to find angle between BA and BC
|
|
# angle = arccos((BA · BC) / (|BA| * |BC|))
|
|
dot_val = BA.dot(BC)
|
|
mag_ba = BA.distance(Point(0, 0))
|
|
mag_bc = BC.distance(Point(0, 0))
|
|
|
|
# numerical check
|
|
if mag_ba == 0 or mag_bc == 0:
|
|
# degenerate, but theoretically we forced a non-degenerate triangle
|
|
angle_deg = 0
|
|
else:
|
|
cos_theta = dot_val / (mag_ba * mag_bc)
|
|
# clamp cos_theta to [-1, 1] to avoid floating rounding errors
|
|
cos_theta = max(-1, min(1, cos_theta))
|
|
angle_rad = sympy.acos(cos_theta)
|
|
angle_deg = float(angle_rad.evalf() * 180 / sympy.pi)
|
|
|
|
question_template = rng.choice(self._prompt_templates["angle_measure"])
|
|
question = question_template.format(A=(A.x, A.y), B=(B.x, B.y), C=(C.x, C.y), a="a", b="b")
|
|
|
|
answer_str = f"{angle_deg:.2f}°"
|
|
metadata = {
|
|
"A": (str(A.x), str(A.y)),
|
|
"B": (str(B.x), str(B.y)),
|
|
"C": (str(C.x), str(C.y)),
|
|
"angle_ABC_degrees": angle_deg,
|
|
}
|
|
return question, answer_str, metadata
|
|
|
|
def score_answer(self, answer: str | None, entry: dict[str, Any]) -> float:
|
|
reward = 0.0
|
|
expected_answer = entry["answer"]
|
|
metadata = entry["metadata"]
|
|
task_type = metadata["task_type"]
|
|
|
|
if answer is not None:
|
|
try:
|
|
if metadata["task_type"] == "angle_measure":
|
|
answer = answer.replace("\u00b0", "")
|
|
expected_answer = expected_answer.replace("\u00b0", "")
|
|
if np.round(float(answer), 2) == np.round(float(expected_answer), 2):
|
|
reward = 1.0
|
|
elif len(answer.strip()) > 0:
|
|
reward = 0.05
|
|
else:
|
|
reward = 0.01
|
|
elif metadata["task_type"] == "orthocenter":
|
|
x_coord = float(answer.split(",")[0].replace("(", "").strip())
|
|
y_coord = float(answer.split(",")[1].replace(")", "").strip())
|
|
|
|
ortho_0, ortho_1 = metadata["ortho"]
|
|
if "/" in ortho_0:
|
|
ortho_0 = int(ortho_0.split("/")[0]) / int(ortho_0.split("/")[1])
|
|
if "/" in ortho_1:
|
|
ortho_1 = int(ortho_1.split("/")[0]) / int(ortho_1.split("/")[1])
|
|
|
|
expected_x = float(ortho_0)
|
|
expected_y = float(ortho_1)
|
|
if x_coord == expected_x and y_coord == expected_y:
|
|
reward = 1.0
|
|
elif (np.round(x_coord, 3) == np.round(expected_x, 3)) and (
|
|
np.round(y_coord, 3) == np.round(expected_y, 3)
|
|
):
|
|
reward = 1.0
|
|
else:
|
|
reward = 0.01
|
|
elif metadata["task_type"] == "incircle_radius":
|
|
if answer == expected_answer:
|
|
reward = 1.0
|
|
elif np.round(float(answer), 2) == np.round(float(metadata["incircle_radius_exact"]), 2):
|
|
reward = 0.5
|
|
elif len(answer.strip()) > 0:
|
|
reward = 0.05
|
|
else:
|
|
reward = 0.01
|
|
else:
|
|
raise ValueError(f"Unknown task_type: {task_type}")
|
|
except Exception as e:
|
|
reward = 0.01
|
|
return reward
|
|
|
|
|
|
class AdvancedGeometryCurriculum(BaseCurriculum):
|
|
def __init__(self):
|
|
super().__init__(AdvancedGeometryCurriculum.__name__, AdvancedGeometryConfig)
|
|
|
|
self._define_attributes(
|
|
ScalarAttributeDefinition(
|
|
name="min_coord",
|
|
field_name="min_coord",
|
|
levels=[-10, -100, -1000, -10000],
|
|
description="Minimum x/y coordinate",
|
|
),
|
|
ScalarAttributeDefinition(
|
|
name="max_coord",
|
|
field_name="max_coord",
|
|
levels=[10, 100, 1000, 10000],
|
|
description="Maximum x/y coordinate",
|
|
),
|
|
)
|
|
|
|
|
|
# Register the dataset
|
|
register_dataset(DATASET_NAME, AdvancedGeometryDataset, AdvancedGeometryConfig, AdvancedGeometryCurriculum)
|