Skip to content

Commit 2cc0a77

Browse files
authored
Bug/gh 223 implicit multi custom symbol (#228)
* GH-223 Added tests to eval fn to validate that custom multi-character symbols are not currently working, and that in-build symbols (theta) do. * Added normalise function to support custom multi character symbols and implicit multiplication * Started refactor to use a transformer instead * Added a comprehensive test suite * Fixed typo * Updated to use correct parser * Added missing parameters * Switched approach to use substitutions before transformers. * Fixed issue with Chi and Lambda Special functions * Fixed number of parameters * Removed redundant function and fixed existing list comprehension * Removed documentation for the workaround with multicharacter and implicit multiplication * Added test for preview of multicharacter and implicit multiplication * Fixed spelling mistake * Fixed spelling mistake * Removed redundant comments * Updated class name and comment to make it clearer that the tests are only for implicit_higher_precedence
1 parent f2e9f1a commit 2cc0a77

10 files changed

+362
-70
lines changed

app/docs/user.md

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ Changes the implicit multiplication convention. If unset it will default to `equ
2323

2424
If set to `implicit_higher_precedence` then implicit multiplication will have higher precedence than explicit multiplication, i.e. `1/ab` will be equal to `1/(ab)` and `1/a*b` will be equal to `(1/a)*b`.
2525

26-
**_NOTE:_** Currently, if implicit multiplication has higher precedence, then multi-character custom symbols are not supported. For example, if you have defined the symbol `bc` with an answer `a/(bc*d)` then `a/(bcd)` will fail, as it is treated as `a/(b * c * d)`.
27-
2826
If set to `equal_precedence` then implicit multiplication will have the same precedence than explicit multiplication, i.e. both `1/ab` and `1/a*b` will be equal to `(1/a)*b`.
2927

3028
#### `criteria`
@@ -65,7 +63,6 @@ All feedback for all incorrect responses will be replaced with the string that t
6563
The $\pm$ and $\mp$ symbols can be represented in the answer or response by `plus_minus` and `minus_plus` respectively.
6664

6765
Answers or responses that contain $\pm$ or $\mp$ has two possible interpretations which requires further criteria for equality. The grading parameter `multiple_answers_criteria` controls this. The default setting, `all`, is that each answer must have a corresponding answer and vice versa. The setting `all_responses` check that all responses are valid answers and the setting `all_answers` checks that all answers are found among the responses.
68-
6966
#### `physical_quantity`
7067

7168
If unset, `physical_quantity` will default to `false`.
@@ -611,8 +608,6 @@ If you want to use a symbol that is usually reserved for some reserved character
611608
1. Create an input symbol where the code is different that the symbol you want to use, e.g. ‘Ef’ or 'Euler' instead of ‘E’
612609
2. Add the symbol you want to use as an alternative, e.g. the alternatives could be set to ‘E’
613610

614-
See note in [`convention`](#convention) for the limitations of using multi character symbols and implicit multiplication higher precedence.
615-
616611
#### Example:
617612
For the answer:
618613
$A/(\epsilon*l)$, `e` is reserved as Euler's number, so we replace `e` with `ef` or any other character(s) that are not reserved or used in the expression and provide alternatives as input symbols:
@@ -630,27 +625,6 @@ Here the answer $A/(ef*l)$ is marked as correct, and so are the alternatives:
630625
- $A/(e*l)$
631626
- $A/(Ep*l)$
632627

633-
634-
635-
636-
#### Example
637-
As implicit multiplication with higher precedence cannot decypher what is a multi-character code and what are two variables that should be multiplied (see [`convention`](#convention) for more detail), single letter codes should be used.
638-
With `"convention": "implicit_higher_precedence"` set
639-
640-
For the answer:
641-
$A/(\epsilon*l)$, `e` is reserved as Euler's number, so we replace `e` with `b` or any other character(s) that are not reserved or used in the expression and provide alternatives as input symbols:
642-
Symbol: $\epsilon$
643-
Code: b
644-
Alternatives: ϵ,ε,E,e,Ep
645-
646-
Here the following are marked as correct:
647-
- $A/(b*l)$ or $A/(bl)$ or $A/bl$
648-
- $A/(ϵ*l)$ or $A/(ϵl)$ or $A/ϵl$
649-
- $A/(ε*l)$ or $A/(εl)$ or $A/εl$
650-
- $A/(E*l)$ or $A/(El)$ or $A/El$
651-
- $A/(e*l)$ or $A/(el)$ or $A/e*l$
652-
- $A/(Ep*l)$ or $A/(Epl)$ or $A/Epl$
653-
654628
### Overriding greek letters or other reserved symbols with input symbols
655629

656630
Sometimes there can be ambiguities in the expected responses. For example `xi` in a response could either be interpreted as the greek letter $\xi$ or as the multiplication $x \cdot i$.

app/evaluation_tests.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,5 +118,56 @@ def test_euler_preview_evaluate(self):
118118
assert result["is_correct"] is True
119119

120120

121+
def test_theta_character_implict_multi_variable(self):
122+
params = {
123+
"strict_syntax": False,
124+
"elementary_functions": True,
125+
"convention": "implicit_higher_precedence",
126+
"symbols": {
127+
"a": {"aliases": ["a"]},
128+
"bc": {"aliases": ["bc"]},
129+
"d": {"aliases": ["d"]}
130+
},
131+
}
132+
answer = "a/(theta*d)"
133+
response_full = "a/(theta*d)"
134+
response_implicit_bracket = "a/(thetad)"
135+
response_implicit_no_bracket = "a/thetad"
136+
137+
result = evaluation_function(response_full, answer, params)
138+
assert result["is_correct"] is True, "Response: a/(theta*d)"
139+
140+
result = evaluation_function(response_implicit_bracket, answer, params)
141+
assert result["is_correct"] is True, "Response: a/(thetad)"
142+
143+
result = evaluation_function(response_implicit_no_bracket, answer, params)
144+
assert result["is_correct"] is True, "Response: a/thetad"
145+
146+
def test_multi_character_implicit_multi_variable(self):
147+
params = {
148+
"strict_syntax": False,
149+
"elementary_functions": True,
150+
"convention": "implicit_higher_precedence",
151+
"symbols": {
152+
"a": {"aliases": ["a"]},
153+
"bc": {"aliases": ["bc"]},
154+
"d": {"aliases": ["d"]}
155+
},
156+
}
157+
answer = "a/(bc*d)"
158+
response_full = "a/(bc*d)"
159+
response_implicit_bracket = "a/(bcd)"
160+
response_implicit_no_bracket = "a/bcd"
161+
162+
result = evaluation_function(response_full, answer, params)
163+
assert result["is_correct"] is True, "Response: a/(bc*d)"
164+
165+
result = evaluation_function(response_implicit_bracket, answer, params)
166+
assert result["is_correct"] is True, "Response: a/(bcd)"
167+
168+
result = evaluation_function(response_implicit_no_bracket, answer, params)
169+
assert result["is_correct"] is True, "Response: a/bcd"
170+
171+
121172
if __name__ == "__main__":
122173
pytest.main(['-xk not slow', '--tb=short', '--durations=10', os.path.abspath(__file__)])

app/preview_tests.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,5 +172,33 @@ def test_lh_rh_response(self):
172172
assert result["preview"]["latex"] == "x + y=x + y"
173173
assert result["preview"]["sympy"] == "x + y=y + x"
174174

175+
def test_multi_character_implicit_multi_variable(self):
176+
params = {
177+
"strict_syntax": False,
178+
"elementary_functions": True,
179+
"convention": "implicit_higher_precedence",
180+
"symbols": {
181+
"a": {"aliases": ["a"], "latex": "a"},
182+
"bc": {"aliases": ["bc"], "latex": "bc"},
183+
"d": {"aliases": ["d"], "latex": "d"}
184+
},
185+
}
186+
187+
response_full = "a/(bc*d)"
188+
response_implicit_bracket = "a/(bcd)"
189+
response_implicit_no_bracket = "a/bcd"
190+
191+
result = preview_function(response_full, params)
192+
assert result["preview"]["latex"] == '\\frac{a}{bc \\cdot d}'
193+
assert result["preview"]["sympy"] == "a/(bc*d)"
194+
195+
result = preview_function(response_implicit_bracket, params)
196+
assert result["preview"]["latex"] == '\\frac{a}{bc \\cdot d}'
197+
assert result["preview"]["sympy"] == "a/(bcd)"
198+
199+
result = preview_function(response_implicit_no_bracket, params)
200+
assert result["preview"]["latex"] =='\\frac{a}{bc \\cdot d}'
201+
assert result["preview"]["sympy"] == "a/bcd"
202+
175203
if __name__ == "__main__":
176204
pytest.main(['-xk not slow', "--tb=line", os.path.abspath(__file__)])

app/tests/multi_character_test.py

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import pytest
2+
from ..utility.expression_utilities import parse_expression, create_sympy_parsing_params
3+
4+
class TestMultiCharImplicitMultiHigherPrecedenceIntegration:
5+
"""
6+
Integration tests for multi-characters symbols and the convention of implicit_higher_precedence enabled.
7+
"""
8+
9+
def assert_expression_equality(self, response, answer, symbols_dict, local_dict=None):
10+
"""
11+
Helper to parse both response and answer with the same parameters
12+
and assert they result in equivalent SymPy expressions.
13+
"""
14+
if local_dict is None:
15+
local_dict = {}
16+
17+
# define parameters
18+
params = {
19+
"complexNumbers": False,
20+
"strict_syntax": False,
21+
"elementary_functions": True,
22+
"convention": "implicit_higher_precedence",
23+
"symbols": symbols_dict,
24+
}
25+
26+
parsing_params = create_sympy_parsing_params(params)
27+
28+
try:
29+
parsed_response = parse_expression(response, parsing_params)
30+
parsed_answer = parse_expression(answer, parsing_params)
31+
except Exception as e:
32+
pytest.fail(f"Parsing failed for input '{response}' or '{answer}': {str(e)}")
33+
34+
assert parsed_response == parsed_answer, \
35+
f"\nInput: {response}\nExpected: {parsed_answer}\nGot: {parsed_response}"
36+
37+
def test_multi_character_implicit_multi_variable(self):
38+
symbols = {
39+
"a": {"aliases": ["a"]},
40+
"bc": {"aliases": ["bc"]},
41+
"d": {"aliases": ["d"]}
42+
}
43+
answer = "a/(bc*d)"
44+
45+
# Case 1: Full explicit
46+
self.assert_expression_equality("a/(bc*d)", answer, symbols)
47+
48+
# Case 2: Implicit with brackets
49+
self.assert_expression_equality("a/(bcd)", answer, symbols)
50+
51+
# Case 3: Implicit no brackets
52+
self.assert_expression_equality("a/bcd", answer, symbols)
53+
54+
def test_multiple_divisions_with_implicit(self):
55+
"""Test multiple divisions in sequence"""
56+
symbols = {
57+
"a": {"aliases": ["a"]},
58+
"b": {"aliases": ["b"]},
59+
"c": {"aliases": ["c"]},
60+
"d": {"aliases": ["d"]},
61+
"f": {"aliases": ["f"]}
62+
}
63+
64+
answer = "a/(b*c)/(d*f)"
65+
response = "a/bc/df"
66+
self.assert_expression_equality(response, answer, symbols)
67+
68+
def test_addition_with_division_and_implicit(self):
69+
"""Test that addition doesn't interfere"""
70+
symbols = {
71+
"a": {"aliases": ["a"]},
72+
"b": {"aliases": ["b"]},
73+
"c": {"aliases": ["c"]},
74+
"d": {"aliases": ["d"]}
75+
}
76+
answer = "a/(b*c) + d"
77+
response = "a/bc + d"
78+
self.assert_expression_equality(response, answer, symbols)
79+
80+
def test_multiplication_after_division(self):
81+
"""Test explicit multiplication after division"""
82+
symbols = {
83+
"a": {"aliases": ["a"]},
84+
"b": {"aliases": ["b"]},
85+
"c": {"aliases": ["c"]},
86+
"d": {"aliases": ["d"]}
87+
}
88+
89+
answer = "(a/(b*c))*d"
90+
response = "a/bc*d"
91+
self.assert_expression_equality(response, answer, symbols)
92+
93+
def test_power_with_implicit_multiplication(self):
94+
symbols = {
95+
"a": {"aliases": ["a"]},
96+
"b": {"aliases": ["b"]},
97+
"c": {"aliases": ["c"]},
98+
}
99+
answer = "a/(b**2*c)"
100+
response = "a/b^2c"
101+
self.assert_expression_equality(response, answer, symbols)
102+
103+
def test_longer_multi_character_symbols(self):
104+
symbols = {
105+
"x": {"aliases": ["x"]},
106+
"abc": {"aliases": ["abc"]},
107+
"df": {"aliases": ["df"]},
108+
}
109+
answer = "x/(abc*df)"
110+
test_cases = [
111+
"x/(abc*df)",
112+
"x/(abcdf)",
113+
"x/abcdf",
114+
]
115+
for response in test_cases:
116+
self.assert_expression_equality(response, answer, symbols)
117+
118+
def test_overlapping_symbol_names(self):
119+
symbols = {
120+
"a": {"aliases": ["a"]},
121+
"ab": {"aliases": ["ab"]},
122+
"abc": {"aliases": ["abc"]},
123+
"c": {"aliases": ["c"]},
124+
}
125+
answer = "a/(abc*c)"
126+
response = "a/abcc"
127+
self.assert_expression_equality(response, answer, symbols)
128+
129+
def test_numbers_with_implicit_multiplication(self):
130+
"""Test with numeric literals"""
131+
symbols = {
132+
"a": {"aliases": ["a"]},
133+
"b": {"aliases": ["b"]},
134+
}
135+
136+
answer = "a/(2*b)"
137+
response = "a/2b"
138+
self.assert_expression_equality(response, answer, symbols)
139+
140+
def test_number_variable_number(self):
141+
"""Test number followed by variable followed by number"""
142+
symbols = {
143+
"x": {"aliases": ["x"]},
144+
}
145+
146+
answer = "1/(2*x*3)"
147+
response = "1/2x3"
148+
self.assert_expression_equality(response, answer, symbols)
149+
150+
def test_implicit_before_parentheses(self):
151+
"""Test implicit multiplication before parentheses"""
152+
symbols = {
153+
"a": {"aliases": ["a"]},
154+
"b": {"aliases": ["b"]},
155+
"c": {"aliases": ["c"]},
156+
"d": {"aliases": ["d"]},
157+
}
158+
159+
answer = "a*(b*c*d)"
160+
response = "a(bcd)"
161+
self.assert_expression_equality(response, answer, symbols)
162+
163+
def test_implicit_before_parentheses_with_multichar(self):
164+
"""Test implicit multiplication before parentheses with multi-char symbols"""
165+
symbols = {
166+
"a": {"aliases": ["a"]},
167+
"bc": {"aliases": ["bc"]},
168+
"d": {"aliases": ["d"]},
169+
}
170+
171+
answer = "a*(bc*d)"
172+
response = "a(bcd)"
173+
self.assert_expression_equality(response, answer, symbols)
174+
175+
def test_complex_expression(self):
176+
"""Test a complex expression"""
177+
symbols = {
178+
"a": {"aliases": ["a"]},
179+
"b": {"aliases": ["b"]},
180+
"c": {"aliases": ["c"]},
181+
"d": {"aliases": ["d"]},
182+
"f": {"aliases": ["f"]},
183+
"g": {"aliases": ["g"]},
184+
}
185+
answer = "a/(b*c) + d/(g*f)"
186+
response = "a/bc + d/gf"
187+
self.assert_expression_equality(response, answer, symbols)
188+
189+
def test_three_way_implicit_multiplication(self):
190+
"""Test three or more implicitly multiplied terms"""
191+
symbols = {
192+
"a": {"aliases": ["a"]},
193+
"b": {"aliases": ["b"]},
194+
"c": {"aliases": ["c"]},
195+
"d": {"aliases": ["d"]},
196+
"f": {"aliases": ["f"]},
197+
}
198+
answer = "a/(b*c*d*f)"
199+
response = "a/bcdf"
200+
self.assert_expression_equality(response, answer, symbols)
201+
202+
def test_explicit_multiplication_should_not_be_grouped(self):
203+
"""Test that explicit multiplication maintains standard precedence"""
204+
symbols = {
205+
"a": {"aliases": ["a"]},
206+
"b": {"aliases": ["b"]},
207+
"c": {"aliases": ["c"]},
208+
}
209+
answer = "(a/b)*c"
210+
response = "a/b*c"
211+
self.assert_expression_equality(response, answer, symbols)
212+
213+
def test_mixed_implicit_and_explicit(self):
214+
"""Test mixing implicit and explicit multiplication"""
215+
symbols = {
216+
"a": {"aliases": ["a"]},
217+
"b": {"aliases": ["b"]},
218+
"c": {"aliases": ["c"]},
219+
"d": {"aliases": ["d"]},
220+
}
221+
answer = "(a/(b*c))*d"
222+
response = "a/bc*d"
223+
self.assert_expression_equality(response, answer, symbols)

app/tests/symbolic_evaluation_tests.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,15 @@ def test_complex_numbers(self, response, answer):
377377
response = "zeta(2)"
378378
answer = "pi**2/6"
379379
input_variations += generate_input_variations(response, answer)
380+
response = "Lambda(x, x**2)"
381+
answer = "Lambda(x, x**2)"
382+
input_variations += generate_input_variations(response, answer)
383+
response = "Lambda((x, y), x + y)"
384+
answer = "Lambda((x, y), x + y)"
385+
input_variations += generate_input_variations(response, answer)
386+
response = "chi(x)"
387+
answer = "chi(x)"
388+
input_variations += generate_input_variations(response, answer)
380389

381390
@pytest.mark.parametrize("response,answer", generate_input_variations(response, answer))
382391
def test_special_functions(self, response, answer):

app/utility/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)