Spaces:
Sleeping
Sleeping
Update quant_solver.py
Browse files- quant_solver.py +184 -29
quant_solver.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
| 2 |
|
| 3 |
import math
|
| 4 |
import re
|
|
|
|
| 5 |
from statistics import mean, median
|
| 6 |
from typing import Dict, List, Optional
|
| 7 |
|
|
@@ -62,7 +63,7 @@ def is_quant_question(text: str) -> bool:
|
|
| 62 |
"triangle", "circle", "rectangle", "perimeter", "area", "volume",
|
| 63 |
"number line", "positive", "negative", "multiple", "factor", "prime",
|
| 64 |
"distance", "speed", "work", "mixture", "consecutive", "algebra",
|
| 65 |
-
"value of x", "value of y", "what is x", "what is y"
|
| 66 |
]
|
| 67 |
if any(k in lower for k in quant_keywords):
|
| 68 |
return True
|
|
@@ -100,6 +101,49 @@ def parse_direct_expression_question(text: str) -> Optional[str]:
|
|
| 100 |
return expr
|
| 101 |
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
def solve_basic_expression(text: str, help_mode: str) -> Optional[SolverResult]:
|
| 104 |
expr = parse_direct_expression_question(text)
|
| 105 |
if not expr:
|
|
@@ -121,22 +165,37 @@ def solve_basic_expression(text: str, help_mode: str) -> Optional[SolverResult]:
|
|
| 121 |
|
| 122 |
def solve_percent_question(text: str, help_mode: str) -> Optional[SolverResult]:
|
| 123 |
lower = text.lower()
|
|
|
|
| 124 |
|
|
|
|
| 125 |
m = re.search(r"what is\s+(\d+(?:\.\d+)?)\s*%\s+of\s+(\d+(?:\.\d+)?)", lower)
|
| 126 |
if m:
|
| 127 |
p = float(m.group(1))
|
| 128 |
n = float(m.group(2))
|
| 129 |
ans = p / 100 * n
|
|
|
|
| 130 |
|
| 131 |
if help_mode == "hint":
|
| 132 |
reply = "Convert the percent to a decimal, then multiply."
|
| 133 |
elif help_mode == "walkthrough":
|
| 134 |
reply = f"{p}% of {n} = {p/100:g} × {n} = {ans:g}."
|
|
|
|
|
|
|
| 135 |
else:
|
| 136 |
reply = f"The answer is {ans:g}."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
-
|
| 139 |
-
|
| 140 |
m = re.search(r"(\d+(?:\.\d+)?)\s+is\s+what percent of\s+(\d+(?:\.\d+)?)", lower)
|
| 141 |
if m:
|
| 142 |
x = float(m.group(1))
|
|
@@ -155,6 +214,39 @@ def solve_percent_question(text: str, help_mode: str) -> Optional[SolverResult]:
|
|
| 155 |
|
| 156 |
return SolverResult(reply=reply, domain="quant", solved=True, help_mode=help_mode, answer_value=f"{ans:g}%")
|
| 157 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
m = re.search(
|
| 159 |
r"(\d+(?:\.\d+)?)\s+(?:is\s+)?(increased|decreased)\s+by\s+(\d+(?:\.\d+)?)\s*%",
|
| 160 |
lower,
|
|
@@ -280,6 +372,48 @@ def solve_probability_question(text: str, help_mode: str) -> Optional[SolverResu
|
|
| 280 |
|
| 281 |
def solve_geometry_question(text: str, help_mode: str) -> Optional[SolverResult]:
|
| 282 |
lower = text.lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
|
| 284 |
m = re.search(r"rectangle.*?length\s*(?:is|=)?\s*(\d+(?:\.\d+)?)\D+width\s*(?:is|=)?\s*(\d+(?:\.\d+)?)", lower)
|
| 285 |
if m and "area" in lower:
|
|
@@ -328,6 +462,50 @@ def solve_geometry_question(text: str, help_mode: str) -> Optional[SolverResult]
|
|
| 328 |
return None
|
| 329 |
|
| 330 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
def solve_linear_equation(text: str, help_mode: str) -> Optional[SolverResult]:
|
| 332 |
if sp is None:
|
| 333 |
return None
|
|
@@ -370,30 +548,6 @@ def solve_linear_equation(text: str, help_mode: str) -> Optional[SolverResult]:
|
|
| 370 |
return None
|
| 371 |
|
| 372 |
|
| 373 |
-
def compare_to_choices_numeric(answer_value: float, choices: Dict[str, str]) -> Optional[str]:
|
| 374 |
-
best_letter = None
|
| 375 |
-
best_diff = float("inf")
|
| 376 |
-
|
| 377 |
-
for letter, raw in choices.items():
|
| 378 |
-
expr = raw.strip()
|
| 379 |
-
expr_val = try_eval_expression(expr)
|
| 380 |
-
|
| 381 |
-
if expr_val is None:
|
| 382 |
-
nums = extract_numbers(expr)
|
| 383 |
-
if len(nums) == 1:
|
| 384 |
-
expr_val = nums[0]
|
| 385 |
-
|
| 386 |
-
if expr_val is None:
|
| 387 |
-
continue
|
| 388 |
-
|
| 389 |
-
diff = abs(expr_val - answer_value)
|
| 390 |
-
if diff < best_diff:
|
| 391 |
-
best_diff = diff
|
| 392 |
-
best_letter = letter
|
| 393 |
-
|
| 394 |
-
return best_letter
|
| 395 |
-
|
| 396 |
-
|
| 397 |
def solve_by_option_checking(text: str, help_mode: str) -> Optional[SolverResult]:
|
| 398 |
choices = extract_choices(text)
|
| 399 |
if len(choices) < 3:
|
|
@@ -440,6 +594,7 @@ def solve_quant(text: str, help_mode: str) -> SolverResult:
|
|
| 440 |
solve_average_question,
|
| 441 |
solve_probability_question,
|
| 442 |
solve_geometry_question,
|
|
|
|
| 443 |
solve_linear_equation,
|
| 444 |
solve_basic_expression,
|
| 445 |
]
|
|
@@ -454,8 +609,8 @@ def solve_quant(text: str, help_mode: str) -> SolverResult:
|
|
| 454 |
|
| 455 |
if help_mode == "hint":
|
| 456 |
reply = (
|
| 457 |
-
"Identify the question type first
|
| 458 |
-
"
|
| 459 |
)
|
| 460 |
elif help_mode == "walkthrough":
|
| 461 |
reply = (
|
|
|
|
| 2 |
|
| 3 |
import math
|
| 4 |
import re
|
| 5 |
+
from fractions import Fraction
|
| 6 |
from statistics import mean, median
|
| 7 |
from typing import Dict, List, Optional
|
| 8 |
|
|
|
|
| 63 |
"triangle", "circle", "rectangle", "perimeter", "area", "volume",
|
| 64 |
"number line", "positive", "negative", "multiple", "factor", "prime",
|
| 65 |
"distance", "speed", "work", "mixture", "consecutive", "algebra",
|
| 66 |
+
"value of x", "value of y", "what is x", "what is y", "divisible"
|
| 67 |
]
|
| 68 |
if any(k in lower for k in quant_keywords):
|
| 69 |
return True
|
|
|
|
| 101 |
return expr
|
| 102 |
|
| 103 |
|
| 104 |
+
def compare_to_choices_numeric(answer_value: float, choices: Dict[str, str], tolerance: float = 1e-9) -> Optional[str]:
|
| 105 |
+
best_letter = None
|
| 106 |
+
best_diff = float("inf")
|
| 107 |
+
|
| 108 |
+
for letter, raw in choices.items():
|
| 109 |
+
expr = raw.strip()
|
| 110 |
+
expr_val = try_eval_expression(expr)
|
| 111 |
+
|
| 112 |
+
if expr_val is None:
|
| 113 |
+
nums = extract_numbers(expr)
|
| 114 |
+
if len(nums) == 1:
|
| 115 |
+
expr_val = nums[0]
|
| 116 |
+
|
| 117 |
+
if expr_val is None:
|
| 118 |
+
continue
|
| 119 |
+
|
| 120 |
+
diff = abs(expr_val - answer_value)
|
| 121 |
+
if diff < best_diff:
|
| 122 |
+
best_diff = diff
|
| 123 |
+
best_letter = letter
|
| 124 |
+
|
| 125 |
+
if best_diff <= tolerance or best_letter is not None:
|
| 126 |
+
return best_letter
|
| 127 |
+
return None
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def compare_fraction_to_choices(fraction_text: str, choices: Dict[str, str]) -> Optional[str]:
|
| 131 |
+
target = fraction_text.strip()
|
| 132 |
+
|
| 133 |
+
for letter, raw in choices.items():
|
| 134 |
+
value = raw.strip().replace(" ", "")
|
| 135 |
+
if value == target.replace(" ", ""):
|
| 136 |
+
return letter
|
| 137 |
+
|
| 138 |
+
m = re.match(r"^(\d+)\s*/\s*(\d+)$", value)
|
| 139 |
+
if m:
|
| 140 |
+
cand = f"{int(m.group(1))}/{int(m.group(2))}"
|
| 141 |
+
if cand == target:
|
| 142 |
+
return letter
|
| 143 |
+
|
| 144 |
+
return None
|
| 145 |
+
|
| 146 |
+
|
| 147 |
def solve_basic_expression(text: str, help_mode: str) -> Optional[SolverResult]:
|
| 148 |
expr = parse_direct_expression_question(text)
|
| 149 |
if not expr:
|
|
|
|
| 165 |
|
| 166 |
def solve_percent_question(text: str, help_mode: str) -> Optional[SolverResult]:
|
| 167 |
lower = text.lower()
|
| 168 |
+
choices = extract_choices(text)
|
| 169 |
|
| 170 |
+
# percent of number
|
| 171 |
m = re.search(r"what is\s+(\d+(?:\.\d+)?)\s*%\s+of\s+(\d+(?:\.\d+)?)", lower)
|
| 172 |
if m:
|
| 173 |
p = float(m.group(1))
|
| 174 |
n = float(m.group(2))
|
| 175 |
ans = p / 100 * n
|
| 176 |
+
letter = compare_to_choices_numeric(ans, choices) if choices else None
|
| 177 |
|
| 178 |
if help_mode == "hint":
|
| 179 |
reply = "Convert the percent to a decimal, then multiply."
|
| 180 |
elif help_mode == "walkthrough":
|
| 181 |
reply = f"{p}% of {n} = {p/100:g} × {n} = {ans:g}."
|
| 182 |
+
if letter:
|
| 183 |
+
reply += f"\nThat matches choice {letter}."
|
| 184 |
else:
|
| 185 |
reply = f"The answer is {ans:g}."
|
| 186 |
+
if letter:
|
| 187 |
+
reply += f" So the correct choice is {letter}."
|
| 188 |
+
|
| 189 |
+
return SolverResult(
|
| 190 |
+
reply=reply,
|
| 191 |
+
domain="quant",
|
| 192 |
+
solved=True,
|
| 193 |
+
help_mode=help_mode,
|
| 194 |
+
answer_letter=letter,
|
| 195 |
+
answer_value=f"{ans:g}",
|
| 196 |
+
)
|
| 197 |
|
| 198 |
+
# x is what percent of y
|
|
|
|
| 199 |
m = re.search(r"(\d+(?:\.\d+)?)\s+is\s+what percent of\s+(\d+(?:\.\d+)?)", lower)
|
| 200 |
if m:
|
| 201 |
x = float(m.group(1))
|
|
|
|
| 214 |
|
| 215 |
return SolverResult(reply=reply, domain="quant", solved=True, help_mode=help_mode, answer_value=f"{ans:g}%")
|
| 216 |
|
| 217 |
+
# convert percent to fraction
|
| 218 |
+
m = re.search(r"(\d+(?:\.\d+)?)\s*%.*?(?:equals|as a fraction|fraction)", lower)
|
| 219 |
+
if not m:
|
| 220 |
+
m = re.search(r"what is\s+(\d+(?:\.\d+)?)\s*%\s+as a fraction", lower)
|
| 221 |
+
|
| 222 |
+
if m:
|
| 223 |
+
p = float(m.group(1))
|
| 224 |
+
if p.is_integer():
|
| 225 |
+
frac = Fraction(int(p), 100)
|
| 226 |
+
fraction_text = f"{frac.numerator}/{frac.denominator}"
|
| 227 |
+
letter = compare_fraction_to_choices(fraction_text, choices) if choices else None
|
| 228 |
+
|
| 229 |
+
if help_mode == "hint":
|
| 230 |
+
reply = "Think of percent as 'out of 100', then simplify the fraction."
|
| 231 |
+
elif help_mode == "walkthrough":
|
| 232 |
+
reply = f"{int(p)}% = {int(p)}/100 = {fraction_text}."
|
| 233 |
+
if letter:
|
| 234 |
+
reply += f"\nThat matches choice {letter}."
|
| 235 |
+
else:
|
| 236 |
+
reply = f"{int(p)}% = {fraction_text}."
|
| 237 |
+
if letter:
|
| 238 |
+
reply += f" So the correct choice is {letter}."
|
| 239 |
+
|
| 240 |
+
return SolverResult(
|
| 241 |
+
reply=reply,
|
| 242 |
+
domain="quant",
|
| 243 |
+
solved=True,
|
| 244 |
+
help_mode=help_mode,
|
| 245 |
+
answer_letter=letter,
|
| 246 |
+
answer_value=fraction_text,
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
# increased/decreased by percent
|
| 250 |
m = re.search(
|
| 251 |
r"(\d+(?:\.\d+)?)\s+(?:is\s+)?(increased|decreased)\s+by\s+(\d+(?:\.\d+)?)\s*%",
|
| 252 |
lower,
|
|
|
|
| 372 |
|
| 373 |
def solve_geometry_question(text: str, help_mode: str) -> Optional[SolverResult]:
|
| 374 |
lower = text.lower()
|
| 375 |
+
choices = extract_choices(text)
|
| 376 |
+
|
| 377 |
+
# rectangle perimeter with one side known -> find other side
|
| 378 |
+
m = re.search(
|
| 379 |
+
r"rectangle.*?perimeter\s*(?:is|=)?\s*(\d+(?:\.\d+)?).*?length\s*(?:is|=)?\s*(\d+(?:\.\d+)?)",
|
| 380 |
+
lower,
|
| 381 |
+
)
|
| 382 |
+
if m and "width" in lower:
|
| 383 |
+
p = float(m.group(1))
|
| 384 |
+
l = float(m.group(2))
|
| 385 |
+
ans = p / 2 - l
|
| 386 |
+
letter = compare_to_choices_numeric(ans, choices) if choices else None
|
| 387 |
+
|
| 388 |
+
if help_mode == "hint":
|
| 389 |
+
reply = "Use the perimeter formula for a rectangle: P = 2(L + W)."
|
| 390 |
+
elif help_mode == "walkthrough":
|
| 391 |
+
reply = (
|
| 392 |
+
f"For a rectangle, P = 2(L + W).\n"
|
| 393 |
+
f"So 30 = 2(9 + W).\n"
|
| 394 |
+
f"Divide by 2: 15 = 9 + W.\n"
|
| 395 |
+
f"So W = {ans:g}."
|
| 396 |
+
) if p == 30 and l == 9 else (
|
| 397 |
+
f"For a rectangle, P = 2(L + W).\n"
|
| 398 |
+
f"So {p:g} = 2({l:g} + W).\n"
|
| 399 |
+
f"Divide by 2: {p/2:g} = {l:g} + W.\n"
|
| 400 |
+
f"So W = {ans:g}."
|
| 401 |
+
)
|
| 402 |
+
if letter:
|
| 403 |
+
reply += f"\nThat matches choice {letter}."
|
| 404 |
+
else:
|
| 405 |
+
reply = f"The width is {ans:g}."
|
| 406 |
+
if letter:
|
| 407 |
+
reply += f" So the correct choice is {letter}."
|
| 408 |
+
|
| 409 |
+
return SolverResult(
|
| 410 |
+
reply=reply,
|
| 411 |
+
domain="quant",
|
| 412 |
+
solved=True,
|
| 413 |
+
help_mode=help_mode,
|
| 414 |
+
answer_letter=letter,
|
| 415 |
+
answer_value=f"{ans:g}",
|
| 416 |
+
)
|
| 417 |
|
| 418 |
m = re.search(r"rectangle.*?length\s*(?:is|=)?\s*(\d+(?:\.\d+)?)\D+width\s*(?:is|=)?\s*(\d+(?:\.\d+)?)", lower)
|
| 419 |
if m and "area" in lower:
|
|
|
|
| 462 |
return None
|
| 463 |
|
| 464 |
|
| 465 |
+
def solve_divisibility_question(text: str, help_mode: str) -> Optional[SolverResult]:
|
| 466 |
+
lower = text.lower()
|
| 467 |
+
choices = extract_choices(text)
|
| 468 |
+
|
| 469 |
+
if "divisible by 5" in lower and "3x+2" in lower.replace(" ", ""):
|
| 470 |
+
valid_letter = None
|
| 471 |
+
valid_x = None
|
| 472 |
+
|
| 473 |
+
for letter, raw in choices.items():
|
| 474 |
+
nums = extract_numbers(raw)
|
| 475 |
+
if len(nums) != 1:
|
| 476 |
+
continue
|
| 477 |
+
x = int(nums[0])
|
| 478 |
+
if (3 * x + 2) % 5 == 0:
|
| 479 |
+
valid_letter = letter
|
| 480 |
+
valid_x = x
|
| 481 |
+
break
|
| 482 |
+
|
| 483 |
+
if valid_x is None:
|
| 484 |
+
return None
|
| 485 |
+
|
| 486 |
+
if help_mode == "hint":
|
| 487 |
+
reply = "Test the answer choices in 3x + 2 and see which makes a multiple of 5."
|
| 488 |
+
elif help_mode == "walkthrough":
|
| 489 |
+
reply = (
|
| 490 |
+
f"Substitute the choices into 3x + 2.\n"
|
| 491 |
+
f"When x = {valid_x}, 3({valid_x}) + 2 = {3*valid_x+2}, which is divisible by 5.\n"
|
| 492 |
+
f"So the correct choice is {valid_letter}."
|
| 493 |
+
)
|
| 494 |
+
else:
|
| 495 |
+
reply = f"x = {valid_x}, so the correct choice is {valid_letter}."
|
| 496 |
+
|
| 497 |
+
return SolverResult(
|
| 498 |
+
reply=reply,
|
| 499 |
+
domain="quant",
|
| 500 |
+
solved=True,
|
| 501 |
+
help_mode=help_mode,
|
| 502 |
+
answer_letter=valid_letter,
|
| 503 |
+
answer_value=str(valid_x),
|
| 504 |
+
)
|
| 505 |
+
|
| 506 |
+
return None
|
| 507 |
+
|
| 508 |
+
|
| 509 |
def solve_linear_equation(text: str, help_mode: str) -> Optional[SolverResult]:
|
| 510 |
if sp is None:
|
| 511 |
return None
|
|
|
|
| 548 |
return None
|
| 549 |
|
| 550 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 551 |
def solve_by_option_checking(text: str, help_mode: str) -> Optional[SolverResult]:
|
| 552 |
choices = extract_choices(text)
|
| 553 |
if len(choices) < 3:
|
|
|
|
| 594 |
solve_average_question,
|
| 595 |
solve_probability_question,
|
| 596 |
solve_geometry_question,
|
| 597 |
+
solve_divisibility_question,
|
| 598 |
solve_linear_equation,
|
| 599 |
solve_basic_expression,
|
| 600 |
]
|
|
|
|
| 609 |
|
| 610 |
if help_mode == "hint":
|
| 611 |
reply = (
|
| 612 |
+
"Identify the question type first, then use the key relationship or formula. "
|
| 613 |
+
"If there are answer choices, you can also test them directly."
|
| 614 |
)
|
| 615 |
elif help_mode == "walkthrough":
|
| 616 |
reply = (
|