Skip to content

Commit c64fb91

Browse files
authored
Fixed issue with E not parsing in preview, when using LaTeX (#241)
* Fixed issue with E not parsing in preview, when using LaTeX * Added that the case of `e` gets retained on replacement * Added tests for multicharacter symbols not starting with E
1 parent 73dd982 commit c64fb91

File tree

3 files changed

+116
-20
lines changed

3 files changed

+116
-20
lines changed

app/evaluation_tests.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,29 @@ def test_multi_character_implicit_multi_variable(self):
168168
result = evaluation_function(response_implicit_no_bracket, answer, params)
169169
assert result["is_correct"] is True, "Response: a/bcd"
170170

171+
@pytest.mark.parametrize(
172+
"response, is_latex, is_correct", [
173+
("e**ea", False, True),
174+
("e**Ea", False, True),
175+
("e^{ea}", True, True),
176+
("e^{Ea}", True, True),
177+
]
178+
)
179+
def test_e_latex(self, response, is_latex, is_correct):
180+
params = {
181+
"is_latex": is_latex,
182+
"strict_syntax": False,
183+
"elementary_functions": True,
184+
"symbols": {
185+
"ea": {"aliases": ["ea", "Ea"], "latex": "ea"},
186+
},
187+
}
188+
answer = "e**ea"
189+
190+
result = evaluation_function(response, answer, params)
191+
assert result["is_correct"] == is_correct
192+
193+
171194

172195
def test_mu_preview_evaluate(self):
173196
response = "10 μA"

app/preview_tests.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,10 @@ def test_natural_logarithm_notation(self):
101101
("e^{0}", True, True, "e^{0}", "1"),
102102
("exp(2)", False, True, "e^{2}", "exp(2)"),
103103
("e**2", False, True, "e^{2}", "E**2"),
104-
("e^{2}", True, True, "e^{2}", "exp(2)"),
104+
("e^{2}", True, True, "e^{2}", "E**2"),
105105
("exp(x)", False, True, "e^{x}", "exp(x)"),
106106
("e**x", False, True, "e^{x}", "E**x"),
107-
("e^{x}", True, True, "e^{x}", "exp(x)")
107+
("e^{x}", True, True, "e^{x}", "E**x")
108108
]
109109
)
110110
def test_eulers_number_notation(self, response, is_latex, elementary_functions, response_latex, response_sympy):
@@ -116,6 +116,34 @@ def test_eulers_number_notation(self, response, is_latex, elementary_functions,
116116
assert preview["latex"] == response_latex
117117
assert preview["sympy"] == response_sympy
118118

119+
@pytest.mark.parametrize(
120+
"response, is_latex, response_latex, response_sympy, symbols", [
121+
("e**ea", False, "e^{ea}", "E**ea", {"ea": {"aliases": ["ea", "Ea"], "latex": "ea"}}),
122+
("e**Ea", False, "e^{ea}", "E**ea", {"ea": {"aliases": ["ea", "Ea"], "latex": "ea"}}),
123+
("e^{ea}", True, "e^{ea}", "e**ea", {"ea": {"aliases": ["ea", "Ea"], "latex": "ea"}}),
124+
# ("e^{Ea}", True, "e^{Ea}", "e**ea", {"ea": {"aliases": ["ea", "Ea"], "latex": "ea"}}), # TODO: Clarify if we want to be able to use aliases for LaTeX?
125+
("e**aea", False, "e^{aea}", "E**aea", {"aea": {"aliases": ["aea", "aEa"], "latex": "aea"}}),
126+
("e**aEa", False, "e^{aea}", "E**aea", {"aea": {"aliases": ["aea", "aEa"], "latex": "aea"}}),
127+
("e^{aea}", True, "e^{aea}", "e**aea", {"aea": {"aliases": ["aea", "aEa"], "latex": "aea"}}),
128+
# ("e^{aEa}", True, "e^{aEa}", "e**aea", {"aea": {"aliases": ["aea", "aEa"], "latex": "aea"}}), # TODO: Clarify if we want to be able to use aliases for LaTeX?
129+
]
130+
)
131+
def test_e_latex(self, response, is_latex, response_latex, response_sympy, symbols):
132+
params = {
133+
"is_latex": is_latex,
134+
"strict_syntax": False,
135+
"elementary_functions": True,
136+
"symbols": symbols,
137+
}
138+
139+
result = preview_function(response, params)
140+
assert "preview" in result.keys()
141+
preview = result["preview"]
142+
143+
assert preview["latex"] == response_latex
144+
assert preview["sympy"] == response_sympy
145+
146+
119147
@pytest.mark.parametrize(
120148
"response, is_latex, response_latex, response_sympy",
121149
[
@@ -199,5 +227,6 @@ def test_multi_character_implicit_multi_variable(self):
199227
assert result["preview"]["latex"] =='\\frac{a}{bc \\cdot d}'
200228
assert result["preview"]["sympy"] == "a/bcd"
201229

230+
202231
if __name__ == "__main__":
203232
pytest.main(['-xk not slow', "--tb=line", os.path.abspath(__file__)])

app/utility/preview_utilities.py

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,31 +37,72 @@ def find_placeholder(exp):
3737
if char not in exp:
3838
return char
3939

40-
def preprocess_E(latex_str: str, placeholder: str) -> str:
40+
41+
def preprocess_E(latex_str: str) -> tuple[str, dict[str, str]]:
4142
"""
42-
Replace all symbols starting with 'E' (including plain 'E') with a
43-
placeholder, so latex2sympy does not interpret 'E' as Euler's number.
43+
Replace all symbols starting with 'E' or 'e' with placeholders,
44+
so latex2sympy does not interpret 'E' as Euler's number.
45+
Returns the modified string and a dict mapping replaced chars to their placeholders.
4446
"""
45-
# Replace E, E_x, ER_2, Efield, etc.
47+
replacements = {}
48+
49+
# Find placeholder for uppercase E if needed
50+
if re.search(r'(?<!\\)E(?:[a-zA-Z_][a-zA-Z0-9_]*|_\{[^}]*\})?', latex_str):
51+
placeholder_E = find_placeholder(latex_str)
52+
if placeholder_E:
53+
replacements['E'] = placeholder_E
54+
55+
# Find placeholder for lowercase e if needed (exclude already used placeholder)
56+
if re.search(r'(?<!\\)e(?:[a-zA-Z_][a-zA-Z0-9_]*|_\{[^}]*\})?', latex_str):
57+
used_chars = latex_str + ''.join(replacements.values())
58+
placeholder_e = find_placeholder(used_chars)
59+
if placeholder_e:
60+
replacements['e'] = placeholder_e
61+
62+
# If no replacements needed, return original string
63+
if not replacements:
64+
return latex_str, {}
65+
66+
# Replace E and e with their respective placeholders
4667
def repl(match):
4768
token = match.group(0)
48-
return placeholder + token[1:]
69+
first_char = token[0]
70+
if first_char in replacements:
71+
return replacements[first_char] + token[1:]
72+
return token
73+
74+
# Match E or e followed by optional alphanumeric/underscore (including within braces)
75+
pattern = re.compile(r'(?<!\\)[Ee](?:[a-zA-Z_][a-zA-Z0-9_]*|_\{[^}]*\})?')
76+
modified_str = pattern.sub(repl, latex_str)
4977

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)
78+
# Return the modified string and the replacements dict
79+
return modified_str, replacements
5380

5481

55-
def postprocess_E(expr, placeholder):
82+
def postprocess_E(expr, replacements):
5683
"""
57-
Replace all placeholder symbols back to symbols starting with E.
84+
Replace all placeholder symbols back to symbols starting with E or e.
85+
86+
Args:
87+
expr: The sympy expression
88+
replacements: Dict mapping original chars ('E', 'e') to their placeholders
5889
"""
90+
if not replacements:
91+
return expr
92+
93+
# Create reverse mapping: placeholder -> original char
94+
placeholder_to_char = {v: k for k, v in replacements.items()}
95+
5996
subs = {}
6097
for s in expr.free_symbols:
6198
name = str(s)
62-
if name.startswith(placeholder):
63-
new_name = "E" + name[len(placeholder):]
64-
subs[s] = Symbol(new_name)
99+
# Check if this symbol starts with any of our placeholders
100+
for placeholder, original_char in placeholder_to_char.items():
101+
if name.startswith(placeholder):
102+
new_name = original_char + name[len(placeholder):]
103+
subs[s] = Symbol(new_name)
104+
break # Only replace once per symbol
105+
65106
return expr.xreplace(subs)
66107

67108

@@ -121,25 +162,28 @@ def parse_latex(response: str, symbols: SymbolDict, simplify: bool, parameters=N
121162

122163
if "\pm" not in symbol_str and "\mp" not in symbol_str:
123164
try:
124-
latex_symbol = latex2sympy(latex_symbol_str)
165+
latex_symbol_str_preprocessed, replacements = preprocess_E(latex_symbol_str)
166+
latex_symbol_parsed = latex2sympy(latex_symbol_str_preprocessed)
167+
latex_symbol_str_postprocess = postprocess_E(latex_symbol_parsed, replacements)
168+
125169
except Exception:
126170
raise ValueError(
127171
f"Couldn't parse latex symbol {latex_symbol_str} "
128172
f"to sympy symbol."
129173
)
130-
substitutions[latex_symbol] = Symbol(sympy_symbol_str)
174+
substitutions[latex_symbol_str_postprocess] = Symbol(sympy_symbol_str)
175+
131176

132177
parsed_responses = set()
133178
for expression in response_set:
134179
try:
135-
e_placeholder = find_placeholder(expression)
136180

137-
expression_preprocessed = preprocess_E(expression, e_placeholder)
181+
expression_preprocessed, replacements = preprocess_E(expression)
138182
expression_parsed = latex2sympy(expression_preprocessed, substitutions)
139183
if isinstance(expression_parsed, list):
140184
expression_parsed = expression_parsed.pop()
141185

142-
expression_postprocess = postprocess_E(expression_parsed, e_placeholder)
186+
expression_postprocess = postprocess_E(expression_parsed, replacements)
143187
if simplify is True:
144188
expression_postprocess = expression_postprocess.simplify()
145189
except Exception as e:

0 commit comments

Comments
 (0)