Spaces:
Sleeping
Sleeping
File size: 5,551 Bytes
fe0c99f 9b70494 fe0c99f de8ccff fe0c99f de8ccff fe0c99f 9b70494 fe0c99f de8ccff fe0c99f 9b70494 fe0c99f 9b70494 fe0c99f 9b70494 fe0c99f de8ccff fe0c99f de8ccff | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | """Robust LaTeX โ SymPy parser with multiple fallback strategies."""
import logging
import re
from sympy import (
Abs,
E,
acos,
asin,
atan,
cos,
cot,
csc,
exp,
ln,
log,
oo,
pi,
sec,
sin,
sqrt,
symbols,
sympify,
tan,
)
from sympy.parsing.latex import parse_latex
logger = logging.getLogger(__name__)
_COMMON = {
r"\sin": "sin", r"\cos": "cos", r"\tan": "tan",
r"\sec": "sec", r"\csc": "csc", r"\cot": "cot",
r"\arcsin": "asin", r"\arccos": "acos", r"\arctan": "atan",
r"\ln": "log", r"\log": "log", r"\exp": "exp",
r"\sqrt": "sqrt", r"\pi": "pi", r"\infty": "oo",
r"\left": "", r"\right": "", r"\,": " ", r"\!": "",
r"\cdot": "*", r"\times": "*",
}
class ExpressionParser:
def __init__(self):
self._x, self._y, self._z, self._t = symbols("x y z t")
self._n, self._k = symbols("n k", integer=True)
def parse(self, latex_str: str) -> dict:
"""Parse a LaTeX math expression into a SymPy object.
Tries three strategies in order: SymPy's ``parse_latex``, a manual
translation pass, and finally a raw ``sympify`` call. The first
strategy that succeeds is returned.
Args:
latex_str: A LaTeX string such as ``r"\\frac{d}{dx} x^2"`` or
``"x^2 + 3x - 1"``.
Returns:
On success: ``{"success": True, "sympy_expr": Expr, "latex": str,
"variables": list[str], "raw": str}``.
On failure: ``{"success": False, "error": str, "latex": str}``.
"""
cleaned = self._preprocess(latex_str)
expr = None
error = None
# Strategy 1: SymPy parse_latex
try:
expr = parse_latex(cleaned)
except Exception as exc: # noqa: BLE001 โ parse_latex wraps many ANTLR / SymPy errors; we fall through to the next strategy.
logger.debug("parse_latex failed for %r: %s", cleaned, exc)
# Strategy 2: manual translation to SymPy string
if expr is None:
try:
py_str = self._latex_to_sympy_str(cleaned)
expr = sympify(py_str, locals={
"x": self._x, "y": self._y, "z": self._z, "t": self._t,
"n": self._n, "k": self._k, "pi": pi, "e": E, "E": E,
"sin": sin, "cos": cos, "tan": tan, "sec": sec,
"csc": csc, "cot": cot, "asin": asin, "acos": acos,
"atan": atan, "log": log, "ln": ln, "exp": exp,
"sqrt": sqrt, "Abs": Abs, "oo": oo,
})
except Exception as exc: # noqa: BLE001 โ sympify exposes many parse-class exceptions; fall through to Strategy 3.
logger.debug("manual sympify failed for %r: %s", cleaned, exc)
# Strategy 3: try raw sympify
if expr is None:
try:
expr = sympify(cleaned.replace("^", "**"))
except Exception as e:
error = str(e)
# ``sympify`` can return non-Basic singletons for inputs like
# ``"..."`` (Python's Ellipsis) which lack ``free_symbols``. Reject
# those alongside None so downstream callers always see a real
# sympy expression on the success branch โ surfaced by the
# hypothesis fuzz suite (``test_parser_never_raises_on_fuzzy_math_like_input``).
if expr is not None and hasattr(expr, "free_symbols"):
return {
"success": True,
"sympy_expr": expr,
"latex": latex_str,
"variables": sorted(str(s) for s in expr.free_symbols),
"raw": str(expr),
}
return {"success": False, "error": error or "Unable to parse expression", "latex": latex_str}
# โโ helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
def _preprocess(self, latex: str) -> str:
s = latex.strip()
s = re.sub(r"\\left|\\right", "", s)
s = s.replace(r"\,", " ").replace(r"\!", "")
s = s.replace("ฯ", "pi").replace("โ", "oo")
s = s.replace("ร", "*").replace("ยท", "*")
s = s.replace("โ", "-")
s = re.sub(r"\\\s+", " ", s)
return re.sub(r"\\operatorname\{(\w+)\}", r"\\\1", s)
def _latex_to_sympy_str(self, latex: str) -> str:
s = latex
# handle \frac{a}{b} โ ((a)/(b))
while r"\frac" in s:
s = re.sub(r"\\frac\{([^{}]*)\}\{([^{}]*)\}", r"((\1)/(\2))", s)
s = re.sub(
r"\\frac\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}",
r"((\1)/(\2))", s,
)
if r"\frac" in s:
break
# handle \sqrt[n]{x} and \sqrt{x}
s = re.sub(r"\\sqrt\[([^\]]+)\]\{([^{}]+)\}", r"((\2)**(1/(\1)))", s)
s = re.sub(r"\\sqrt\{([^{}]+)\}", r"sqrt(\1)", s)
for pat, rep in _COMMON.items():
s = s.replace(pat, rep)
s = s.replace("^", "**").replace("{", "(").replace("}", ")")
# insert multiplication: 2x โ 2*x, )x โ )*x, x( โ x*(
s = re.sub(r"(\d)([a-zA-Z(])", r"\1*\2", s)
s = re.sub(r"\)(\w)", r")*\1", s)
s = re.sub(r"\)\(", r")*(", s)
s = re.sub(r"(?<![a-zA-Z])([a-zA-Z])\(", r"\1*(", s)
s = re.sub(r"([a-zA-Z0-9\)])\s+([a-zA-Z])", r"\1*\2", s)
return re.sub(r"\s+", " ", s).strip()
|