Skip to content

Commit b2d7882

Browse files
Fixed some issues with input symbols and inequalities in criteria
- Made criteria keywords protected from input symbol aliasing - Added extra outcome when order or equality comparison generates an error (e.g. when checking if a > b where a and b are functions of x and a > b for some values of x but not for others).
1 parent 22a68cc commit b2d7882

File tree

6 files changed

+110
-18
lines changed

6 files changed

+110
-18
lines changed

app/context/symbolic.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ def do_comparison(comparison_symbol, expression):
9090
"<=": lambda expr: bool(expression.cancel().simplify().simplify() <= 0),
9191
}
9292
comparison = comparisons[comparison_symbol.strip()]
93-
result = comparison(expression)
93+
try:
94+
result = comparison(expression)
95+
except Exception:
96+
result = None
9497
return result
9598

9699

@@ -218,10 +221,14 @@ def mathematical_equivalence(unused_input):
218221
return {
219222
label+"_TRUE": None
220223
}
221-
else:
224+
elif result is False:
222225
return {
223226
label+"_FALSE": None
224227
}
228+
else:
229+
return {
230+
label+"_UNKNOWN": None
231+
}
225232

226233
def set_equivalence(unused_input):
227234
matches = {"responses": [False]*len(response_list), "answers": [False]*len(answer_list)}
@@ -718,14 +725,19 @@ def criterion_eval_node(criterion, parameters_dict, generate_feedback=True):
718725
def evaluation_node_internal(unused_input):
719726
result = check_criterion(criterion, parameters_dict, generate_feedback)
720727
label = criterion.content_string()
721-
if result:
728+
if result is True:
722729
return {
723730
label+"_TRUE": feedback_string_generator_inputs
724731
}
725-
else:
732+
elif result is False:
726733
return {
727734
label+"_FALSE": feedback_string_generator_inputs
728735
}
736+
else:
737+
return {
738+
label+"_UNKNOWN": feedback_string_generator_inputs
739+
}
740+
729741
label = criterion.content_string()
730742
graph = CriteriaGraph(label)
731743
END = CriteriaGraph.END
@@ -747,6 +759,14 @@ def evaluation_node_internal(unused_input):
747759
feedback_string_generator=symbolic_feedback_string_generators["GENERIC"]("FALSE")
748760
)
749761
graph.attach(label+"_FALSE", END.label)
762+
graph.attach(
763+
label,
764+
label+"_UNKNOWN",
765+
summary="True",
766+
details=label+" is false.",
767+
feedback_string_generator=symbolic_feedback_string_generators["GENERIC"]("FALSE")
768+
)
769+
graph.attach(label+"_UNKNOWN", END.label)
750770
return graph
751771

752772

app/docs/user.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,20 @@ The `criteria` parameter reserves `response` and `answer` as keywords that will
3737

3838
**Note:** The `criteria` parameters functionality is currently under development and will rarely produce appropriate feedback and can be quite difficult to debug.
3939

40+
### Available criteria
41+
42+
**Note:** In the table below EXPRESSION is used to denote some mathematical expression, i.e. a string that contains mathematical symbols and operators, but no equal signs `=` or inequality signs `>`, '<'.
43+
44+
| Name | Syntax | Description | Example |
45+
|-------|:-------------------------------|:------------------------------------|:--------------------|
46+
| EQUAL | `EXPRESSION = EXPRESSION` | Checks if the expressions are equal | `answer = response` - Default way to check equality of expressions |
47+
| ORDER | `EXPRESSION ORDER EXPRESSION` | Checks if the expressions have the given order. ORDER operators can be `>`, `<`, `>=`, `<=` | `answer > response` - Checks if the answer is greater than the response |
48+
| WHERE | `EXPRESSION = EXPRESSION where EXPRESSION = EXPRESSION; ... ; EXPRESSION = EXPRESSION` | Checks if the equality on the left side of `where` are equal if the equalities in the comma-separated list on the right side of `where` | `answer = response where x = 0` - Checks if the curves given by the answer and the response intersect when $x=0$. |
49+
| WRITTEN_AS | `EXPRESSION written as EXPRESSION` | Syntactical comparison, checks if the two expressions are written the same way. | `response written as answer` - Checks if the response is written in the same for as the answer (e.g. if answer is `(x+1)(x+2)` then the response `x^2+3x+2` will not satisfy the criteria but `(x+3)(x+4)` will). |
50+
| PROPORTIONAL | `EXPRESSION proportional to EXPRESSION` | Checks if one expression can be written is equivalent to the other expression multiplied by some constant. | `answer proportional to response` |
51+
| CONTAINS | `EXPRESSION contains EXPRESSION` | Checks if the left expression has the right expression as a subexpression. | `response contains x` - Checks if the response contains the symbol x |
52+
53+
4054
## `elementary_functions`
4155

4256
When using implicit multiplication function names with multiple characters are sometimes split and not interpreted properly. Setting `elementary_functions` to true will reserve the function names listed below and prevent them from being split. If a name is said to have one or more alternatives this means that it will accept the alternative names but the reserved name is what will be shown in the preview.

app/evaluation.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def determine_context(parameters):
5555
input_symbols_reserved_codes.append(input_symbol[0])
5656
input_symbols_reserved_aliases += [ip for ip in input_symbol[1] if len(ip.strip()) > 0]
5757

58-
reserved_keywords_codes = {"where", "written as"}
58+
reserved_keywords_codes = {"where", "written as", "contains"}
5959
reserved_keywords_aliases = {"plus_minus", "minus_plus"}
6060
for re in parameters["reserved_expressions_strings"].values():
6161
reserved_keywords_aliases = reserved_keywords_aliases.union(set(re.keys()))
@@ -239,12 +239,6 @@ def evaluation_function(response, answer, params, include_test_data=False) -> di
239239

240240
parameters = deepcopy(params)
241241

242-
# CONSIDER: Can this be moved into the preprocessing procedures in a consistent way?
243-
# Can it be turned into its own context? Or moved into the determine_context procedure?
244-
# What solution will be most consistently reusable?
245-
if parameters.get("is_latex", False):
246-
response = parse_latex(response, parameters.get("symbols", {}), False)
247-
248242
reserved_expressions_strings = {
249243
"learner": {
250244
"response": response
@@ -269,13 +263,31 @@ def evaluation_function(response, answer, params, include_test_data=False) -> di
269263
else:
270264
evaluation_result.latex = preview["latex"]
271265
evaluation_result.simplified = preview["sympy"]
266+
267+
reserved_expressions_keys = list(reserved_expressions_strings["learner"].keys())+list(reserved_expressions_strings["task"].keys())
272268
parameters.update(
273269
{
274270
"context": context,
275-
"parsing_parameters": context["parsing_parameters_generator"](parameters),
271+
"reserved_keywords": context["reserved_keywords"]+reserved_expressions_keys,
272+
}
273+
)
274+
parsing_parameters = context["parsing_parameters_generator"](parameters, unsplittable_symbols=reserved_expressions_keys)
275+
parameters.update(
276+
{
277+
"parsing_parameters": parsing_parameters,
276278
}
277279
)
278280

281+
# CONSIDER: Can this be moved into the preprocessing procedures in a consistent way?
282+
# Can it be turned into its own context? Or moved into the determine_context procedure?
283+
# What solution will be most consistently reusable?
284+
if parameters.get("is_latex", False):
285+
parameters["reserved_expressions_strings"]["learner"].update(
286+
{
287+
"response": parse_latex(response, parameters.get("symbols", {}), False, parameters=parameters),
288+
}
289+
)
290+
279291
# FIXME: Move this into expression_utilities
280292
if params.get("strict_syntax", True):
281293
if "^" in response:
@@ -287,13 +299,10 @@ def evaluation_function(response, answer, params, include_test_data=False) -> di
287299
if reserved_expressions_success is False:
288300
return evaluation_result.serialise(include_test_data)
289301
reserved_expressions_parsed = {**reserved_expressions["learner"], **reserved_expressions["task"]}
290-
parameters.update({"reserved_keywords": parameters["context"]["reserved_keywords"]+list(reserved_expressions_parsed.keys())})
291302

292303
criteria_parser = context["generate_criteria_parser"](reserved_expressions)
293304
criteria = create_criteria_dict(criteria_parser, parameters)
294305

295-
parsing_parameters = parameters["context"]["parsing_parameters_generator"](parameters, unsplittable_symbols=list(reserved_expressions_parsed.keys()))
296-
297306
evaluation_parameters = FrozenValuesDictionary(
298307
{
299308
"reserved_expressions_strings": reserved_expressions_strings,

app/evaluation_tests.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ class TestEvaluationFunction():
2323
"""
2424

2525
# Import tests that makes sure that mathematical expression comparison works as expected
26-
from .tests.symbolic_evaluation_tests import TestEvaluationFunction as TestSymbolicComparison
26+
#from .tests.symbolic_evaluation_tests import TestEvaluationFunction as TestSymbolicComparison
2727

2828
# Import tests that makes sure that physical quantities are handled as expected
29-
from .tests.physical_quantity_evaluation_tests import TestEvaluationFunction as TestQuantities
29+
#from .tests.physical_quantity_evaluation_tests import TestEvaluationFunction as TestQuantities
3030

3131
# Import tests that corresponds to examples in documentation and examples module
3232
from .tests.example_tests import TestEvaluationFunction as TestExamples
@@ -81,4 +81,4 @@ def test_CHEM40002_1_5_instance_2024_25(self):
8181

8282

8383
if __name__ == "__main__":
84-
pytest.main(['-k not slow', '--tb=line', '--durations=10', os.path.abspath(__file__)])
84+
pytest.main(['-xk not slow', '--tb=short', '--durations=10', os.path.abspath(__file__)])

app/tests/example_tests.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,54 @@ def test_syntactical_comparison(self, response, answer, criteria, value, feedbac
507507
assert result["is_correct"] is value
508508
assert set(feedback_tags) == set(result["tags"])
509509

510+
@pytest.mark.parametrize(
511+
"response, answer, criteria, value, feedback_tags, additional_params",
512+
[
513+
(
514+
"2*x^2+0.5+0.25*sin(x)^2",
515+
"2*x^2",
516+
"answer <= response, 2+answer > response",
517+
False,
518+
[
519+
"answer <= response_TRUE",
520+
"2+answer > response_UNKNOWN",
521+
],
522+
{
523+
"symbol_assumptions": "('x', 'real')"
524+
}
525+
),
526+
(
527+
"pi*n",
528+
"0",
529+
"sin(response)=0, response contains n",
530+
True,
531+
[
532+
"sin(response)=0_TRUE",
533+
"sin(response)=0_SAME_SYMBOLS_TRUE",
534+
"response contains n_TRUE",
535+
],
536+
{
537+
"symbols": {
538+
"n": {
539+
"latex": r"\(n\)",
540+
"aliases": ["i", "k", "N", "I", "K"],
541+
},
542+
},
543+
"symbol_assumptions": "('n', 'integer')"
544+
}
545+
),
546+
]
547+
)
548+
def test_custom_comparison_with_criteria(self, response, answer, criteria, value, feedback_tags, additional_params):
549+
params = {
550+
"strict_syntax": False,
551+
"elementary_functions": True,
552+
"criteria": criteria,
553+
}
554+
params.update(additional_params)
555+
result = evaluation_function(response, answer, params, include_test_data=True)
556+
assert result["is_correct"] is value
557+
assert set(feedback_tags) == set(result["tags"])
510558

511559
if __name__ == "__main__":
512560
pytest.main(['-sk not slow', "--tb=line", os.path.abspath(__file__)])

app/utility/expression_utilities.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,7 @@ def create_sympy_parsing_params(params, unsplittable_symbols=tuple(), symbol_ass
616616
"rationalise": params.get("rationalise", True),
617617
"constants": set(),
618618
"complexNumbers": params["complexNumbers"],
619+
"reserved_keywords": params.get("reserved_keywords",[]),
619620
}
620621

621622
symbol_assumptions = list(symbol_assumptions)

0 commit comments

Comments
 (0)