j-js commited on
Commit
14a41e4
·
verified ·
1 Parent(s): a08b987

Update quant_solver.py

Browse files
Files changed (1) hide show
  1. quant_solver.py +118 -157
quant_solver.py CHANGED
@@ -25,200 +25,161 @@ def extract_choices(text: str) -> Dict[str, str]:
25
  return {m.group(1).upper(): normalize_spaces(m.group(2)) for m in matches}
26
 
27
 
28
- def has_answer_choices(text: str) -> bool:
29
- return len(extract_choices(text)) >= 3
30
 
 
31
 
32
- def is_quant_question(text: str) -> bool:
33
- lower = clean_math_text(text).lower()
34
- keywords = [
35
- "solve", "equation", "percent", "ratio", "probability", "mean", "median",
36
- "average", "sum", "difference", "product", "quotient", "triangle", "circle",
37
- "rectangle", "area", "perimeter", "volume", "algebra", "integer", "divisible",
38
- "number", "fraction", "decimal", "geometry", "distance", "speed", "work",
39
- ]
40
- if any(k in lower for k in keywords):
41
- return True
42
- if "=" in lower and re.search(r"[a-z]", lower):
43
- return True
44
- if re.search(r"\d", lower) and ("?" in lower or has_answer_choices(lower)):
45
- return True
46
- return False
47
-
48
-
49
- def _prepare_expression(expr: str) -> str:
50
- expr = clean_math_text(expr).strip()
51
- expr = expr.replace("^", "**")
52
- expr = re.sub(r"(\d)\s*\(", r"\1*(", expr)
53
- expr = re.sub(r"\)\s*(\d)", r")*\1", expr)
54
- expr = re.sub(r"(\d)([a-zA-Z])", r"\1*\2", expr)
55
- return expr
56
-
57
-
58
- def _extract_equation(text: str) -> Optional[str]:
59
- cleaned = clean_math_text(text)
60
- if "=" not in cleaned:
61
  return None
62
- patterns = [
63
- r"([A-Za-z0-9\.\+\-\*/\^\(\)\s]*[a-zA-Z][A-Za-z0-9\.\+\-\*/\^\(\)\s]*=[A-Za-z0-9\.\+\-\*/\^\(\)\s]+)",
64
- r"([0-9A-Za-z\.\+\-\*/\^\(\)\s]+=[0-9A-Za-z\.\+\-\*/\^\(\)\s]+)",
65
- ]
66
- for pattern in patterns:
67
- for m in re.finditer(pattern, cleaned):
68
- candidate = m.group(1).strip()
69
- tokens = re.findall(r"[a-z]", candidate.lower())
70
- if tokens and not candidate.lower().startswith(("how do", "can you", "please", "what is", "solve ")):
71
- return candidate
72
- eq_index = cleaned.find("=")
73
- left = re.findall(r"[A-Za-z0-9\.\+\-\*/\^\(\)\s]+$", cleaned[:eq_index])
74
- right = re.findall(r"^[A-Za-z0-9\.\+\-\*/\^\(\)\s]+", cleaned[eq_index + 1:])
75
- if left and right:
76
- candidate = left[0].strip().split()[-1] + " = " + right[0].strip().split()[0]
77
- if re.search(r"[a-z]", candidate.lower()):
78
- return candidate
79
- return None
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
- def _parse_number(text: str) -> Optional[float]:
83
- raw = clean_math_text(text).strip().lower()
84
- pct = re.fullmatch(r"(-?\d+(?:\.\d+)?)%", raw.replace(" ", ""))
85
- if pct:
86
- return float(pct.group(1)) / 100.0
87
- frac = re.fullmatch(r"(-?\d+)\s*/\s*(-?\d+)", raw)
88
- if frac:
89
- den = float(frac.group(2))
90
- if den == 0:
91
- return None
92
- return float(frac.group(1)) / den
93
- try:
94
- return float(eval(_prepare_expression(raw), {"__builtins__": {}}, {"sqrt": math.sqrt, "pi": math.pi}))
95
- except Exception:
96
  return None
97
 
 
 
 
98
 
99
- def _best_choice(answer_value: float, choices: Dict[str, str]) -> Optional[str]:
100
- best_letter = None
101
- best_diff = float("inf")
102
- for letter, raw in choices.items():
103
- parsed = _parse_number(raw)
104
- if parsed is None:
105
- continue
106
- diff = abs(parsed - answer_value)
107
- if diff < best_diff:
108
- best_diff = diff
109
- best_letter = letter
110
- if best_letter is not None and best_diff <= 1e-6:
111
- return best_letter
112
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
 
115
  def _solve_percent(text: str) -> Optional[SolverResult]:
 
116
  lower = clean_math_text(text).lower()
117
- choices = extract_choices(text)
118
 
119
- m = re.search(r"(\d+(?:\.\d+)?)\s*(?:%|percent)\s+of\s+(?:a\s+)?number\s+is\s+(\d+(?:\.\d+)?)", lower)
120
- if m:
121
- p = float(m.group(1))
122
- value = float(m.group(2))
123
- ans = value / (p / 100.0)
124
- return SolverResult(
125
- domain="quant",
126
- solved=True,
127
- topic="percent",
128
- answer_value=f"{ans:g}",
129
- answer_letter=_best_choice(ans, choices) if choices else None,
130
- internal_answer=f"{ans:g}",
131
- steps=[
132
- f"Let the number be n.",
133
- f"Write {p}% of n as {p/100:g}n.",
134
- f"Set {p/100:g}n = {value} and solve for n.",
135
- ],
136
- )
137
 
138
- m = re.search(r"what is\s+(\d+(?:\.\d+)?)\s*(?:%|percent)\s+of\s+(\d+(?:\.\d+)?)", lower)
139
  if m:
140
  p = float(m.group(1))
141
- n = float(m.group(2))
142
- ans = p / 100.0 * n
 
 
143
  return SolverResult(
144
  domain="quant",
145
  solved=True,
146
  topic="percent",
147
  answer_value=f"{ans:g}",
148
- answer_letter=_best_choice(ans, choices) if choices else None,
149
  internal_answer=f"{ans:g}",
150
- steps=[f"Convert {p}% to {p/100:g}.", f"Multiply by {n}."]
151
  )
152
- return None
153
 
154
-
155
- def _solve_mean_median(text: str) -> Optional[SolverResult]:
156
- lower = clean_math_text(text).lower()
157
- nums = [float(n) for n in re.findall(r"-?\d+(?:\.\d+)?", lower)]
158
- if not nums:
159
- return None
160
- if "mean" in lower or "average" in lower:
161
- ans = mean(nums)
162
- return SolverResult(domain="quant", solved=True, topic="statistics", answer_value=f"{ans:g}", internal_answer=f"{ans:g}", steps=["Add the values.", f"Divide by {len(nums)}."])
163
- if "median" in lower:
164
- ans = median(nums)
165
- return SolverResult(domain="quant", solved=True, topic="statistics", answer_value=f"{ans:g}", internal_answer=f"{ans:g}", steps=["Order the values.", "Take the middle value."])
166
  return None
167
 
168
 
169
  def _solve_linear_equation(text: str) -> Optional[SolverResult]:
 
170
  if sp is None:
171
  return None
172
- expr = _extract_equation(text)
173
- if not expr:
174
- return None
175
- try:
176
- lhs, rhs = expr.split("=", 1)
177
- symbols = sorted(set(re.findall(r"\b[a-z]\b", expr)))
178
- if not symbols:
179
- return None
180
- var_name = symbols[0]
181
- var = sp.symbols(var_name)
182
- sol = sp.solve(sp.Eq(sp.sympify(_prepare_expression(lhs)), sp.sympify(_prepare_expression(rhs))), var)
183
- if not sol:
184
- return None
185
- value = sol[0]
186
- try:
187
- as_float = float(value)
188
- except Exception:
189
- as_float = None
190
- choices = extract_choices(text)
191
- return SolverResult(
192
- domain="quant",
193
- solved=True,
194
- topic="algebra",
195
- answer_value=str(value),
196
- answer_letter=_best_choice(as_float, choices) if (as_float is not None and choices) else None,
197
- internal_answer=f"{var_name} = {value}",
198
- steps=[
199
- "Treat the statement as an equation.",
200
- "Undo operations on both sides to isolate the variable.",
201
- f"That gives {var_name} = {value}.",
202
- ],
203
- )
204
- except Exception:
205
  return None
206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
  def solve_quant(text: str) -> SolverResult:
209
- text = text or ""
210
- for fn in (_solve_percent, _solve_mean_median, _solve_linear_equation):
 
 
 
 
 
 
211
  result = fn(text)
212
- if result is not None:
213
  return result
 
214
  return SolverResult(
215
  domain="quant",
216
  solved=False,
217
  topic="general_quant",
218
- reply="This looks quantitative, but it does not match a strong rule-based pattern yet.",
219
- steps=[
220
- "Identify the quantity the question wants.",
221
- "Translate the wording into an equation, ratio, or diagram.",
222
- "Carry out the calculation carefully.",
223
- ],
224
- )
 
25
  return {m.group(1).upper(): normalize_spaces(m.group(2)) for m in matches}
26
 
27
 
28
+ def _solve_successive_percent(text: str) -> Optional[SolverResult]:
 
29
 
30
+ t = clean_math_text(text).lower()
31
 
32
+ percents = re.findall(r"(\d+(?:\.\d+)?)\s*%", t)
33
+ if len(percents) < 2:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
+ values = [float(p) for p in percents]
37
+
38
+ mult = 1
39
+ for p in values:
40
+ if "decrease" in t or "discount" in t:
41
+ mult *= 1 - p / 100
42
+ else:
43
+ mult *= 1 + p / 100
44
+
45
+ net = (mult - 1) * 100
46
+
47
+ return SolverResult(
48
+ domain="quant",
49
+ solved=True,
50
+ topic="percent",
51
+ answer_value=f"{net:.2f}%",
52
+ internal_answer=f"{net:.2f}%",
53
+ steps=[
54
+ "Convert each percent change to a multiplier.",
55
+ "Multiply the successive multipliers.",
56
+ "Convert the final multiplier back to a percent change.",
57
+ ],
58
+ )
59
+
60
+
61
+ def _solve_ratio_total(text: str) -> Optional[SolverResult]:
62
+
63
+ t = clean_math_text(text)
64
+
65
+ ratio = re.search(r"(\d+)\s*:\s*(\d+)", t)
66
+ total = re.search(r"total\s*(?:is|=)?\s*(\d+)", t.lower())
67
 
68
+ if not ratio or not total:
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  return None
70
 
71
+ a = int(ratio.group(1))
72
+ b = int(ratio.group(2))
73
+ total_val = int(total.group(1))
74
 
75
+ part_sum = a + b
76
+ unit = total_val / part_sum
77
+
78
+ return SolverResult(
79
+ domain="quant",
80
+ solved=True,
81
+ topic="ratio",
82
+ answer_value=f"{a * unit:g}",
83
+ internal_answer=f"{a * unit:g}",
84
+ steps=[
85
+ "Add the ratio parts.",
86
+ "Divide the total by the sum of the ratio.",
87
+ "Multiply by the requested ratio component.",
88
+ ],
89
+ )
90
+
91
+
92
+ def _solve_remainder(text: str) -> Optional[SolverResult]:
93
+
94
+ t = clean_math_text(text).lower()
95
+
96
+ m = re.search(r"remainder.*?(\d+).*?divided by (\d+)", t)
97
+
98
+ if not m:
99
+ return None
100
+
101
+ a = int(m.group(1))
102
+ b = int(m.group(2))
103
+
104
+ r = a % b
105
+
106
+ return SolverResult(
107
+ domain="quant",
108
+ solved=True,
109
+ topic="number_theory",
110
+ answer_value=str(r),
111
+ internal_answer=str(r),
112
+ steps=[
113
+ "Divide the number by the divisor.",
114
+ "The remainder is the leftover after division.",
115
+ ],
116
+ )
117
 
118
 
119
  def _solve_percent(text: str) -> Optional[SolverResult]:
120
+
121
  lower = clean_math_text(text).lower()
 
122
 
123
+ m = re.search(r"(\d+)% of a number is (\d+)", lower)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
 
125
  if m:
126
  p = float(m.group(1))
127
+ val = float(m.group(2))
128
+
129
+ ans = val / (p / 100)
130
+
131
  return SolverResult(
132
  domain="quant",
133
  solved=True,
134
  topic="percent",
135
  answer_value=f"{ans:g}",
 
136
  internal_answer=f"{ans:g}",
 
137
  )
 
138
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  return None
140
 
141
 
142
  def _solve_linear_equation(text: str) -> Optional[SolverResult]:
143
+
144
  if sp is None:
145
  return None
146
+
147
+ m = re.search(r"([a-z])\s*/\s*(\d+)\s*=\s*(\d+)", text)
148
+
149
+ if not m:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  return None
151
 
152
+ var = m.group(1)
153
+ a = float(m.group(2))
154
+ b = float(m.group(3))
155
+
156
+ ans = a * b
157
+
158
+ return SolverResult(
159
+ domain="quant",
160
+ solved=True,
161
+ topic="algebra",
162
+ answer_value=str(ans),
163
+ internal_answer=str(ans),
164
+ )
165
+
166
 
167
  def solve_quant(text: str) -> SolverResult:
168
+
169
+ for fn in (
170
+ _solve_successive_percent,
171
+ _solve_ratio_total,
172
+ _solve_remainder,
173
+ _solve_percent,
174
+ _solve_linear_equation,
175
+ ):
176
  result = fn(text)
177
+ if result:
178
  return result
179
+
180
  return SolverResult(
181
  domain="quant",
182
  solved=False,
183
  topic="general_quant",
184
+ reply="This looks quantitative but does not match a strong rule-based solver yet.",
185
+ )