Skip to content

Commit fea06f5

Browse files
authored
Fixed E being parsed as Eulers in preview (#235)
* Fixed E being parsed as Eulers in preview * Removed E from the list of potentially placeholders * Added note to dev.md for `E` parsing and added more tests for Euler preview * Added link to PR
1 parent d53f444 commit fea06f5

File tree

4 files changed

+104
-12
lines changed

4 files changed

+104
-12
lines changed

app/docs/dev.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,5 +209,7 @@ When the evaluation function preview is called the code in `preview.py` will be
209209

210210
**Remark**: Since it is likely that there will be significant overlap between the response preview and the response evaluation (e.g. code for parsing and interpreting the response), it is good practice if they can share as much code as possible to ensure consistency. For this reason it might be better to move the preview functions fully inside the context (either by making a `preview` subfolder in the `context` folder, or by moving the implementation of the preview function inside the context files themselves). In this case the `preview.py` and `evaluation.py` could also share the same code for determining the right context to use.
211211

212+
`E` is treated as `E` in when `is_latex` is enabled. As latex2sympy2 does not parse E correctly. More details can be found ont the PR: https://github.com/lambda-feedback/compareExpressions/pull/235
213+
212214
# Tests
213215
There are two main groups of tests, evaluation tests and preview tests. The evaluation test can be run by calling `evaluation_tests.py`

app/evaluation_tests.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import pytest
22
import os
33

4+
from app.preview import preview_function
5+
from app.utility.preview_utilities import Params
46
from .evaluation import evaluation_function
57

68

@@ -92,5 +94,29 @@ def test_physical_qualities_no_tolerance(self):
9294
result = evaluation_function(response, answer, params)
9395
assert result["is_correct"] is False
9496

97+
def test_euler_preview_evaluate(self):
98+
response = "ER_2"
99+
params = Params(is_latex=True, elementary_functions=False, strict_syntax=False)
100+
result = preview_function(response, params)
101+
assert "preview" in result.keys()
102+
103+
preview = result["preview"]
104+
assert preview["latex"] == "ER_2"
105+
assert preview["sympy"] == "E*R_2"
106+
107+
params = {
108+
"atol": 0.0,
109+
"rtol": 0.0,
110+
"strict_syntax": False,
111+
"physical_quantity": False,
112+
"elementary_functions": False,
113+
}
114+
115+
response = preview["sympy"]
116+
answer = "E*R_2"
117+
result = evaluation_function(response, answer, params)
118+
assert result["is_correct"] is True
119+
120+
95121
if __name__ == "__main__":
96122
pytest.main(['-xk not slow', '--tb=short', '--durations=10', os.path.abspath(__file__)])

app/preview_tests.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,39 @@ def test_natural_logarithm_notation(self):
7777
assert preview["latex"] == r"\ln{\left(x \right)}"
7878

7979
@pytest.mark.parametrize(
80-
"response, response_latex, response_sympy", [
81-
("e", "e", "E",),
82-
("oo", "\\infty", "oo"),
83-
("ex", "e \\cdot x", "Ex"),
84-
("e * x", "e \\cdot x", "E * x")
80+
"response, is_latex, elementary_functions, response_latex, response_sympy", [
81+
("e", False, True, "e", "E",),
82+
("oo", False, True, "\\infty", "oo"),
83+
("ex", False, True, "e \\cdot x", "Ex"),
84+
("e * x", False, True, "e \\cdot x", "E * x"),
85+
("E", False, True, "e", "E", ),
86+
("e", False, False, "e", "e",),
87+
("ex", False, False, "e \\cdot x", "ex"),
88+
("e * x", False, False, "e \\cdot x", "e * x"),
89+
("E", False, False, "E", "E",),
90+
("E", True, True, "E", "E",),
91+
("e", True, False, "e", "E",),
92+
("ex", True, False, "ex", "E*x"),
93+
("e * x", True, False, "e * x", "E*x"),
94+
("E", True, False, "E", "E",),
95+
("ER_2", True, False, "ER_2", "E*R_2",),
96+
# TODO: add exp (0), (1), (2) and (x)
97+
("exp(1)", False, True, "e^{1}", "exp(1)"),
98+
("e**1", False, True, "e^{1}", "E**1"),
99+
("e^{1}", True, True, "e^{1}", "E"),
100+
("exp(0)", False, True, "e^{0}", "exp(0)"),
101+
("e**0", False, True, "e^{0}", "E**0"),
102+
("e^{0}", True, True, "e^{0}", "1"),
103+
("exp(2)", False, True, "e^{2}", "exp(2)"),
104+
("e**2", False, True, "e^{2}", "E**2"),
105+
("e^{2}", True, True, "e^{2}", "exp(2)"),
106+
("exp(x)", False, True, "e^{x}", "exp(x)"),
107+
("e**x", False, True, "e^{x}", "E**x"),
108+
("e^{x}", True, True, "e^{x}", "exp(x)")
85109
]
86110
)
87-
def test_eulers_number_notation(self, response, response_latex, response_sympy):
88-
params = Params(is_latex=False, elementary_functions=True, strict_syntax=False)
111+
def test_eulers_number_notation(self, response, is_latex, elementary_functions, response_latex, response_sympy):
112+
params = Params(is_latex=is_latex, elementary_functions=elementary_functions, strict_syntax=False)
89113
result = preview_function(response, params)
90114
assert "preview" in result.keys()
91115

app/utility/preview_utilities.py

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
from typing import TypedDict
23
from typing_extensions import NotRequired
34

@@ -31,6 +32,40 @@ class Result(TypedDict):
3132
preview: Preview
3233

3334

35+
def find_placeholder(exp):
36+
for char in 'abcdfghjkoqrtvwxyzABCDFGHIJKLMNOPQRSTUVWXYZ':
37+
if char not in exp:
38+
return char
39+
40+
def preprocess_E(latex_str: str, placeholder: str) -> str:
41+
"""
42+
Replace all symbols starting with 'E' (including plain 'E') with a
43+
placeholder, so latex2sympy does not interpret 'E' as Euler's number.
44+
"""
45+
# Replace E, E_x, ER_2, Efield, etc.
46+
def repl(match):
47+
token = match.group(0)
48+
return placeholder + token[1:]
49+
50+
# Match E followed by optional subscript or alphanumeric/underscore
51+
pattern = re.compile(r'(?<![\\a-zA-Z])E([A-Za-z0-9_]*(?:_\{[^}]*\})?)')
52+
return pattern.sub(repl, latex_str)
53+
54+
55+
def postprocess_E(expr, placeholder):
56+
"""
57+
Replace all placeholder symbols back to symbols starting with E.
58+
"""
59+
subs = {}
60+
for s in expr.free_symbols:
61+
name = str(s)
62+
if name.startswith(placeholder):
63+
new_name = "E" + name[len(placeholder):]
64+
subs[s] = Symbol(new_name)
65+
return expr.xreplace(subs)
66+
67+
68+
3469
def parse_latex(response: str, symbols: SymbolDict, simplify: bool, parameters=None) -> str:
3570
"""Parse a LaTeX string to a sympy string while preserving custom symbols.
3671
@@ -97,15 +132,20 @@ def parse_latex(response: str, symbols: SymbolDict, simplify: bool, parameters=N
97132
parsed_responses = set()
98133
for expression in response_set:
99134
try:
100-
expression = latex2sympy(expression, substitutions)
101-
if isinstance(expression, list):
102-
expression = expression.pop()
135+
e_placeholder = find_placeholder(expression)
136+
137+
expression_preprocessed = preprocess_E(expression, e_placeholder)
138+
expression_parsed = latex2sympy(expression_preprocessed, substitutions)
139+
if isinstance(expression_parsed, list):
140+
expression_parsed = expression_parsed.pop()
141+
142+
expression_postprocess = postprocess_E(expression_parsed, e_placeholder)
103143
if simplify is True:
104-
expression = expression.simplify()
144+
expression_postprocess = expression_postprocess.simplify()
105145
except Exception as e:
106146
raise ValueError(str(e))
107147

108-
parsed_responses.add(str(expression.xreplace(substitutions)))
148+
parsed_responses.add(str(expression_postprocess.xreplace(substitutions)))
109149

110150
if len(parsed_responses) < 2:
111151
return parsed_responses.pop()

0 commit comments

Comments
 (0)