diff --git a/.github/workflows/staging-deploy.yml b/.github/workflows/staging-deploy.yml index 533127f..8a8dd88 100644 --- a/.github/workflows/staging-deploy.yml +++ b/.github/workflows/staging-deploy.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8"] + python-version: ["3.12"] steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/test-lint.yml b/.github/workflows/test-lint.yml index b9d4bce..0f1e91d 100644 --- a/.github/workflows/test-lint.yml +++ b/.github/workflows/test-lint.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8"] + python-version: ["3.12"] steps: - name: Checkout uses: actions/checkout@v4 diff --git a/app/Dockerfile b/app/Dockerfile index 9f5ca25..22e2aee 100755 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -1,12 +1,14 @@ # Base image that bundles AWS Lambda Python 3.8 image with some middleware functions # FROM base-eval-tmp # FROM rabidsheep55/python-base-eval-layer -FROM ghcr.io/lambda-feedback/baseevalutionfunctionlayer:main-3.8 +FROM ghcr.io/lambda-feedback/baseevalutionfunctionlayer:main-3.12 -RUN yum install -y git +RUN dnf install -y git findutils && dnf clean all WORKDIR /app +ENV PYTHONPATH=/app + # Copy and install any packages/modules needed for your evaluation script. COPY requirements.txt . RUN pip3 install -r requirements.txt @@ -57,4 +59,4 @@ RUN chmod 644 $(find . -type f) RUN chmod 755 $(find . -type d) # The entrypoint for AWS is to invoke the handler function within the app package -CMD [ "/app/app.handler" ] +CMD [ "app.handler" ] diff --git a/app/context/symbolic.py b/app/context/symbolic.py index 77c41d9..960c989 100644 --- a/app/context/symbolic.py +++ b/app/context/symbolic.py @@ -1,5 +1,5 @@ from copy import deepcopy -from sympy import Add, Pow, Mul, Equality, pi, im, I, N +from sympy import Add, Pow, Mul, Equality, pi, im, I, N, oo from sympy import re as real_part from ..utility.expression_utilities import ( @@ -83,6 +83,22 @@ def create_expressions_for_comparison(criterion, parameters_dict, local_substitu return lhs_expr, rhs_expr +def do_comparison_infinite(comparison_symbol, lhs_expr, rhs_expr): + # When either side is infinite, subtracting (e.g. oo - oo) yields nan, making + # the subtraction-based do_comparison unreliable. Compare the sides directly instead. + direct_comparisons = { + "=": lhs_expr == rhs_expr, + ">": lhs_expr > rhs_expr, + ">=": lhs_expr >= rhs_expr, + "<": lhs_expr < rhs_expr, + "<=": lhs_expr <= rhs_expr, + } + try: + return bool(direct_comparisons[comparison_symbol.strip()]) + except Exception: + return None + + def do_comparison(comparison_symbol, expression): comparisons = { "=": lambda expr: bool(expression.cancel().simplify().simplify() == 0), @@ -106,7 +122,14 @@ def check_equality(criterion, parameters_dict, local_substitutions=[]): elif not isinstance(lhs_expr, Equality) and isinstance(rhs_expr, Equality): result = False else: - result = do_comparison(criterion.content, lhs_expr-rhs_expr) + try: + either_is_infinite = lhs_expr.is_infinite or rhs_expr.is_infinite + except (KeyError, TypeError): + either_is_infinite = False + if either_is_infinite: + result = do_comparison_infinite(criterion.content, lhs_expr, rhs_expr) + else: + result = do_comparison(criterion.content, lhs_expr-rhs_expr) # There are some types of expression, e.g. those containing hyperbolic trigonometric functions, that can behave # unpredictably when simplification is applied. For that reason we check several different combinations of # simplifications here in order to reduce the likelihood of false negatives. diff --git a/app/evaluation.py b/app/evaluation.py index 891f9af..b2592c0 100644 --- a/app/evaluation.py +++ b/app/evaluation.py @@ -294,6 +294,10 @@ def evaluation_function(response, answer, params, include_test_data=False) -> di if "!" in response: evaluation_result.add_feedback(("NOTATION_WARNING_FACTORIAL", symbolic_comparison_internal_messages("NOTATION_WARNING_FACTORIAL")(dict()))) + if "!!!" in response: + evaluation_result.add_feedback( + ("NOTATION_WARNING_TRIPLE_FACTORIAL", symbolic_comparison_internal_messages("NOTATION_WARNING_TRIPLE_FACTORIAL")(dict()))) + reserved_expressions_success, reserved_expressions = parse_reserved_expressions(reserved_expressions_strings, parameters, evaluation_result) if reserved_expressions_success is False: return evaluation_result.serialise(include_test_data) diff --git a/app/feedback/symbolic.py b/app/feedback/symbolic.py index c4f5c6f..d341fa8 100644 --- a/app/feedback/symbolic.py +++ b/app/feedback/symbolic.py @@ -21,6 +21,7 @@ "PARSE_ERROR": f"`{inputs.get('x','')}` could not be parsed as a valid mathematical expression. Ensure that correct codes for input symbols are used, correct notation is used, that the expression is unambiguous and that all parentheses are closed.", "NOTATION_WARNING_EXPONENT": "Note that `^` cannot be used to denote exponentiation, use `**` instead.", "NOTATION_WARNING_FACTORIAL": "Note that `!` cannot be used to denote factorial, use `factorial(...)` instead.", + "NOTATION_WARNING_TRIPLE_FACTORIAL": "Note that `!!!` is not supported.", "EXPRESSION_NOT_EQUALITY": "The response was an expression but was expected to be an equality.", "EQUALITY_NOT_EXPRESSION": "The response was an equality but was expected to be an expression.", "EQUALITIES_EQUIVALENT": None, diff --git a/app/requirements.txt b/app/requirements.txt index 1ac96c7..1ee4286 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -1,6 +1,6 @@ pydot typing_extensions -mpmath==1.2.1 -sympy==1.12 +mpmath==1.3.0 +sympy==1.14 antlr4-python3-runtime==4.7.2 git+https://github.com/lambda-feedback/latex2sympy.git@master#egg=latex2sympy2 \ No newline at end of file diff --git a/app/tests/symbolic_evaluation_test.py b/app/tests/symbolic_evaluation_test.py index 10c4bc1..9362bfb 100644 --- a/app/tests/symbolic_evaluation_test.py +++ b/app/tests/symbolic_evaluation_test.py @@ -784,10 +784,6 @@ def test_warning_inappropriate_symbol(self): '(0,002*6800*v)/1,2', '(0.002*6800*v)/1.2' ), - ( - '-∞', - '-inf' - ), ( 'x.y', 'x*y' @@ -1887,15 +1883,52 @@ def test_sum_in_answer(self, response, answer, value): result = evaluation_function(response, answer, params) assert result["is_correct"] is value - def test_exclamation_mark_for_factorial(self): - response = "3!" - answer = "factorial(3)" + @pytest.mark.parametrize( + "response, answer, value", + [ + ("3!", "factorial(3)", True), + ("(n+1)!", "factorial(n+1)", True), + ("n!", "factorial(n)", True), + ("a!=b", "factorial(3)", False), + ("2*n!", "2*factorial(n)", True), + ("3!", "3!", True), + ("3*sin(n)!", "3*factorial(sin(n))", True) + ] + ) + def test_exclamation_mark_for_factorial(self, response, answer, value): params = { "strict_syntax": False, "elementary_functions": True, } result = evaluation_function(response, answer, params) - assert result["is_correct"] is True + assert result["is_correct"] is value + + @pytest.mark.parametrize( + "response, answer, value", + [ + ("3!!", "factorial2(3)", True), + ("(n+1)!!", "factorial2(n+1)", True), + ("n!!", "factorial2(n)", True), + ("a!=b", "factorial2(3)", False), + ("2*n!!", "2*factorial2(n)", True), + ("3!!", "3!!", True), + ] + ) + def test_double_exclamation_mark_for_factorial(self, response, answer, value): + params = { + "strict_syntax": False, + "elementary_functions": True, + } + result = evaluation_function(response, answer, params) + assert result["is_correct"] is value + + def test_warning_for_triple_factorial(self): + answer = '2^4!' + response = '2^4!!!' + params = {'strict_syntax': False} + result = evaluation_function(response, answer, params, include_test_data=True) + assert result["is_correct"] is False + assert "NOTATION_WARNING_TRIPLE_FACTORIAL" in result["tags"] def test_alternatives_to_input_symbols_takes_priority_over_elementary_function_alternatives(self): answer = "Ef*exp(x)" @@ -1949,6 +1982,11 @@ def test_unexpected_equalities_in_response_that_generates_set(self): result = evaluation_function(response, answer, params) assert result["is_correct"] is False + def test_infinity_unicode_symbol(self): + params = {'strict_syntax': True, 'elementary_functions': True} + result = evaluation_function('-∞', '-inf', params) + assert result["is_correct"] is True + def test_infinity_alias(self): response = "2.694" answer = "infinity" diff --git a/app/utility/expression_utilities.py b/app/utility/expression_utilities.py index 008e9b5..895ce7c 100644 --- a/app/utility/expression_utilities.py +++ b/app/utility/expression_utilities.py @@ -62,7 +62,7 @@ def _print_log(self, expr, exp=None): ('acsch', ['arccsch', 'arccosech']), ('asech', ['arcsech']), ('exp', ['Exp']), ('E', ['e']), ('log', ['ln']), ('sqrt', []), ('sign', []), ('Abs', ['abs']), ('Max', ['max']), ('Min', ['min']), ('arg', []), ('ceiling', ['ceil']), ('floor', []), - ('oo',['Infinity', 'inf', 'infinity']), + ('oo',['Infinity', 'inf', 'infinity', '∞']), # Special symbols to make sure plus_minus and minus_plus are not destroyed during preprocessing ('plus_minus', []), ('minus_plus', []), # Below this line should probably not be collected with elementary functions. Some like 'common operations' would be a better name