From fdb556fd331c36a9fdd693d9162427b056c65558 Mon Sep 17 00:00:00 2001 From: Schmeitzke Date: Thu, 30 Jan 2025 17:51:23 +0100 Subject: [PATCH 1/3] Implemented simple and advanced geomtry dataset makers, including test scripts --- reasoning_gym/geometry/__init__.py | 9 + reasoning_gym/geometry/advanced_geometry.py | 212 ++++++++++++++++++++ reasoning_gym/geometry/simple_geometry.py | 140 +++++++++++++ tests/test_advanced_geometry.py | 88 ++++++++ tests/test_simple_geometry.py | 92 +++++++++ 5 files changed, 541 insertions(+) create mode 100644 reasoning_gym/geometry/__init__.py create mode 100644 reasoning_gym/geometry/advanced_geometry.py create mode 100644 reasoning_gym/geometry/simple_geometry.py create mode 100644 tests/test_advanced_geometry.py create mode 100644 tests/test_simple_geometry.py diff --git a/reasoning_gym/geometry/__init__.py b/reasoning_gym/geometry/__init__.py new file mode 100644 index 00000000..d6539df1 --- /dev/null +++ b/reasoning_gym/geometry/__init__.py @@ -0,0 +1,9 @@ +from .simple_geometry import SimpleGeometryConfig, SimpleGeometryDataset +from .advanced_geometry import AdvancedGeometryConfig, AdvancedGeometryDataset + +__all__ = [ + "SimpleGeometryConfig", + "SimpleGeometryDataset", + "AdvancedGeometryConfig", + "AdvancedGeometryDataset", +] diff --git a/reasoning_gym/geometry/advanced_geometry.py b/reasoning_gym/geometry/advanced_geometry.py new file mode 100644 index 00000000..f8221614 --- /dev/null +++ b/reasoning_gym/geometry/advanced_geometry.py @@ -0,0 +1,212 @@ +import random +from dataclasses import dataclass +from typing import Optional, List + +import sympy +from sympy.geometry import Point, Triangle + +from ..factory import ProceduralDataset, register_dataset + + +@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] = None + + def __post_init__(self): + if self.task_types is None: + # Default set of advanced tasks + self.task_types = [ + "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." + + +class AdvancedGeometryDataset(ProceduralDataset): + """ + A dataset for advanced geometry tasks using coordinate geometry. + """ + + def __init__(self, config: AdvancedGeometryConfig): + self._prompt_templates = { + "orthocenter": [ + "Given triangle ABC with coordinates A={A}, B={B}, and C={C}, find the coordinates of its orthocenter.", + "For triangle with vertices A={A}, B={B}, and C={C}, determine the orthocenter (intersection of altitudes).", + ], + "incircle_radius": [ + "Consider triangle ABC with coordinates A={A}, B={B}, and C={C}. Compute the radius of its incircle.", + "Find the incircle radius of triangle ABC whose vertices are A={A}, B={B}, and C={C}.", + ], + "angle_measure": [ + "In triangle ABC with coordinates A={A}, B={B}, and C={C}, find the measure (in degrees) of angle ABC.", + "Given a triangle with vertices A={A}, B={B}, C={C}, determine the angle at B in degrees.", + ], + } + 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}") + + 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): + xA = rng.randint(self.config.min_coord, self.config.max_coord) + yA = rng.randint(self.config.min_coord, self.config.max_coord) + xB = rng.randint(self.config.min_coord, self.config.max_coord) + yB = rng.randint(self.config.min_coord, self.config.max_coord) + xC = rng.randint(self.config.min_coord, self.config.max_coord) + yC = rng.randint(self.config.min_coord, self.config.max_coord) + + A = Point(xA, yA) + B = Point(xB, yB) + C = Point(xC, yC) + tri = Triangle(A, B, C) + + # Check that the triangle is non-degenerate (area != 0) + if tri.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. + """ + tri = Triangle(A, B, C) + # Sympy can give altitudes or direct concurrency point + ortho = tri.orthocenter + # Format the answer + # The orthocenter may have rational coordinates, so let's convert to float or simplified fraction + # We'll store both numeric approximations and exact forms in metadata + x_ortho_approx = float(ortho.x.evalf()) + y_ortho_approx = float(ortho.y.evalf()) + + # Choose a prompt + 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) + ) + # Round to e.g. 3 decimals or keep a string representation + answer_str = f"({x_ortho_approx:.3f}, {y_ortho_approx:.3f})" + + metadata = { + "A": (A.x, A.y), + "B": (B.x, B.y), + "C": (C.x, C.y), + "orthocenter_exact": (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. + """ + tri = Triangle(A, B, C) + incircle = tri.incircle() + # incircle is a Circle object; radius is incircle.radius + radius = incircle.radius + + # 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": (A.x, A.y), + "B": (B.x, B.y), + "C": (C.x, 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) + ) + + answer_str = f"{angle_deg:.2f}°" + metadata = { + "A": (A.x, A.y), + "B": (B.x, B.y), + "C": (C.x, C.y), + "angle_ABC_degrees": angle_deg, + } + return question, answer_str, metadata + + +# Register the dataset +register_dataset("advanced_geometry", AdvancedGeometryDataset, AdvancedGeometryConfig) diff --git a/reasoning_gym/geometry/simple_geometry.py b/reasoning_gym/geometry/simple_geometry.py new file mode 100644 index 00000000..3714abe8 --- /dev/null +++ b/reasoning_gym/geometry/simple_geometry.py @@ -0,0 +1,140 @@ +import random +from dataclasses import dataclass +from typing import Optional + +from ..factory import ProceduralDataset, register_dataset + +@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)?" + ), + ( + "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." + ), + ( + "Consider a convex {n_sides}-gon whose first {n_minus_1} interior angles " + "are: {angle_list}. Determine the measure of the remaining angle." + ), + ] + 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": { + "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, + }, + } + + 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." + ) + +# Register the dataset so it can be accessed similarly to the others +register_dataset("simple_geometry", SimpleGeometryDataset, SimpleGeometryConfig) diff --git a/tests/test_advanced_geometry.py b/tests/test_advanced_geometry.py new file mode 100644 index 00000000..cf371d10 --- /dev/null +++ b/tests/test_advanced_geometry.py @@ -0,0 +1,88 @@ +import pytest + +from reasoning_gym.geometry.advanced_geometry import ( + AdvancedGeometryDataset, + AdvancedGeometryConfig, +) + +def test_advanced_geometry_config_validation(): + """Test that invalid configs raise appropriate errors.""" + # min_coord >= max_coord + with pytest.raises(AssertionError): + config = AdvancedGeometryConfig(min_coord=5, max_coord=5) + config.validate() + + with pytest.raises(AssertionError): + config = AdvancedGeometryConfig(min_coord=10, max_coord=0) + config.validate() + + # size <= 0 + with pytest.raises(AssertionError): + config = AdvancedGeometryConfig(size=0) + config.validate() + + # Empty task_types + with pytest.raises(AssertionError): + config = AdvancedGeometryConfig(task_types=[]) + config.validate() + + +def test_advanced_geometry_dataset_deterministic(): + """Test the dataset generates the same items with the same seed.""" + config = AdvancedGeometryConfig(min_coord=-5, max_coord=5, size=5, seed=42) + dataset1 = AdvancedGeometryDataset(config) + dataset2 = AdvancedGeometryDataset(config) + + for i in range(len(dataset1)): + assert dataset1[i] == dataset2[i], ( + f"Item mismatch at index {i} for same seed. " + f"Dataset1: {dataset1[i]} vs Dataset2: {dataset2[i]}" + ) + + +def test_advanced_geometry_dataset_items(): + """Test basic properties of generated items.""" + config = AdvancedGeometryConfig(min_coord=-3, max_coord=3, size=5, seed=123) + dataset = AdvancedGeometryDataset(config) + + for i in range(len(dataset)): + item = dataset[i] + # Check structure + assert isinstance(item, dict), "Generated item must be a dictionary." + assert "question" in item, "Item must contain a 'question' key." + assert "answer" in item, "Item must contain an 'answer' key." + assert "metadata" in item, "Item must contain a 'metadata' key." + + # Basic metadata checks + metadata = item["metadata"] + assert "A" in metadata and "B" in metadata and "C" in metadata, ( + "Metadata should contain coordinates for points A, B, and C." + ) + + # Check answer format depending on task type + # For angle measure tasks, answer should end with '°' + if "angle_measure" in item["question"].lower() or "angle at" in item["question"].lower(): + assert item["answer"].endswith("°"), ( + f"Expected angle measure in degrees, got {item['answer']}" + ) + + +def test_advanced_geometry_dataset_iteration(): + """Test that iteration respects dataset size and is repeatable.""" + config = AdvancedGeometryConfig(min_coord=-2, max_coord=2, size=3, seed=999) + dataset = AdvancedGeometryDataset(config) + + # Test manual iteration + items = [] + for item in dataset: + items.append(item) + assert len(items) == config.size, "Iterator should yield exactly 'size' items." + + # Test list conversion + items_list = list(dataset) + assert len(items_list) == config.size, "List conversion should yield exactly 'size' items." + + # Test multiple iterations produce the same results + first_items = list(dataset) + second_items = list(dataset) + assert first_items == second_items, "Multiple iterations should yield the same items." diff --git a/tests/test_simple_geometry.py b/tests/test_simple_geometry.py new file mode 100644 index 00000000..4d8702df --- /dev/null +++ b/tests/test_simple_geometry.py @@ -0,0 +1,92 @@ +import pytest + +from reasoning_gym.geometry.simple_geometry import ( + SimpleGeometryDataset, + SimpleGeometryConfig, +) + +def test_simple_geometry_config_validation(): + """Test invalid configs raise appropriate errors.""" + # min_sides < 3 + with pytest.raises(AssertionError): + config = SimpleGeometryConfig(min_sides=2, max_sides=5) + config.validate() + + # max_sides < min_sides + with pytest.raises(AssertionError): + config = SimpleGeometryConfig(min_sides=4, max_sides=3) + config.validate() + + # Invalid angles + with pytest.raises(AssertionError): + config = SimpleGeometryConfig(min_angle=-10) + config.validate() + + with pytest.raises(AssertionError): + config = SimpleGeometryConfig(min_angle=10, max_angle=5) + config.validate() + + +def test_simple_geometry_dataset_deterministic(): + """Test the dataset generates the same items with the same seed.""" + config = SimpleGeometryConfig(seed=42, size=5, min_sides=3, max_sides=4) + dataset1 = SimpleGeometryDataset(config) + dataset2 = SimpleGeometryDataset(config) + + for i in range(len(dataset1)): + assert dataset1[i] == dataset2[i], ( + f"Item mismatch at index {i} for same seed. " + f"Dataset1: {dataset1[i]} vs Dataset2: {dataset2[i]}" + ) + + +def test_simple_geometry_dataset_items(): + """Test basic properties of generated items.""" + config = SimpleGeometryConfig( + min_sides=3, + max_sides=5, + min_angle=10, + max_angle=120, + size=10, + seed=123 + ) + dataset = SimpleGeometryDataset(config) + + for i in range(len(dataset)): + item = dataset[i] + # Check structure + assert isinstance(item, dict), "Generated item must be a dictionary." + assert "question" in item, "Item must contain a 'question' key." + assert "answer" in item, "Item must contain an 'answer' key." + assert "metadata" in item, "Item must contain a 'metadata' key." + + metadata = item["metadata"] + assert "n_sides" in metadata, "Metadata should contain 'n_sides'." + assert "missing_angle_rounded" in metadata, ( + "Metadata should contain the computed 'missing_angle_rounded'." + ) + + # Check that the missing angle is a valid float or integer + missing_angle = float(item["answer"]) + assert missing_angle > 0, f"Missing angle should be positive, found {missing_angle}" + + +def test_simple_geometry_dataset_iteration(): + """Test that iteration respects dataset size and is repeatable.""" + config = SimpleGeometryConfig(min_sides=3, max_sides=4, size=5, seed=42) + dataset = SimpleGeometryDataset(config) + + # Test manual iteration + items = [] + for item in dataset: + items.append(item) + assert len(items) == config.size, "Iterator should yield exactly 'size' items." + + # Test list conversion + items_list = list(dataset) + assert len(items_list) == config.size, "List conversion should yield exactly 'size' items." + + # Test multiple iterations produce the same results + first_items = list(dataset) + second_items = list(dataset) + assert first_items == second_items, "Multiple iterations should yield the same items." From 1af455b42455db826d80d5a9789ad72e59ec1bd1 Mon Sep 17 00:00:00 2001 From: Schmeitzke Date: Fri, 31 Jan 2025 14:14:10 +0100 Subject: [PATCH 2/3] Bug fix for segment object --- reasoning_gym/geometry/advanced_geometry.py | 72 ++++++++++++++------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/reasoning_gym/geometry/advanced_geometry.py b/reasoning_gym/geometry/advanced_geometry.py index f8221614..ca098b55 100644 --- a/reasoning_gym/geometry/advanced_geometry.py +++ b/reasoning_gym/geometry/advanced_geometry.py @@ -3,7 +3,7 @@ from typing import Optional, List import sympy -from sympy.geometry import Point, Triangle +from sympy.geometry import Point, Triangle, Segment from ..factory import ProceduralDataset, register_dataset @@ -92,20 +92,24 @@ def _generate_non_degenerate_triangle(self, rng: random.Random): """ max_attempts = 100 for _ in range(max_attempts): - xA = rng.randint(self.config.min_coord, self.config.max_coord) - yA = rng.randint(self.config.min_coord, self.config.max_coord) - xB = rng.randint(self.config.min_coord, self.config.max_coord) - yB = rng.randint(self.config.min_coord, self.config.max_coord) - xC = rng.randint(self.config.min_coord, self.config.max_coord) - yC = rng.randint(self.config.min_coord, self.config.max_coord) - - A = Point(xA, yA) - B = Point(xB, yB) - C = Point(xC, yC) - tri = Triangle(A, B, C) - - # Check that the triangle is non-degenerate (area != 0) - if tri.area != 0: + # 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.") @@ -114,12 +118,21 @@ def _build_orthocenter_task(self, rng: random.Random, A: Point, B: Point, C: Poi """ Build a question about finding the orthocenter of triangle ABC. """ - tri = Triangle(A, B, C) - # Sympy can give altitudes or direct concurrency point - ortho = tri.orthocenter - # Format the answer - # The orthocenter may have rational coordinates, so let's convert to float or simplified fraction - # We'll store both numeric approximations and exact forms in metadata + # Create line segments for the sides + AB = Segment(A, B) + BC = Segment(B, C) + CA = Segment(C, A) + + # Calculate altitudes + # Get perpendicular lines from each vertex to the opposite side + alt_A = A.perpendicular_line(BC) + alt_B = B.perpendicular_line(CA) + alt_C = C.perpendicular_line(AB) + + # Find orthocenter (intersection of any two altitudes) + ortho = alt_A.intersection(alt_B)[0] + + # Format coordinates x_ortho_approx = float(ortho.x.evalf()) y_ortho_approx = float(ortho.y.evalf()) @@ -144,10 +157,19 @@ def _build_incircle_radius_task(self, rng: random.Random, A: Point, B: Point, C: """ Build a question about finding the incircle radius of triangle ABC. """ - tri = Triangle(A, B, C) - incircle = tri.incircle() - # incircle is a Circle object; radius is incircle.radius - radius = incircle.radius + # 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()) From 3578884c42e7bcc1b18eb8f0f25a3fb220cec839 Mon Sep 17 00:00:00 2001 From: Schmeitzke Date: Fri, 31 Jan 2025 14:20:55 +0100 Subject: [PATCH 3/3] Bug fix for absent method in sympy --- reasoning_gym/geometry/advanced_geometry.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/reasoning_gym/geometry/advanced_geometry.py b/reasoning_gym/geometry/advanced_geometry.py index ca098b55..3beae559 100644 --- a/reasoning_gym/geometry/advanced_geometry.py +++ b/reasoning_gym/geometry/advanced_geometry.py @@ -118,30 +118,24 @@ def _build_orthocenter_task(self, rng: random.Random, A: Point, B: Point, C: Poi """ Build a question about finding the orthocenter of triangle ABC. """ - # Create line segments for the sides - AB = Segment(A, B) - BC = Segment(B, C) - CA = Segment(C, A) + # Convert segments to lines + BC_line = sympy.Line(B, C) + CA_line = sympy.Line(C, A) - # Calculate altitudes - # Get perpendicular lines from each vertex to the opposite side - alt_A = A.perpendicular_line(BC) - alt_B = B.perpendicular_line(CA) - alt_C = C.perpendicular_line(AB) + # 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) + # Find orthocenter (intersection of any two altitudes, e.g. alt_A and alt_B) ortho = alt_A.intersection(alt_B)[0] - # Format coordinates x_ortho_approx = float(ortho.x.evalf()) y_ortho_approx = float(ortho.y.evalf()) - # Choose a prompt 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) ) - # Round to e.g. 3 decimals or keep a string representation answer_str = f"({x_ortho_approx:.3f}, {y_ortho_approx:.3f})" metadata = { @@ -153,6 +147,7 @@ def _build_orthocenter_task(self, rng: random.Random, A: Point, B: Point, C: Poi } 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.