Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions app/evaluation_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,29 @@ def test_multi_character_implicit_multi_variable(self):
result = evaluation_function(response_implicit_no_bracket, answer, params)
assert result["is_correct"] is True, "Response: a/bcd"

@pytest.mark.parametrize(
"response, is_latex, is_correct", [
("e**ea", False, True),
("e**Ea", False, True),
("e^{ea}", True, True),
("e^{Ea}", True, True),
]
)
def test_e_latex(self, response, is_latex, is_correct):
params = {
"is_latex": is_latex,
"strict_syntax": False,
"elementary_functions": True,
"symbols": {
"ea": {"aliases": ["ea", "Ea"], "latex": "ea"},
},
}
answer = "e**ea"

result = evaluation_function(response, answer, params)
assert result["is_correct"] == is_correct



if __name__ == "__main__":
pytest.main(['-xk not slow', '--tb=short', '--durations=10', os.path.abspath(__file__)])
34 changes: 31 additions & 3 deletions app/preview_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ def test_natural_logarithm_notation(self):
("e * x", True, False, "e * x", "E*x"),
("E", True, False, "E", "E",),
("ER_2", True, False, "ER_2", "E*R_2",),
# TODO: add exp (0), (1), (2) and (x)
("exp(1)", False, True, "e^{1}", "exp(1)"),
("e**1", False, True, "e^{1}", "E**1"),
("e^{1}", True, True, "e^{1}", "E"),
Expand All @@ -102,10 +101,10 @@ def test_natural_logarithm_notation(self):
("e^{0}", True, True, "e^{0}", "1"),
("exp(2)", False, True, "e^{2}", "exp(2)"),
("e**2", False, True, "e^{2}", "E**2"),
("e^{2}", True, True, "e^{2}", "exp(2)"),
("e^{2}", True, True, "e^{2}", "E**2"),
("exp(x)", False, True, "e^{x}", "exp(x)"),
("e**x", False, True, "e^{x}", "E**x"),
("e^{x}", True, True, "e^{x}", "exp(x)")
("e^{x}", True, True, "e^{x}", "E**x")
]
)
def test_eulers_number_notation(self, response, is_latex, elementary_functions, response_latex, response_sympy):
Expand All @@ -117,6 +116,34 @@ def test_eulers_number_notation(self, response, is_latex, elementary_functions,
assert preview["latex"] == response_latex
assert preview["sympy"] == response_sympy

@pytest.mark.parametrize(
"response, is_latex, response_latex, response_sympy, symbols", [
("e**ea", False, "e^{ea}", "E**ea", {"ea": {"aliases": ["ea", "Ea"], "latex": "ea"}}),
("e**Ea", False, "e^{ea}", "E**ea", {"ea": {"aliases": ["ea", "Ea"], "latex": "ea"}}),
("e^{ea}", True, "e^{ea}", "e**ea", {"ea": {"aliases": ["ea", "Ea"], "latex": "ea"}}),
# ("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?
("e**aea", False, "e^{aea}", "E**aea", {"aea": {"aliases": ["aea", "aEa"], "latex": "aea"}}),
("e**aEa", False, "e^{aea}", "E**aea", {"aea": {"aliases": ["aea", "aEa"], "latex": "aea"}}),
("e^{aea}", True, "e^{aea}", "e**aea", {"aea": {"aliases": ["aea", "aEa"], "latex": "aea"}}),
# ("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?
]
)
def test_e_latex(self, response, is_latex, response_latex, response_sympy, symbols):
params = {
"is_latex": is_latex,
"strict_syntax": False,
"elementary_functions": True,
"symbols": symbols,
}

result = preview_function(response, params)
assert "preview" in result.keys()
preview = result["preview"]

assert preview["latex"] == response_latex
assert preview["sympy"] == response_sympy


@pytest.mark.parametrize(
"response, is_latex, response_latex, response_sympy",
[
Expand Down Expand Up @@ -200,5 +227,6 @@ def test_multi_character_implicit_multi_variable(self):
assert result["preview"]["latex"] =='\\frac{a}{bc \\cdot d}'
assert result["preview"]["sympy"] == "a/bcd"


if __name__ == "__main__":
pytest.main(['-xk not slow', "--tb=line", os.path.abspath(__file__)])
80 changes: 62 additions & 18 deletions app/utility/preview_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,31 +37,72 @@ def find_placeholder(exp):
if char not in exp:
return char

def preprocess_E(latex_str: str, placeholder: str) -> str:

def preprocess_E(latex_str: str) -> tuple[str, dict[str, str]]:
"""
Replace all symbols starting with 'E' (including plain 'E') with a
placeholder, so latex2sympy does not interpret 'E' as Euler's number.
Replace all symbols starting with 'E' or 'e' with placeholders,
so latex2sympy does not interpret 'E' as Euler's number.
Returns the modified string and a dict mapping replaced chars to their placeholders.
"""
# Replace E, E_x, ER_2, Efield, etc.
replacements = {}

# Find placeholder for uppercase E if needed
if re.search(r'(?<!\\)E(?:[a-zA-Z_][a-zA-Z0-9_]*|_\{[^}]*\})?', latex_str):
placeholder_E = find_placeholder(latex_str)
if placeholder_E:
replacements['E'] = placeholder_E

# Find placeholder for lowercase e if needed (exclude already used placeholder)
if re.search(r'(?<!\\)e(?:[a-zA-Z_][a-zA-Z0-9_]*|_\{[^}]*\})?', latex_str):
used_chars = latex_str + ''.join(replacements.values())
placeholder_e = find_placeholder(used_chars)
if placeholder_e:
replacements['e'] = placeholder_e

# If no replacements needed, return original string
if not replacements:
return latex_str, {}

# Replace E and e with their respective placeholders
def repl(match):
token = match.group(0)
return placeholder + token[1:]
first_char = token[0]
if first_char in replacements:
return replacements[first_char] + token[1:]
return token

# Match E or e followed by optional alphanumeric/underscore (including within braces)
pattern = re.compile(r'(?<!\\)[Ee](?:[a-zA-Z_][a-zA-Z0-9_]*|_\{[^}]*\})?')
modified_str = pattern.sub(repl, latex_str)

# Match E followed by optional subscript or alphanumeric/underscore
pattern = re.compile(r'(?<![\\a-zA-Z])E([A-Za-z0-9_]*(?:_\{[^}]*\})?)')
return pattern.sub(repl, latex_str)
# Return the modified string and the replacements dict
return modified_str, replacements


def postprocess_E(expr, placeholder):
def postprocess_E(expr, replacements):
"""
Replace all placeholder symbols back to symbols starting with E.
Replace all placeholder symbols back to symbols starting with E or e.

Args:
expr: The sympy expression
replacements: Dict mapping original chars ('E', 'e') to their placeholders
"""
if not replacements:
return expr

# Create reverse mapping: placeholder -> original char
placeholder_to_char = {v: k for k, v in replacements.items()}

subs = {}
for s in expr.free_symbols:
name = str(s)
if name.startswith(placeholder):
new_name = "E" + name[len(placeholder):]
subs[s] = Symbol(new_name)
# Check if this symbol starts with any of our placeholders
for placeholder, original_char in placeholder_to_char.items():
if name.startswith(placeholder):
new_name = original_char + name[len(placeholder):]
subs[s] = Symbol(new_name)
break # Only replace once per symbol

return expr.xreplace(subs)


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

if "\pm" not in symbol_str and "\mp" not in symbol_str:
try:
latex_symbol = latex2sympy(latex_symbol_str)
latex_symbol_str_preprocessed, replacements = preprocess_E(latex_symbol_str)
latex_symbol_parsed = latex2sympy(latex_symbol_str_preprocessed)
latex_symbol_str_postprocess = postprocess_E(latex_symbol_parsed, replacements)

except Exception:
raise ValueError(
f"Couldn't parse latex symbol {latex_symbol_str} "
f"to sympy symbol."
)
substitutions[latex_symbol] = Symbol(sympy_symbol_str)
substitutions[latex_symbol_str_postprocess] = Symbol(sympy_symbol_str)


parsed_responses = set()
for expression in response_set:
try:
e_placeholder = find_placeholder(expression)

expression_preprocessed = preprocess_E(expression, e_placeholder)
expression_preprocessed, replacements = preprocess_E(expression)
expression_parsed = latex2sympy(expression_preprocessed, substitutions)
if isinstance(expression_parsed, list):
expression_parsed = expression_parsed.pop()

expression_postprocess = postprocess_E(expression_parsed, e_placeholder)
expression_postprocess = postprocess_E(expression_parsed, replacements)
if simplify is True:
expression_postprocess = expression_postprocess.simplify()
except Exception as e:
Expand Down