From 192905ea8148197f0011420bd93002f81c0b2bb5 Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Fri, 16 Oct 2026 12:50:51 +0100 Subject: [PATCH 1/7] Refactor evaluation graph construction: replace repetitive graph initialization logic with `outcome_graph` helper function, update imports, and streamline related modules. --- app/context/graph_helpers.py | 10 +++++++++- app/context/physical_quantity/graphs/order.py | 6 ++---- app/context/symbolic/graphs/generic.py | 6 ++---- app/context/symbolic/graphs/substitution.py | 6 ++---- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/app/context/graph_helpers.py b/app/context/graph_helpers.py index 662ac91..7412dc9 100644 --- a/app/context/graph_helpers.py +++ b/app/context/graph_helpers.py @@ -5,7 +5,7 @@ from collections.abc import Mapping from typing import Any -from lambdafeedback.criteria import CriteriaGraph, FeedbackStringGenerator, OutputNode, ReachedCriteria +from lambdafeedback.criteria import CriteriaGraph, Evaluate, FeedbackStringGenerator, OutputNode, ReachedCriteria def outcome_tag(label: str, result: bool | None, inputs: Mapping[str, Any] | None = None) -> ReachedCriteria: @@ -18,6 +18,14 @@ def outcome_tag(label: str, result: bool | None, inputs: Mapping[str, Any] | Non return {label+"_UNKNOWN": inputs} +def outcome_graph(label: str, summary: str, details: str, evaluate: Evaluate) -> tuple[CriteriaGraph, OutputNode]: + """A new graph `label` with an end node and a single evaluation node `label`, ready for its outcomes to be attached.""" + graph = CriteriaGraph(label) + end = graph.add_end_node() + graph.add_evaluation_node(label, summary=summary, details=details, evaluate=evaluate) + return graph, end + + def attach_terminal_criterion( graph: CriteriaGraph, evaluation_label: str, diff --git a/app/context/physical_quantity/graphs/order.py b/app/context/physical_quantity/graphs/order.py index 5779267..c86b2aa 100644 --- a/app/context/physical_quantity/graphs/order.py +++ b/app/context/physical_quantity/graphs/order.py @@ -5,7 +5,7 @@ from lambdafeedback.criteria import CriteriaGraph, ReachedCriteria from lambdafeedback.slr_parsing import ExprNode -from app.context.graph_helpers import attach_terminal_criterion, outcome_tag +from app.context.graph_helpers import attach_terminal_criterion, outcome_graph, outcome_tag from app.context.physical_quantity.comparison import comparison_function, parse_criterion_sides from app.types import Parameters from app.feedback.physical_quantity import feedback_string_generators as physical_quantity_feedback_string_generators @@ -14,8 +14,6 @@ def comparison_base_graph( criterion: ExprNode, parameters: Parameters, comparison_operator: str, label: str | None = None ) -> CriteriaGraph: - graph = CriteriaGraph(label) # type: ignore[arg-type] # Bug 8 in the bug report: label is None here - END = graph.add_end_node() reserved_expressions = parameters["reserved_expressions"].items() if label is None: @@ -30,7 +28,7 @@ def comparison_base_graph( def compare_evaluate(_response: Any) -> ReachedCriteria: return outcome_tag(label, compare(lhs, rhs, stardard_forms) is True) - graph.add_evaluation_node( + graph, END = outcome_graph( label, summary=f"Is {inputs[0]} {comparison_operator} {inputs[1]}?", details=f"checks if {inputs[0]} {comparison_operator} {inputs[1]}.", diff --git a/app/context/symbolic/graphs/generic.py b/app/context/symbolic/graphs/generic.py index 7e4b172..99c5e2f 100644 --- a/app/context/symbolic/graphs/generic.py +++ b/app/context/symbolic/graphs/generic.py @@ -5,7 +5,7 @@ from lambdafeedback.criteria import CriteriaGraph, ReachedCriteria from lambdafeedback.slr_parsing import ExprNode -from app.context.graph_helpers import attach_terminal_criterion, outcome_tag +from app.context.graph_helpers import attach_terminal_criterion, outcome_graph, outcome_tag from app.context.symbolic.comparison import check_criterion from app.types import Parameters from app.feedback.symbolic import feedback_generators as symbolic_feedback_string_generators @@ -19,9 +19,7 @@ def evaluation_node_internal(_response: Any) -> ReachedCriteria: result = check_criterion(criterion, parameters_dict) return outcome_tag(label, result, feedback_string_generator_inputs) - graph = CriteriaGraph(label) - END = graph.add_end_node() - graph.add_evaluation_node(label, summary=label, details="Checks if "+label+" is true.", evaluate=evaluation_node_internal) + graph, END = outcome_graph(label, summary=label, details="Checks if "+label+" is true.", evaluate=evaluation_node_internal) attach_terminal_criterion( graph, label, diff --git a/app/context/symbolic/graphs/substitution.py b/app/context/symbolic/graphs/substitution.py index 85d6c21..dd11cf7 100644 --- a/app/context/symbolic/graphs/substitution.py +++ b/app/context/symbolic/graphs/substitution.py @@ -8,7 +8,7 @@ from lambdafeedback.expression_parsing import parse_expression from lambdafeedback.slr_parsing import ExprNode -from app.context.graph_helpers import attach_terminal_criterion, outcome_tag +from app.context.graph_helpers import attach_terminal_criterion, outcome_graph, outcome_tag from app.context.symbolic.comparison import check_equality, parse_where_substitutions, where_substitution_nodes from app.context.symbolic.variations import one_addition_to_subtraction, one_exponent_flip, one_swap_addition_and_multiplication from app.types import Parameters, Substitutions @@ -152,9 +152,7 @@ def expression_check(_response: Any) -> ReachedCriteria: return outcome_tag(label, result is True) subs_string = ", ".join([s.content_string() for s in subs]) - graph = CriteriaGraph(label) - END = graph.add_end_node() - graph.add_evaluation_node( + graph, END = outcome_graph( label, summary=label, details="Checks if "+expression.content_string()+" where "+subs_string+".", From 53ec450548912f31aa74bb268532f6cdf59a0e81 Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Fri, 16 Oct 2026 13:09:57 +0100 Subject: [PATCH 2/7] Refactor symbolic comparison: simplify retry logic for expression comparison, eliminate redundant structures, and streamline simplification handling. --- app/context/symbolic/comparison.py | 42 ++++++++++++++---------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/app/context/symbolic/comparison.py b/app/context/symbolic/comparison.py index 72945b2..18bea4b 100644 --- a/app/context/symbolic/comparison.py +++ b/app/context/symbolic/comparison.py @@ -1,6 +1,6 @@ from __future__ import annotations -from collections.abc import Callable, Iterable +from collections.abc import Iterable from sympy import Basic, Equality, im, I from sympy import re as real_part @@ -116,32 +116,28 @@ def check_equality( result = do_comparison_infinite(criterion.content, lhs_expr, rhs_expr) else: result = do_comparison(criterion.content, lhs_expr-rhs_expr) + if result is not False: + return result # 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. - # Each side is simplified at most once, and only if a retry needs it - simplified: dict[str, Basic] = {} - - def simplified_side(side: str, expr: Basic) -> Basic: - if side not in simplified: - simplified[side] = expr.simplify() - return simplified[side] - - differences_to_retry: tuple[Callable[[], Basic], ...] = ( - lambda: lhs_expr-simplified_side("rhs", rhs_expr), - lambda: simplified_side("lhs", lhs_expr)-rhs_expr, - lambda: simplified_side("lhs", lhs_expr)-simplified_side("rhs", rhs_expr), - ) - for difference in differences_to_retry: - if result is not False: - break - result = do_comparison(criterion.content, difference()) - - if result is False: - result = compare_numerically(lhs_expr, rhs_expr, parameters_dict) - - return result + # Each side is simplified only once a retry needs it. + simplified_rhs_expr = rhs_expr.simplify() + result = do_comparison(criterion.content, lhs_expr-simplified_rhs_expr) + if result is not False: + return result + + simplified_lhs_expr = lhs_expr.simplify() + result = do_comparison(criterion.content, simplified_lhs_expr-rhs_expr) + if result is not False: + return result + + result = do_comparison(criterion.content, simplified_lhs_expr-simplified_rhs_expr) + if result is not False: + return result + + return compare_numerically(lhs_expr, rhs_expr, parameters_dict) def check_order(criterion: ExprNode, parameters_dict: Parameters) -> bool | None: From 4748e8201ff0b2a887f4d84a9ec6d1a9718689b1 Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Fri, 16 Oct 2026 13:25:13 +0100 Subject: [PATCH 3/7] Refactor context handling: replace context dictionaries with `Context` dataclass, introduce `base.py` for shared context logic, update evaluation methods to use `Context` object, and streamline context selection logic in `select_context`. --- app/context/base.py | 34 +++++++++++ app/context/physical_quantity/__init__.py | 24 ++++---- app/context/symbolic/__init__.py | 25 ++++---- app/docs/dev.md | 13 ++-- app/evaluation.py | 72 ++++++++++++----------- 5 files changed, 105 insertions(+), 63 deletions(-) create mode 100644 app/context/base.py diff --git a/app/context/base.py b/app/context/base.py new file mode 100644 index 0000000..3d3c1b4 --- /dev/null +++ b/app/context/base.py @@ -0,0 +1,34 @@ +"""The `Context` that specialises the evaluation function for one kind of response (symbolic or physical quantity).""" + +from __future__ import annotations + +from collections.abc import Callable, Mapping +from dataclasses import dataclass +from typing import Any + +from lambdafeedback.criteria import CriteriaGraph +from lambdafeedback.slr_parsing import SLRParser + +from app.preview.common import Result +from app.result import EvaluationResult +from app.types import Parameters + + +@dataclass(frozen=True) +class Context: + # Previews the response, as the preview function would + expression_preview: Callable[[str, Parameters], Result] + # Builds the parser for the criteria from the parsed reserved expressions + generate_criteria_parser: Callable[[Mapping[str, Mapping[str, Any]]], SLRParser] + # `(name, expression, parameters)`: the expression string, ready to be parsed + expression_preprocess: Callable[[str, str, Parameters], str] + # `(name, expression, parsing_parameters, evaluation_result)`: the parsed expression + expression_parse: Callable[[str, str, Any, EvaluationResult], Any] + # Parameters used when they are not given + default_parameters: Mapping[str, Any] + # Criteria used when none are given + default_criteria: tuple[str, ...] + # Builds the criteria graph for each parsed criterion + feedback_procedure_generator: Callable[[Parameters], dict[str, CriteriaGraph]] + # `(parameters, unsplittable_symbols=...)`: the parsing parameters passed to `expression_parse` + parsing_parameters_generator: Callable[..., Any] diff --git a/app/context/physical_quantity/__init__.py b/app/context/physical_quantity/__init__.py index fe77019..de4942f 100644 --- a/app/context/physical_quantity/__init__.py +++ b/app/context/physical_quantity/__init__.py @@ -2,12 +2,14 @@ from collections.abc import Iterable from copy import deepcopy +from types import MappingProxyType from typing import Any from lambdafeedback.criteria import CriteriaGraph from lambdafeedback.expression_parsing import ExpressionParams, SympyParsingConfig from lambdafeedback.units import QuantityParams, preprocess_quantity +from app.context.base import Context from app.context.defaults import base_default_parameters from app.context.physical_quantity.criteria_parser import generate_criteria_parser from app.context.physical_quantity.graphs.match import criterion_match_node @@ -75,16 +77,16 @@ def parsing_parameters_generator( ) -default_criteria = ["response matches answer"] +default_criteria = ("response matches answer",) -context: dict[str, Any] = { - "expression_preview": preview_function, - "generate_criteria_parser": generate_criteria_parser, - "expression_preprocess": expression_preprocess, - "expression_parse": parse_quantity, - "default_parameters": default_parameters, - "default_criteria": default_criteria, - "feedback_procedure_generator": feedback_procedure_generator, - "parsing_parameters_generator": parsing_parameters_generator, -} +context = Context( + expression_preview=preview_function, + generate_criteria_parser=generate_criteria_parser, + expression_preprocess=expression_preprocess, + expression_parse=parse_quantity, + default_parameters=MappingProxyType(default_parameters), + default_criteria=default_criteria, + feedback_procedure_generator=feedback_procedure_generator, + parsing_parameters_generator=parsing_parameters_generator, +) diff --git a/app/context/symbolic/__init__.py b/app/context/symbolic/__init__.py index 30dfd71..7ce9272 100644 --- a/app/context/symbolic/__init__.py +++ b/app/context/symbolic/__init__.py @@ -2,7 +2,7 @@ from collections.abc import Iterable from copy import deepcopy -from typing import Any +from types import MappingProxyType from lambdafeedback.criteria import CriteriaGraph, build_criteria_parser from lambdafeedback.expression_parsing import ( @@ -12,6 +12,7 @@ preprocess_expression, ) +from app.context.base import Context from app.context.defaults import base_default_parameters from app.context.symbolic.graphs.equivalence import criterion_equality_node from app.context.symbolic.graphs.generic import criterion_eval_node @@ -34,7 +35,7 @@ def expression_parse(name: str, expr: str, parameters: SympyParsingConfig, evalu return expressions[0] if len(expressions) == 1 else set(expressions) -default_criteria = ["response = answer"] +default_criteria = ("response = answer",) graph_templates: dict[str, GraphTemplate] = { @@ -59,13 +60,13 @@ def parsing_parameters_generator( return SympyParsingConfig.from_params(ExpressionParams.from_dict(params), unsplittable_symbols, symbol_assumptions) -context: dict[str, Any] = { - "expression_preview": preview_function, - "generate_criteria_parser": build_criteria_parser, - "expression_preprocess": expression_preprocess, - "expression_parse": expression_parse, - "default_parameters": default_parameters, - "default_criteria": default_criteria, - "feedback_procedure_generator": feedback_procedure_generator, - "parsing_parameters_generator": parsing_parameters_generator, -} +context = Context( + expression_preview=preview_function, + generate_criteria_parser=build_criteria_parser, + expression_preprocess=expression_preprocess, + expression_parse=expression_parse, + default_parameters=MappingProxyType(default_parameters), + default_criteria=default_criteria, + feedback_procedure_generator=feedback_procedure_generator, + parsing_parameters_generator=parsing_parameters_generator, +) diff --git a/app/docs/dev.md b/app/docs/dev.md index 951100f..fc39862 100644 --- a/app/docs/dev.md +++ b/app/docs/dev.md @@ -47,24 +47,23 @@ The overall flow of the evaluation procedure can be described as follows: ### Context -The context is a data structure that contains at least the following seven pieces of information: -- `default_parameters` A dictionary where the keys are parameter names and the values are the default values that the evaluation function will use unless another value is provided together with the response. The required fields are context-dependent, currently all contexts start from the default parameters found in `context/defaults.py` and the `physical_quantity` context adds a few extra fields, see the default parameters defined in `context/physical_quantity/__init__.py`. +The context is a frozen `Context` dataclass, defined in `context/base.py`, with the following fields: +- `default_parameters` A read-only mapping where the keys are parameter names and the values are the default values that the evaluation function will use unless another value is provided together with the response. The required fields are context-dependent, currently all contexts start from the default parameters found in `context/defaults.py` and the `physical_quantity` context adds a few extra fields, see the default parameters defined in `context/physical_quantity/__init__.py`. - `expression_parse` function that parses expressions (i.e. the `response` and `answer` inputs) into the form used by the feedback generation procedure. - `expression_preprocess` function that performs string manipulations that makes ensures that correctly written input expressions follows the conventions expected by `expression_parse`. - `expression_preview` is a function that generates a string that can be turned into a human-readable representation of how the evaluation function interpreted the response. - `feedback_procedure_generator` function that generates a function for each criteria that can be used to evaluate if the criteria is satisfied or not. The output from this function should be a list of tags that the feedback string generator can use to produce human readable feedback. -- `feedback_string_generator` function that takes tags and outputs human readable feedback strings. - `generate_criteria_parser` function that generates a parser that can be used to turn the criteria (given in string form) into a form that the feedback generation procedure can use to determine if they are correct or not. +- `default_criteria` the criteria used when none are given in `params`. +- `parsing_parameters_generator` function that builds the parsing parameters passed to `expression_parse` from the evaluation parameters. -The context can also contain other fields if necessary. - -**Remark:** The current implementation uses a dictionary rather than a dedicated class for ease of iteration during the initial development phase. +`select_context` in `evaluation.py` picks the context from `params`. There are currently two different contexts: - `symbolic`: Handles comparisons of various symbolic expressions. Defined in the `context/symbolic/` package. - `physical_quantity`: Handles comparisons of expressions involving units. Defined in the `context/physical_quantity/` package. -Each context package exposes its `context` dictionary from its `__init__.py`. +Each context package exposes its `context` (a `Context`) from its `__init__.py`. **Remark:** Handwritten expressions are sent as latex, which requires extra preprocessing before the right context can be determined in some cases. It should be considered whether a new context, perhaps called `handwritten`, should be created for this purpose. diff --git a/app/evaluation.py b/app/evaluation.py index f9fc812..de4702b 100644 --- a/app/evaluation.py +++ b/app/evaluation.py @@ -13,6 +13,7 @@ parse_latex, ) +from app.context.base import Context from app.context.physical_quantity import context as quantity_context from app.context.symbolic import context as symbolic_context from app.feedback.symbolic import feedback_generators as symbolic_feedback_string_generators, internal_feedback @@ -59,12 +60,18 @@ def _raise_on_collisions(keywords: Iterable[str], symbols: Collection[str], kind raise Exception("`"+"`, `".join(collisions)+f"` are reserved keywords and cannot be used as input symbol {kind}s.") -def determine_context(parameters: Parameters) -> dict[str, Any]: +def select_context(parameters: Parameters) -> Context: + """The physical quantity context if `physical_quantity` is set, otherwise the symbolic context.""" if parameters.get("physical_quantity", False) is True: - context = deepcopy(quantity_context) - else: - context = deepcopy(symbolic_context) + return quantity_context + return symbolic_context + +def _reserved_keywords(parameters: Parameters) -> list[str]: + """ + The keywords reserved by the criteria grammar. + Raises if a reserved keyword or reserved expression name is used as an input symbol. + """ input_symbols_reserved_codes, input_symbols_reserved_aliases = _input_symbol_codes_and_aliases(parameters) reserved_keywords_codes = {"where", "written as", "contains", "proportional to"} @@ -75,15 +82,15 @@ def determine_context(parameters: Parameters) -> dict[str, Any]: _raise_on_collisions(reserved_keywords_codes, input_symbols_reserved_codes, "code") _raise_on_collisions(reserved_keywords_aliases, input_symbols_reserved_aliases, "alternative") - context.update({"reserved_keywords": list(reserved_keywords_codes)}) - return context + return list(reserved_keywords_codes) def parse_reserved_expressions( - reserved_expressions: dict[str, dict[str, str]], parameters: Parameters, result: EvaluationResult + context: Context, reserved_expressions: dict[str, dict[str, str]], parameters: Parameters, result: EvaluationResult ) -> tuple[bool, FrozenValuesDictionary]: """ Input: + context: the context used to preprocess and parse the expressions reserved_expressions: dictionary with the following format { "learner": @@ -97,8 +104,8 @@ def parse_reserved_expressions( parameters: dict that contains evaluation function configuration parameters result: the EvaluationResult object that will hold feedback responses """ - parse = parameters["context"]["expression_parse"] - preprocess = parameters["context"]["expression_preprocess"] + parse = context.expression_parse + preprocess = context.expression_preprocess parsing_parameters = deepcopy(parameters["parsing_parameters"]) symbolic_comparison_internal_messages = symbolic_feedback_string_generators["INTERNAL"] reserved_expressions_dict = FrozenValuesDictionary() @@ -147,10 +154,10 @@ def parse_reserved_expressions( return success, reserved_expressions_dict -def get_criteria_string(parameters: Parameters) -> str: +def get_criteria_string(context: Context, parameters: Parameters) -> str: criteria = parameters.get("criteria", None) if criteria is None: - criteria = ",".join(parameters["context"]["default_criteria"]) + criteria = ",".join(context.default_criteria) if (parameters.get("syntactical_comparison", False) is True) and ("responsewrittenasanswer" not in "".join(criteria.split())): criteria = criteria+", response written as answer" return criteria @@ -180,9 +187,9 @@ def split_criteria(criteria_string: str) -> list[str]: return criteria_string_list -def create_criteria_dict(criteria_parser: SLRParser, parameters: Parameters) -> FrozenValuesDictionary: - preprocess = parameters["context"]["expression_preprocess"] - original_criteria_string = get_criteria_string(parameters) +def create_criteria_dict(context: Context, criteria_parser: SLRParser, parameters: Parameters) -> FrozenValuesDictionary: + preprocess = context.expression_preprocess + original_criteria_string = get_criteria_string(context, parameters) try: criteria_string = preprocess("criteria", original_criteria_string, parameters) except (BracketNotationError, AbsoluteValueNotationError) as e: @@ -240,10 +247,10 @@ def _apply_tolerance_aliases(params: dict[str, Any]) -> None: params["atol"] = params["absolute_tolerance"] -def _build_parameters(params: dict[str, Any], response: str, answer: str) -> dict[str, Any]: +def _build_parameters(context: Context, params: dict[str, Any], response: str, answer: str) -> dict[str, Any]: """ A copy of `params` completed with the context's defaults, the reserved expressions - (with `response` read as LaTeX if `is_latex` is set), the context and the parsing parameters. + (with `response` read as LaTeX if `is_latex` is set), the reserved keywords and the parsing parameters. """ parameters = deepcopy(params) @@ -256,22 +263,20 @@ def _build_parameters(params: dict[str, Any], response: str, answer: str) -> dic } } parameters.update({"reserved_expressions_strings": reserved_expressions_strings}) - context = determine_context(parameters) - default_parameters = context["default_parameters"] - for (key, value) in default_parameters.items(): + reserved_keywords = _reserved_keywords(parameters) + for (key, value) in context.default_parameters.items(): if key not in parameters.keys(): parameters.update({key: value}) if "criteria" not in parameters.keys(): - parameters.update({"criteria": ",".join(context["default_criteria"])}) + parameters.update({"criteria": ",".join(context.default_criteria)}) reserved_expressions_keys = list(reserved_expressions_strings["learner"].keys())+list(reserved_expressions_strings["task"].keys()) parameters.update( { - "context": context, - "reserved_keywords": context["reserved_keywords"]+reserved_expressions_keys, + "reserved_keywords": reserved_keywords+reserved_expressions_keys, } ) - parsing_parameters = context["parsing_parameters_generator"](parameters, unsplittable_symbols=reserved_expressions_keys) + parsing_parameters = context.parsing_parameters_generator(parameters, unsplittable_symbols=reserved_expressions_keys) parameters.update( { "parsing_parameters": parsing_parameters, @@ -279,7 +284,7 @@ def _build_parameters(params: dict[str, Any], response: str, answer: str) -> dic ) # CONSIDER: Can this be moved into the preprocessing procedures in a consistent way? - # Can it be turned into its own context? Or moved into the determine_context procedure? + # Can it be turned into its own context? Or moved into the select_context procedure? # What solution will be most consistently reusable? if parameters.get("is_latex", False): parameters["reserved_expressions_strings"]["learner"].update( @@ -311,11 +316,12 @@ def _add_notation_warnings(evaluation_result: EvaluationResult, response: str, p def _set_preview_fields( - evaluation_result: EvaluationResult, response: str, parameters: Parameters, reserved_expressions: Mapping[str, Mapping[str, Any]] + context: Context, evaluation_result: EvaluationResult, response: str, parameters: Parameters, + reserved_expressions: Mapping[str, Mapping[str, Any]] ) -> None: """Sets the `latex` and `simplified` forms of the response on `evaluation_result`.""" try: - preview = parameters["context"]["expression_preview"](response, deepcopy(parameters))["preview"] + preview = context.expression_preview(response, deepcopy(parameters))["preview"] except Exception: evaluation_result.latex = response evaluation_result.simplified = response @@ -363,26 +369,26 @@ def evaluation_function(response: str, answer: str, params: dict[str, Any], incl evaluation_result = EvaluationResult() evaluation_result.is_correct = False - parameters = _build_parameters(params, response, answer) - context = parameters["context"] + context = select_context(params) + parameters = _build_parameters(context, params, response, answer) _add_notation_warnings(evaluation_result, response, params) reserved_expressions_success, reserved_expressions = parse_reserved_expressions( - parameters["reserved_expressions_strings"], parameters, evaluation_result + context, parameters["reserved_expressions_strings"], parameters, evaluation_result ) if reserved_expressions_success is False: return evaluation_result.serialise(include_test_data) - _set_preview_fields(evaluation_result, response, parameters, reserved_expressions) + _set_preview_fields(context, evaluation_result, response, parameters, reserved_expressions) - criteria_parser = context["generate_criteria_parser"](reserved_expressions) - criteria = create_criteria_dict(criteria_parser, parameters) + criteria_parser = context.generate_criteria_parser(reserved_expressions) + criteria = create_criteria_dict(context, criteria_parser, parameters) evaluation_parameters = _build_evaluation_parameters(parameters, reserved_expressions, criteria, evaluation_result) # Performs evaluation of response - feedback_procedures = context["feedback_procedure_generator"](evaluation_parameters) + feedback_procedures = context.feedback_procedure_generator(evaluation_parameters) for graph in feedback_procedures.values(): _disable_evaluation_nodes(graph, evaluation_parameters["disabled_evaluation_nodes"]) generate_feedback(feedback_procedures, evaluation_parameters) From 721c35f083d7738cb4cf857e6213d2b418709f25 Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Fri, 16 Oct 2026 13:29:04 +0100 Subject: [PATCH 4/7] Refactor quantity parsing and usage: replace `QuantityDict` with `ParsedQuantity` dataclass, update type aliases and dependent methods, and streamline parameter lookups in evaluation logic. --- app/context/physical_quantity/graphs/match.py | 26 +++++------ app/context/physical_quantity/graphs/order.py | 2 +- app/context/physical_quantity/parsing.py | 45 ++++++++++++------- app/types.py | 4 -- 4 files changed, 43 insertions(+), 34 deletions(-) diff --git a/app/context/physical_quantity/graphs/match.py b/app/context/physical_quantity/graphs/match.py index 89ddc7c..c369c2f 100644 --- a/app/context/physical_quantity/graphs/match.py +++ b/app/context/physical_quantity/graphs/match.py @@ -22,7 +22,7 @@ def _tolerances(parameters: Parameters) -> tuple[float, float]: rtol = parameters["parsing_parameters"]["rtol"] atol = parameters["parsing_parameters"]["atol"] if parameters.get('atol', 0) == 0 and parameters.get('rtol', 0) == 0: - ans = parameters["reserved_expressions"]["answer"]["quantity"].value + ans = parameters["reserved_expressions"]["answer"].quantity.value if ans is not None: rtol = compute_relative_tolerance_from_significant_decimals(ans.content_string()) return rtol, atol @@ -32,10 +32,10 @@ def _missing_or_unexpected(parameters: Parameters, label: str, lhs_string: str, """Tag for a response missing a value or unit that the answer has (or vice versa), otherwise None.""" # TODO: Better system for identifying if we expect the response to have value/unit or not if ('answer' in lhs_string and 'response' in rhs_string) or ('response' in lhs_string and 'answer' in rhs_string): - res_value = parameters["reserved_expressions"]["response"]["original"]["value"] - ans_value = parameters["reserved_expressions"]["answer"]["original"]["value"] - res_unit = parameters["reserved_expressions"]["response"]["original"]["unit"] - ans_unit = parameters["reserved_expressions"]["answer"]["original"]["unit"] + res_value = parameters["reserved_expressions"]["response"].original_value + ans_value = parameters["reserved_expressions"]["answer"].original_value + res_unit = parameters["reserved_expressions"]["response"].original_unit + ans_unit = parameters["reserved_expressions"]["answer"].original_unit if res_value is None and ans_value is not None: return {label+"_MISSING_VALUE": {"lhs": lhs_string, "rhs": rhs_string}} if res_value is not None and ans_value is None: @@ -54,13 +54,13 @@ def _within_tolerance(parameters: Parameters, lhs_string: str, rhs_string: str, # numerical tolerances can be applied appropriately if rtol > 0 or atol > 0: if (lhs_string == 'answer' and rhs_string == 'response') or (lhs_string == 'response' and rhs_string == 'answer'): - ans = parameters["reserved_expressions"]["answer"]["standard"]["value"].simplify() - res = parameters["reserved_expressions"]["response"]["standard"]["value"].simplify() + ans = parameters["reserved_expressions"]["answer"].standard_value.simplify() + res = parameters["reserved_expressions"]["response"].standard_value.simplify() if (ans is not None and ans.is_constant()) and (res is not None and res.is_constant()): if rtol > 0 and (ans != 0): value_match = bool(abs(float((ans-res)/ans)) < rtol) elif atol > 0 or (ans == 0): - answer_unit_factor = float(parameters["reserved_expressions"]["answer"]["quantity"].unit_factor) + answer_unit_factor = float(parameters["reserved_expressions"]["answer"].quantity.unit_factor) value_match = bool(abs(float(ans-res)) < atol*answer_unit_factor) return value_match @@ -75,12 +75,12 @@ def quantity_match(_response: Any) -> ReachedCriteria: if missing_or_unexpected is not None: return missing_or_unexpected - substitutions = [(key, expr["standard"]["value"]) for (key, expr) in reserved_expressions] + substitutions = [(key, expr.standard_value) for (key, expr) in reserved_expressions] value_match = is_equal(lhs, rhs, substitutions) if value_match is False: value_match = _within_tolerance(parameters, lhs_string, rhs_string, rtol, atol) - substitutions = [(key, expr["standard"]["unit"]) for (key, expr) in reserved_expressions] + substitutions = [(key, expr.standard_unit) for (key, expr) in reserved_expressions] unit_match = is_equal(lhs, rhs, substitutions) return outcome_tag(label, value_match is True and unit_match is True, {"lhs": lhs_string, "rhs": rhs_string}) @@ -91,7 +91,7 @@ def _dimension_match(parameters: Parameters, label: str, lhs: Basic, rhs: Basic, reserved_expressions = parameters["reserved_expressions"].items() def dimension_match(_response: Any) -> ReachedCriteria: - substitutions = [(key, expr["dimension"]) for (key, expr) in reserved_expressions] + substitutions = [(key, expr.dimension) for (key, expr) in reserved_expressions] dimension_match, _ = is_proportional(lhs, rhs, substitutions) return outcome_tag(label+"_DIMENSION_MATCH", dimension_match is True, {"lhs": lhs_string, "rhs": rhs_string}) return dimension_match @@ -101,14 +101,14 @@ def _unit_comparison(parameters: Parameters, label: str, lhs: Basic, rhs: Basic) reserved_expressions = parameters["reserved_expressions"].items() def unit_comparison(_response: Any) -> ReachedCriteria: - substitutions = [(key, expr["original"]["unit"]) for (key, expr) in reserved_expressions] + substitutions = [(key, expr.original_unit) for (key, expr) in reserved_expressions] if is_equal(lhs, rhs, substitutions) is True: output_tags = { label+"_UNIT_COMPARISON"+"_IDENTICAL": None } else: - local_substitutions = [(key, expr["quantity"].expanded_unit) for (key, expr) in reserved_expressions] + local_substitutions = [(key, expr.quantity.expanded_unit) for (key, expr) in reserved_expressions] result, ratio = is_proportional(lhs, rhs, local_substitutions) # `is_proportional` returns a ratio whenever the result is True if result is True and ratio is not None and ratio >= 1000: diff --git a/app/context/physical_quantity/graphs/order.py b/app/context/physical_quantity/graphs/order.py index c86b2aa..a2756ca 100644 --- a/app/context/physical_quantity/graphs/order.py +++ b/app/context/physical_quantity/graphs/order.py @@ -22,7 +22,7 @@ def comparison_base_graph( inputs = criterion.children _, _, lhs, rhs = parse_criterion_sides(criterion, parameters) - stardard_forms = [(key, expr["standard"]["value"]*expr["standard"]["unit"]) for (key, expr) in reserved_expressions] + stardard_forms = [(key, expr.standard_value*expr.standard_unit) for (key, expr) in reserved_expressions] compare = comparison_function(comparison_operator) def compare_evaluate(_response: Any) -> ReachedCriteria: diff --git a/app/context/physical_quantity/parsing.py b/app/context/physical_quantity/parsing.py index ce87184..2be6c28 100644 --- a/app/context/physical_quantity/parsing.py +++ b/app/context/physical_quantity/parsing.py @@ -1,17 +1,42 @@ from __future__ import annotations +from dataclasses import dataclass + +from sympy import Basic + from lambdafeedback.expression_parsing import parse_expression -from lambdafeedback.units import parse_quantity as parse_physical_quantity +from lambdafeedback.units import PhysicalQuantity, parse_quantity as parse_physical_quantity -from app.types import Parameters, QuantityDict +from app.types import Expression, Parameters from app.feedback.physical_quantity import reverted_unit_feedback from app.result import EvaluationResult -def parse_quantity(name: str, expr: str, parameters: Parameters, evaluation_result: EvaluationResult) -> QuantityDict: +@dataclass(frozen=True) +class ParsedQuantity: + """A physical quantity together with the forms of its value and unit used by the criteria.""" + quantity: PhysicalQuantity + # The value with the unit's factor applied: one expression, a set for several values, or None if there is no value + standard_value: Expression | None + # The value and unit as written, or None if missing + original_value: Basic | None + original_unit: Basic | None + + @property + def standard_unit(self) -> Basic | None: + """The unit in SI base units, without its factor.""" + return self.quantity.standard_unit + + @property + def dimension(self) -> Basic: + return self.quantity.dimension + + +def parse_quantity(name: str, expr: str, parameters: Parameters, evaluation_result: EvaluationResult) -> ParsedQuantity: quantity = parse_physical_quantity(expr, parameters["quantity"], name) for index, reverted in enumerate(quantity.reverted_units): evaluation_result.add_feedback(f"{name}_REVERTED_UNIT_{index}", reverted_unit_feedback(reverted)) + standard_value: Expression | None if quantity.standard_values is None: standard_value = None elif len(quantity.standard_values) == 1: @@ -20,16 +45,4 @@ def parse_quantity(name: str, expr: str, parameters: Parameters, evaluation_resu standard_value = set(quantity.standard_values) value = parse_expression(quantity.value.content_string(), quantity.parsing_config) if quantity.value is not None else None unit = parse_expression(quantity.unit.content_string(), quantity.parsing_config) if quantity.unit is not None else None - quantity_dict = { - "quantity": quantity, - "standard": { - "value": standard_value, - "unit": quantity.standard_unit - }, - "dimension": quantity.dimension, - "original": { - "value": value, - "unit": unit - }, - } - return quantity_dict + return ParsedQuantity(quantity=quantity, standard_value=standard_value, original_value=value, original_unit=unit) diff --git a/app/types.py b/app/types.py index 92859a5..958a7fb 100644 --- a/app/types.py +++ b/app/types.py @@ -13,10 +13,6 @@ # CONSIDER: Replace with a dataclass so that the available parameters are explicit. Parameters: TypeAlias = Mapping[str, Any] -# A parsed physical quantity, as returned by `physical_quantity.parsing.parse_quantity`. -# CONSIDER: Replace with a dataclass. -QuantityDict: TypeAlias = dict[str, Any] - # A parsed response or answer: one expression, or a set of them for a list of answers. Expression: TypeAlias = Basic | set[Basic] From c399e21ba709b69f04c2b3bbc7af1861285836fc Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Fri, 16 Oct 2026 14:51:32 +0100 Subject: [PATCH 5/7] Refactor evaluation parameters: replace `FrozenValuesDictionary` with `EvaluationParameters` dataclass, update all affected modules and type hints, streamline parameter access, and update dev documentation accordingly. --- app/context/base.py | 3 +- app/context/physical_quantity/__init__.py | 5 +- app/context/physical_quantity/comparison.py | 7 ++- app/context/physical_quantity/graphs/match.py | 44 +++++++------- app/context/physical_quantity/graphs/order.py | 14 ++--- app/context/symbolic/__init__.py | 5 +- app/context/symbolic/comparison.py | 19 +++--- app/context/symbolic/graphs/equivalence.py | 26 ++++----- app/context/symbolic/graphs/generic.py | 4 +- .../symbolic/graphs/set_equivalence.py | 13 +++-- app/context/symbolic/graphs/substitution.py | 11 ++-- .../symbolic/graphs/syntactical_form.py | 8 +-- app/context/symbolic/numerical.py | 19 +++--- app/docs/dev.md | 4 +- app/evaluation.py | 58 ++++++++++--------- app/evaluation_parameters.py | 36 ++++++++++++ app/frozen_dict.py | 20 ------- app/types.py | 8 ++- 18 files changed, 165 insertions(+), 139 deletions(-) create mode 100644 app/evaluation_parameters.py delete mode 100644 app/frozen_dict.py diff --git a/app/context/base.py b/app/context/base.py index 3d3c1b4..b1605de 100644 --- a/app/context/base.py +++ b/app/context/base.py @@ -9,6 +9,7 @@ from lambdafeedback.criteria import CriteriaGraph from lambdafeedback.slr_parsing import SLRParser +from app.evaluation_parameters import EvaluationParameters from app.preview.common import Result from app.result import EvaluationResult from app.types import Parameters @@ -29,6 +30,6 @@ class Context: # Criteria used when none are given default_criteria: tuple[str, ...] # Builds the criteria graph for each parsed criterion - feedback_procedure_generator: Callable[[Parameters], dict[str, CriteriaGraph]] + feedback_procedure_generator: Callable[[EvaluationParameters], dict[str, CriteriaGraph]] # `(parameters, unsplittable_symbols=...)`: the parsing parameters passed to `expression_parse` parsing_parameters_generator: Callable[..., Any] diff --git a/app/context/physical_quantity/__init__.py b/app/context/physical_quantity/__init__.py index de4942f..4a7cdec 100644 --- a/app/context/physical_quantity/__init__.py +++ b/app/context/physical_quantity/__init__.py @@ -15,6 +15,7 @@ from app.context.physical_quantity.graphs.match import criterion_match_node from app.context.physical_quantity.graphs.order import greater_than_node, greater_than_or_equal_node, less_than_node, less_than_or_equal_node from app.context.physical_quantity.parsing import parse_quantity +from app.evaluation_parameters import EvaluationParameters from app.types import GraphTemplate, Parameters from app.preview.physical_quantity import preview_function @@ -32,9 +33,9 @@ def expression_preprocess(name: str, expr: str, parameters: Parameters) -> str: } -def feedback_procedure_generator(parameters_dict: Parameters) -> dict[str, CriteriaGraph]: +def feedback_procedure_generator(parameters_dict: EvaluationParameters) -> dict[str, CriteriaGraph]: graphs = dict() - for (label, criterion) in parameters_dict["criteria"].items(): + for (label, criterion) in parameters_dict.criteria.items(): graph_template = graph_templates.get(criterion.label.strip(), criterion_match_node) graphs.update({label: graph_template(criterion, parameters_dict)}) return graphs diff --git a/app/context/physical_quantity/comparison.py b/app/context/physical_quantity/comparison.py index 5f3daf5..cb8664c 100644 --- a/app/context/physical_quantity/comparison.py +++ b/app/context/physical_quantity/comparison.py @@ -8,14 +8,15 @@ from lambdafeedback.slr_parsing import ExprNode from app.context.operators import COMPARISON_OPERATORS -from app.types import Parameters, Substitutions +from app.evaluation_parameters import EvaluationParameters +from app.types import Substitutions NONE_PLACEHOLDER = Symbol('NONE_PLACEHOLDER') -def parse_criterion_sides(criterion: ExprNode, parameters: Parameters) -> tuple[str, str, Basic, Basic]: +def parse_criterion_sides(criterion: ExprNode, parameters: EvaluationParameters) -> tuple[str, str, Basic, Basic]: """The left- and right-hand sides of `criterion` as strings, and parsed without simplification.""" - parsing_params = parameters["parsing_parameters"]["sympy"].replace(simplify=False) + parsing_params = parameters.parsing_parameters["sympy"].replace(simplify=False) lhs_string = criterion.children[0].content_string() rhs_string = criterion.children[1].content_string() return lhs_string, rhs_string, parse_expression(lhs_string, parsing_params), parse_expression(rhs_string, parsing_params) diff --git a/app/context/physical_quantity/graphs/match.py b/app/context/physical_quantity/graphs/match.py index c369c2f..33c4324 100644 --- a/app/context/physical_quantity/graphs/match.py +++ b/app/context/physical_quantity/graphs/match.py @@ -11,31 +11,31 @@ from app.context.graph_helpers import attach_terminal_criterion, outcome_tag from app.context.physical_quantity.comparison import comparison_function, is_proportional, parse_criterion_sides -from app.types import Parameters +from app.evaluation_parameters import EvaluationParameters from app.feedback.physical_quantity import feedback_string_generators as physical_quantity_feedback_string_generators is_equal = comparison_function("=") -def _tolerances(parameters: Parameters) -> tuple[float, float]: +def _tolerances(parameters: EvaluationParameters) -> tuple[float, float]: """`(rtol, atol)`, with `rtol` taken from the significant figures of the answer if no tolerance is given.""" - rtol = parameters["parsing_parameters"]["rtol"] - atol = parameters["parsing_parameters"]["atol"] - if parameters.get('atol', 0) == 0 and parameters.get('rtol', 0) == 0: - ans = parameters["reserved_expressions"]["answer"].quantity.value + rtol = parameters.parsing_parameters["rtol"] + atol = parameters.parsing_parameters["atol"] + if parameters.atol == 0 and parameters.rtol == 0: + ans = parameters.reserved_expressions["answer"].quantity.value if ans is not None: rtol = compute_relative_tolerance_from_significant_decimals(ans.content_string()) return rtol, atol -def _missing_or_unexpected(parameters: Parameters, label: str, lhs_string: str, rhs_string: str) -> ReachedCriteria | None: +def _missing_or_unexpected(parameters: EvaluationParameters, label: str, lhs_string: str, rhs_string: str) -> ReachedCriteria | None: """Tag for a response missing a value or unit that the answer has (or vice versa), otherwise None.""" # TODO: Better system for identifying if we expect the response to have value/unit or not if ('answer' in lhs_string and 'response' in rhs_string) or ('response' in lhs_string and 'answer' in rhs_string): - res_value = parameters["reserved_expressions"]["response"].original_value - ans_value = parameters["reserved_expressions"]["answer"].original_value - res_unit = parameters["reserved_expressions"]["response"].original_unit - ans_unit = parameters["reserved_expressions"]["answer"].original_unit + res_value = parameters.reserved_expressions["response"].original_value + ans_value = parameters.reserved_expressions["answer"].original_value + res_unit = parameters.reserved_expressions["response"].original_unit + ans_unit = parameters.reserved_expressions["answer"].original_unit if res_value is None and ans_value is not None: return {label+"_MISSING_VALUE": {"lhs": lhs_string, "rhs": rhs_string}} if res_value is not None and ans_value is None: @@ -47,28 +47,28 @@ def _missing_or_unexpected(parameters: Parameters, label: str, lhs_string: str, return None -def _within_tolerance(parameters: Parameters, lhs_string: str, rhs_string: str, rtol: float, atol: float) -> bool: +def _within_tolerance(parameters: EvaluationParameters, lhs_string: str, rhs_string: str, rtol: float, atol: float) -> bool: """Whether the standard values of the response and answer are within the numerical tolerances.""" value_match = False # TODO: better analysis of where `answer` is found in the criteria so that # numerical tolerances can be applied appropriately if rtol > 0 or atol > 0: if (lhs_string == 'answer' and rhs_string == 'response') or (lhs_string == 'response' and rhs_string == 'answer'): - ans = parameters["reserved_expressions"]["answer"].standard_value.simplify() - res = parameters["reserved_expressions"]["response"].standard_value.simplify() + ans = parameters.reserved_expressions["answer"].standard_value.simplify() + res = parameters.reserved_expressions["response"].standard_value.simplify() if (ans is not None and ans.is_constant()) and (res is not None and res.is_constant()): if rtol > 0 and (ans != 0): value_match = bool(abs(float((ans-res)/ans)) < rtol) elif atol > 0 or (ans == 0): - answer_unit_factor = float(parameters["reserved_expressions"]["answer"].quantity.unit_factor) + answer_unit_factor = float(parameters.reserved_expressions["answer"].quantity.unit_factor) value_match = bool(abs(float(ans-res)) < atol*answer_unit_factor) return value_match def _quantity_match( - parameters: Parameters, label: str, lhs: Basic, rhs: Basic, lhs_string: str, rhs_string: str, rtol: float, atol: float + parameters: EvaluationParameters, label: str, lhs: Basic, rhs: Basic, lhs_string: str, rhs_string: str, rtol: float, atol: float ) -> Evaluate: - reserved_expressions = parameters["reserved_expressions"].items() + reserved_expressions = parameters.reserved_expressions.items() def quantity_match(_response: Any) -> ReachedCriteria: missing_or_unexpected = _missing_or_unexpected(parameters, label, lhs_string, rhs_string) @@ -87,8 +87,8 @@ def quantity_match(_response: Any) -> ReachedCriteria: return quantity_match -def _dimension_match(parameters: Parameters, label: str, lhs: Basic, rhs: Basic, lhs_string: str, rhs_string: str) -> Evaluate: - reserved_expressions = parameters["reserved_expressions"].items() +def _dimension_match(parameters: EvaluationParameters, label: str, lhs: Basic, rhs: Basic, lhs_string: str, rhs_string: str) -> Evaluate: + reserved_expressions = parameters.reserved_expressions.items() def dimension_match(_response: Any) -> ReachedCriteria: substitutions = [(key, expr.dimension) for (key, expr) in reserved_expressions] @@ -97,8 +97,8 @@ def dimension_match(_response: Any) -> ReachedCriteria: return dimension_match -def _unit_comparison(parameters: Parameters, label: str, lhs: Basic, rhs: Basic) -> Evaluate: - reserved_expressions = parameters["reserved_expressions"].items() +def _unit_comparison(parameters: EvaluationParameters, label: str, lhs: Basic, rhs: Basic) -> Evaluate: + reserved_expressions = parameters.reserved_expressions.items() def unit_comparison(_response: Any) -> ReachedCriteria: substitutions = [(key, expr.original_unit) for (key, expr) in reserved_expressions] @@ -254,7 +254,7 @@ def _add_match_nodes( ) -def criterion_match_node(criterion: ExprNode, parameters: Parameters, label: str | None = None) -> CriteriaGraph: +def criterion_match_node(criterion: ExprNode, parameters: EvaluationParameters, label: str | None = None) -> CriteriaGraph: graph = CriteriaGraph(label) # type: ignore[arg-type] # Bug 8 in the bug report: label is None here END = graph.add_end_node() rtol, atol = _tolerances(parameters) diff --git a/app/context/physical_quantity/graphs/order.py b/app/context/physical_quantity/graphs/order.py index a2756ca..fbc9df8 100644 --- a/app/context/physical_quantity/graphs/order.py +++ b/app/context/physical_quantity/graphs/order.py @@ -7,14 +7,14 @@ from app.context.graph_helpers import attach_terminal_criterion, outcome_graph, outcome_tag from app.context.physical_quantity.comparison import comparison_function, parse_criterion_sides -from app.types import Parameters +from app.evaluation_parameters import EvaluationParameters from app.feedback.physical_quantity import feedback_string_generators as physical_quantity_feedback_string_generators def comparison_base_graph( - criterion: ExprNode, parameters: Parameters, comparison_operator: str, label: str | None = None + criterion: ExprNode, parameters: EvaluationParameters, comparison_operator: str, label: str | None = None ) -> CriteriaGraph: - reserved_expressions = parameters["reserved_expressions"].items() + reserved_expressions = parameters.reserved_expressions.items() if label is None: label = criterion.content_string() @@ -57,19 +57,19 @@ def compare_evaluate(_response: Any) -> ReachedCriteria: return graph -def greater_than_node(criterion: ExprNode, parameters: Parameters, label: str | None = None) -> CriteriaGraph: +def greater_than_node(criterion: ExprNode, parameters: EvaluationParameters, label: str | None = None) -> CriteriaGraph: return comparison_base_graph(criterion, parameters, comparison_operator=">", label=label) -def greater_than_or_equal_node(criterion: ExprNode, parameters: Parameters, label: str | None = None) -> CriteriaGraph: +def greater_than_or_equal_node(criterion: ExprNode, parameters: EvaluationParameters, label: str | None = None) -> CriteriaGraph: # TODO: Add nodes for the equal case return comparison_base_graph(criterion, parameters, comparison_operator=">=", label=label) -def less_than_node(criterion: ExprNode, parameters: Parameters, label: str | None = None) -> CriteriaGraph: +def less_than_node(criterion: ExprNode, parameters: EvaluationParameters, label: str | None = None) -> CriteriaGraph: return comparison_base_graph(criterion, parameters, comparison_operator="<", label=label) -def less_than_or_equal_node(criterion: ExprNode, parameters: Parameters, label: str | None = None) -> CriteriaGraph: +def less_than_or_equal_node(criterion: ExprNode, parameters: EvaluationParameters, label: str | None = None) -> CriteriaGraph: # TODO: Add nodes for the equal case return comparison_base_graph(criterion, parameters, comparison_operator="<=", label=label) diff --git a/app/context/symbolic/__init__.py b/app/context/symbolic/__init__.py index 7ce9272..f6761d5 100644 --- a/app/context/symbolic/__init__.py +++ b/app/context/symbolic/__init__.py @@ -18,6 +18,7 @@ from app.context.symbolic.graphs.generic import criterion_eval_node from app.context.symbolic.graphs.substitution import criterion_where_node from app.context.symbolic.graphs.syntactical_form import criterion_written_as_node +from app.evaluation_parameters import EvaluationParameters from app.result import EvaluationResult from app.types import Expression, GraphTemplate, Parameters from app.preview.symbolic import preview_function @@ -45,9 +46,9 @@ def expression_parse(name: str, expr: str, parameters: SympyParsingConfig, evalu } -def feedback_procedure_generator(parameters_dict: Parameters) -> dict[str, CriteriaGraph]: +def feedback_procedure_generator(parameters_dict: EvaluationParameters) -> dict[str, CriteriaGraph]: graphs = dict() - criteria = set(parameters_dict["criteria"].items()) + criteria = set(parameters_dict.criteria.items()) for (label, criterion) in criteria: graph_template = graph_templates.get(criterion.label, criterion_eval_node) graphs.update({label: graph_template(criterion, parameters_dict)}) diff --git a/app/context/symbolic/comparison.py b/app/context/symbolic/comparison.py index 18bea4b..30850a8 100644 --- a/app/context/symbolic/comparison.py +++ b/app/context/symbolic/comparison.py @@ -10,7 +10,8 @@ from app.context.operators import COMPARISON_OPERATORS from app.context.symbolic.numerical import compare_numerically -from app.types import Parameters, SubstitutionList, Substitutions +from app.evaluation_parameters import EvaluationParameters +from app.types import SubstitutionList, Substitutions def unsimplified_with_unsplittable(parsing_params: SympyParsingConfig, symbols: Iterable[str]) -> SympyParsingConfig: @@ -40,7 +41,7 @@ def parse_where_substitutions(subs_node: ExprNode, parsing_params: SympyParsingC return local_subs -def check_criterion(criterion: ExprNode, parameters_dict: Parameters) -> bool | None: +def check_criterion(criterion: ExprNode, parameters_dict: EvaluationParameters) -> bool | None: label = criterion.label.strip() if label in {"EQUALITY", "WRITTEN_AS"}: result = check_equality(criterion, parameters_dict) @@ -56,10 +57,10 @@ def check_criterion(criterion: ExprNode, parameters_dict: Parameters) -> bool | def create_expressions_for_comparison( - criterion: ExprNode, parameters_dict: Parameters, local_substitutions: Substitutions = () + criterion: ExprNode, parameters_dict: EvaluationParameters, local_substitutions: Substitutions = () ) -> tuple[Basic, Basic]: - reserved_expressions = list(parameters_dict["reserved_expressions"].items()) - parsing_params = unsimplified_with_unsplittable(parameters_dict["parsing_parameters"], parameters_dict["reserved_expressions"].keys()) + reserved_expressions = list(parameters_dict.reserved_expressions.items()) + parsing_params = unsimplified_with_unsplittable(parameters_dict.parsing_parameters, parameters_dict.reserved_expressions.keys()) lhs = criterion.children[0].content_string() rhs = criterion.children[1].content_string() lhs_expr = parse_expression(lhs, parsing_params).subs(local_substitutions).subs(reserved_expressions).subs(local_substitutions) @@ -100,7 +101,7 @@ def do_comparison(comparison_symbol: str, expression: Basic) -> bool | None: def check_equality( - criterion: ExprNode, parameters_dict: Parameters, local_substitutions: Substitutions = () + criterion: ExprNode, parameters_dict: EvaluationParameters, local_substitutions: Substitutions = () ) -> bool | None: lhs_expr, rhs_expr = create_expressions_for_comparison(criterion, parameters_dict, local_substitutions) if isinstance(lhs_expr, Equality) and not isinstance(rhs_expr, Equality): @@ -140,13 +141,13 @@ def check_equality( return compare_numerically(lhs_expr, rhs_expr, parameters_dict) -def check_order(criterion: ExprNode, parameters_dict: Parameters) -> bool | None: +def check_order(criterion: ExprNode, parameters_dict: EvaluationParameters) -> bool | None: lhs_expr, rhs_expr = create_expressions_for_comparison(criterion, parameters_dict) result = do_comparison(criterion.content, lhs_expr-rhs_expr) return result -def check_proportionality(criterion: ExprNode, parameters_dict: Parameters) -> bool | None: +def check_proportionality(criterion: ExprNode, parameters_dict: EvaluationParameters) -> bool | None: lhs_expr, rhs_expr = create_expressions_for_comparison(criterion, parameters_dict) result = None if lhs_expr.cancel().simplify().simplify() != 0: @@ -160,7 +161,7 @@ def check_proportionality(criterion: ExprNode, parameters_dict: Parameters) -> b return result -def check_contains_symbol(criterion: ExprNode, parameters_dict: Parameters) -> bool: +def check_contains_symbol(criterion: ExprNode, parameters_dict: EvaluationParameters) -> bool: lhs_expr, rhs_expr = create_expressions_for_comparison(criterion, parameters_dict) result = rhs_expr in lhs_expr.atoms() return result diff --git a/app/context/symbolic/graphs/equivalence.py b/app/context/symbolic/graphs/equivalence.py index 26b0796..b2c9de3 100644 --- a/app/context/symbolic/graphs/equivalence.py +++ b/app/context/symbolic/graphs/equivalence.py @@ -11,15 +11,15 @@ from app.context.graph_helpers import attach_terminal_criterion, outcome_tag from app.context.symbolic.comparison import check_equality, unsimplified_with_unsplittable from app.context.symbolic.graphs.set_equivalence import add_set_equivalence_nodes, as_sets -from app.types import Parameters +from app.evaluation_parameters import EvaluationParameters from app.feedback.symbolic import feedback_generators as symbolic_feedback_string_generators -def _equality_equivalence(label: str, parameters_dict: Parameters) -> Evaluate: +def _equality_equivalence(label: str, parameters_dict: EvaluationParameters) -> Evaluate: def equality_equivalence(_response: Any) -> ReachedCriteria: result = False - res = parameters_dict["reserved_expressions"]["response"] - ans = parameters_dict["reserved_expressions"]["answer"] + res = parameters_dict.reserved_expressions["response"] + ans = parameters_dict.reserved_expressions["answer"] if (not isinstance(res, Equality)) and isinstance(ans, Equality): return { @@ -40,15 +40,15 @@ def equality_equivalence(_response: Any) -> ReachedCriteria: symbols_in_equality_ratio = res_difference.free_symbols else: symbols_in_equality_ratio = ((res.args[0]-res.args[1])/(ans.args[0]-ans.args[1])).simplify().free_symbols - result = {str(s) for s in symbols_in_equality_ratio}.issubset(parameters_dict["parsing_parameters"].constants) + result = {str(s) for s in symbols_in_equality_ratio}.issubset(parameters_dict.parsing_parameters.constants) return outcome_tag(label, result is True) return equality_equivalence -def _same_symbols(label: str, lhs: str, rhs: str, parameters_dict: Parameters) -> Evaluate: +def _same_symbols(label: str, lhs: str, rhs: str, parameters_dict: EvaluationParameters) -> Evaluate: def same_symbols(_response: Any) -> ReachedCriteria: - local_substitutions = list(parameters_dict["reserved_expressions"].items()) - parsing_params = unsimplified_with_unsplittable(parameters_dict["parsing_parameters"], parameters_dict["reserved_expressions"].keys()) + local_substitutions = list(parameters_dict.reserved_expressions.items()) + parsing_params = unsimplified_with_unsplittable(parameters_dict.parsing_parameters, parameters_dict.reserved_expressions.keys()) lsym = parse_expression(lhs, parsing_params).subs(local_substitutions) rsym = parse_expression(rhs, parsing_params).subs(local_substitutions) result = lsym.free_symbols == rsym.free_symbols @@ -57,7 +57,7 @@ def same_symbols(_response: Any) -> ReachedCriteria: def add_equality_equivalence_nodes( - graph: CriteriaGraph, END: OutputNode, parameters_dict: Parameters, label: str, lhs: str, rhs: str + graph: CriteriaGraph, END: OutputNode, parameters_dict: EvaluationParameters, label: str, lhs: str, rhs: str ) -> None: """Adds the nodes that check if the equalities `lhs` and `rhs` are equivalent to `graph`.""" graph.add_evaluation_node( @@ -114,7 +114,7 @@ def add_equality_equivalence_nodes( def add_mathematical_equivalence_nodes( - graph: CriteriaGraph, END: OutputNode, criterion: ExprNode, parameters_dict: Parameters, label: str, lhs: str, rhs: str + graph: CriteriaGraph, END: OutputNode, criterion: ExprNode, parameters_dict: EvaluationParameters, label: str, lhs: str, rhs: str ) -> None: """ Adds the nodes that check if `lhs` and `rhs` are mathematically equal to `graph`, @@ -181,7 +181,7 @@ def mathematical_equivalence(_response: Any) -> ReachedCriteria: ) -def criterion_equality_node(criterion: ExprNode, parameters_dict: Parameters, label: str | None = None) -> CriteriaGraph: +def criterion_equality_node(criterion: ExprNode, parameters_dict: EvaluationParameters, label: str | None = None) -> CriteriaGraph: if label is None: label = criterion.content_string() @@ -190,8 +190,8 @@ def criterion_equality_node(criterion: ExprNode, parameters_dict: Parameters, la lhs = criterion.children[0].content_string() rhs = criterion.children[1].content_string() - res = parameters_dict["reserved_expressions"]["response"] - ans = parameters_dict["reserved_expressions"]["answer"] + res = parameters_dict.reserved_expressions["response"] + ans = parameters_dict.reserved_expressions["answer"] use_set_equivalence, response_list, answer_list = as_sets(res, ans) if use_set_equivalence is True: diff --git a/app/context/symbolic/graphs/generic.py b/app/context/symbolic/graphs/generic.py index 99c5e2f..f8d38e7 100644 --- a/app/context/symbolic/graphs/generic.py +++ b/app/context/symbolic/graphs/generic.py @@ -7,11 +7,11 @@ from app.context.graph_helpers import attach_terminal_criterion, outcome_graph, outcome_tag from app.context.symbolic.comparison import check_criterion -from app.types import Parameters +from app.evaluation_parameters import EvaluationParameters from app.feedback.symbolic import feedback_generators as symbolic_feedback_string_generators -def criterion_eval_node(criterion: ExprNode, parameters_dict: Parameters) -> CriteriaGraph: +def criterion_eval_node(criterion: ExprNode, parameters_dict: EvaluationParameters) -> CriteriaGraph: feedback_string_generator_inputs = {'criterion': criterion} label = criterion.content_string() diff --git a/app/context/symbolic/graphs/set_equivalence.py b/app/context/symbolic/graphs/set_equivalence.py index 66d5c5d..48be7d8 100644 --- a/app/context/symbolic/graphs/set_equivalence.py +++ b/app/context/symbolic/graphs/set_equivalence.py @@ -9,7 +9,8 @@ from app.context.graph_helpers import attach_terminal_criterion from app.context.symbolic.comparison import check_equality -from app.types import Expression, Parameters +from app.evaluation_parameters import EvaluationParameters +from app.types import Expression from app.feedback.symbolic import feedback_generators as symbolic_feedback_string_generators @@ -28,7 +29,7 @@ def as_sets(response: Expression, answer: Expression) -> tuple[bool, Expression, def add_set_equivalence_nodes( - graph: CriteriaGraph, END: OutputNode, criterion: ExprNode, parameters_dict: Parameters, label: str, lhs: str, rhs: str, + graph: CriteriaGraph, END: OutputNode, criterion: ExprNode, parameters_dict: EvaluationParameters, label: str, lhs: str, rhs: str, response_list: set[Basic], answer_list: set[Basic], ) -> None: """ @@ -48,26 +49,26 @@ def set_equivalence(_response: Any) -> ReachedCriteria: if result is True: matches["responses"][i] = True matches["answers"][j] = True - if parameters_dict["multiple_answers_criteria"] == "all": + if parameters_dict.multiple_answers_criteria == "all": is_correct = all(matches["responses"]) and all(matches["answers"]) if is_correct is False: return { label+"_MULTIPLE_ANSWER_FAIL_ALL": None } - elif parameters_dict["multiple_answers_criteria"] == "all_responses": + elif parameters_dict.multiple_answers_criteria == "all_responses": is_correct = all(matches["responses"]) if is_correct is False: return { label+"_MULTIPLE_ANSWER_FAIL_RESPONSE": None } - elif parameters_dict["multiple_answers_criteria"] == "all_answers": + elif parameters_dict.multiple_answers_criteria == "all_answers": is_correct = all(matches["answers"]) if is_correct is False: return { label+"_MULTIPLE_ANSWER_FAIL_ANSWER": None } else: - raise SyntaxWarning(f"Unknown multiple_answers_criteria: {parameters_dict['multiple_answers_criteria']}") + raise SyntaxWarning(f"Unknown multiple_answers_criteria: {parameters_dict.multiple_answers_criteria}") return { label+"_TRUE": None } diff --git a/app/context/symbolic/graphs/substitution.py b/app/context/symbolic/graphs/substitution.py index dd11cf7..884049b 100644 --- a/app/context/symbolic/graphs/substitution.py +++ b/app/context/symbolic/graphs/substitution.py @@ -11,7 +11,8 @@ from app.context.graph_helpers import attach_terminal_criterion, outcome_graph, outcome_tag from app.context.symbolic.comparison import check_equality, parse_where_substitutions, where_substitution_nodes from app.context.symbolic.variations import one_addition_to_subtraction, one_exponent_flip, one_swap_addition_and_multiplication -from app.types import Parameters, Substitutions +from app.evaluation_parameters import EvaluationParameters +from app.types import Substitutions from app.feedback.symbolic import feedback_generators as symbolic_feedback_string_generators @@ -139,8 +140,8 @@ def get_candidates(_response: Any) -> ReachedCriteria: ) -def criterion_where_node(criterion: ExprNode, parameters_dict: Parameters, label: str | None = None) -> CriteriaGraph: - parsing_params = parameters_dict["parsing_parameters"] +def criterion_where_node(criterion: ExprNode, parameters_dict: EvaluationParameters, label: str | None = None) -> CriteriaGraph: + parsing_params = parameters_dict.parsing_parameters expression = criterion.children[0] subs = where_substitution_nodes(criterion.children[1]) local_subs = parse_where_substitutions(criterion.children[1], parsing_params) @@ -179,8 +180,8 @@ def expression_check(_response: Any) -> ReachedCriteria: if expression_to_vary is None: return graph - reserved_expressions = list(parameters_dict["reserved_expressions"].items()) - response_value = parameters_dict["reserved_expressions"]["response"].subs(local_subs) + reserved_expressions = list(parameters_dict.reserved_expressions.items()) + response_value = parameters_dict.reserved_expressions["response"].subs(local_subs) expression_to_vary = parse_expression(expression_to_vary.content_string(), parsing_params).subs(reserved_expressions) variation_groups = _variation_groups(criterion, expression_to_vary) values_and_expressions, values_and_variations_group = _group_values(expression_to_vary, variation_groups, local_subs) diff --git a/app/context/symbolic/graphs/syntactical_form.py b/app/context/symbolic/graphs/syntactical_form.py index d8aae9f..b977e3c 100644 --- a/app/context/symbolic/graphs/syntactical_form.py +++ b/app/context/symbolic/graphs/syntactical_form.py @@ -7,7 +7,7 @@ from lambdafeedback.slr_parsing import ExprNode from app.context.graph_helpers import outcome_tag -from app.types import Parameters +from app.evaluation_parameters import EvaluationParameters from app.feedback.symbolic import feedback_generators as symbolic_feedback_string_generators from app.feedback.syntactical_forms import PATTERNS as syntactical_forms @@ -60,7 +60,7 @@ def _add_known_form_nodes(graph: CriteriaGraph, label: str, lhs: str, rhs: str, graph.attach(str(lhs)+"_COMPARE_FORM_TO_"+form_label, label+"_FALSE") -def criterion_written_as_node(criterion: ExprNode, parameters_dict: Parameters, label: str | None = None) -> CriteriaGraph: +def criterion_written_as_node(criterion: ExprNode, parameters_dict: EvaluationParameters, label: str | None = None) -> CriteriaGraph: if label is None: label = criterion.content_string() @@ -74,8 +74,8 @@ def criterion_written_as_node(criterion: ExprNode, parameters_dict: Parameters, return graph # TODO: Make a version that can compare the forms of any two reserved expressions - answer = parameters_dict["reserved_expressions_strings"]["task"]["answer"] - response = parameters_dict["reserved_expressions_strings"]["learner"]["response"] + answer = parameters_dict.reserved_expressions_strings["task"]["answer"] + response = parameters_dict.reserved_expressions_strings["learner"]["response"] graph.add_evaluation_node( str(rhs)+"_WRITTEN_AS_KNOWN_FORM", diff --git a/app/context/symbolic/numerical.py b/app/context/symbolic/numerical.py index 684eb84..c15207b 100644 --- a/app/context/symbolic/numerical.py +++ b/app/context/symbolic/numerical.py @@ -2,19 +2,18 @@ from sympy import Basic, pi, N -from app.types import Parameters +from app.evaluation_parameters import EvaluationParameters - -def compare_numerically(lhs_expr: Basic, rhs_expr: Basic, parameters_dict: Parameters) -> bool: +def compare_numerically(lhs_expr: Basic, rhs_expr: Basic, parameters_dict: EvaluationParameters) -> bool: """ Compares `lhs_expr` and `rhs_expr` numerically using the `atol` and `rtol` parameters. Returns False if numerical comparison is not enabled (`numerical` is not set and neither `atol` nor `rtol` is positive), otherwise whether the difference is within both tolerances. """ - if not (parameters_dict.get("numerical", False) - or float(parameters_dict.get("rtol", 0)) > 0 - or float(parameters_dict.get("atol", 0)) > 0): + if not (parameters_dict.numerical + or float(parameters_dict.rtol) > 0 + or float(parameters_dict.atol) > 0): return False # REMARK: 'pi' should be a reserved symbol but it is sometimes not treated as one, possibly because of input symbols. @@ -30,18 +29,18 @@ def replace_pi(expr: Basic) -> Basic: # Separates LHS and RHS, parses and evaluates them res = N(replace_pi(lhs_expr)) ans = N(replace_pi(rhs_expr)) - if float(parameters_dict.get("atol", 0)) > 0: + if float(parameters_dict.atol) > 0: try: absolute_error = abs(float(ans-res)) - error_below_atol = bool(absolute_error < float(parameters_dict["atol"])) + error_below_atol = bool(absolute_error < float(parameters_dict.atol)) except TypeError: error_below_atol = None else: error_below_atol = True - if float(parameters_dict.get("rtol", 0)) > 0: + if float(parameters_dict.rtol) > 0: try: relative_error = abs(float((ans-res)/ans)) - error_below_rtol = bool(relative_error < float(parameters_dict["rtol"])) + error_below_rtol = bool(relative_error < float(parameters_dict.rtol)) except TypeError: error_below_rtol = None else: diff --git a/app/docs/dev.md b/app/docs/dev.md index fc39862..2065d31 100644 --- a/app/docs/dev.md +++ b/app/docs/dev.md @@ -39,8 +39,8 @@ The returned dictionary will be referred to as the `result` in this documentatio The overall flow of the evaluation procedure can be described as follows: 1. The function uses the parameters given in `params` to determine the context of the evaluation. What context means will be discussed in more detail in section TODO: Add section name here. -2. After the context is determined the response, answer and criteria (either supplied via `params` or from the context) are analysed an necessary information is stored for future use in a dictionary with frozen valuues, i.e. a dictionary where new items can be added but existing items cannot be changed. -3. The feedback generating procedure supplied by the context is used to generate feedback based on the contents of the frozen value dictionary. +2. After the context is determined the response, answer and criteria (either supplied via `params` or from the context) are analysed an necessary information is stored for future use in a frozen `EvaluationParameters` dataclass, defined in `evaluation_parameters.py`. +3. The feedback generating procedure supplied by the context is used to generate feedback based on the contents of the `EvaluationParameters`. 4. If all criteria are found to be satisfied the response is considered correct, i.e. the `is_correct` field in the result is set to true and the feedback string and list of tags generated by the feedback generation procedure are added to their respective fields. **TODO** Describe what further information is supplied when `include_test_data` is set to true. diff --git a/app/evaluation.py b/app/evaluation.py index de4702b..a4398df 100644 --- a/app/evaluation.py +++ b/app/evaluation.py @@ -5,7 +5,7 @@ from typing import Any from lambdafeedback.criteria import CriteriaGraph -from lambdafeedback.slr_parsing import SLRParser +from lambdafeedback.slr_parsing import ExprNode, SLRParser from lambdafeedback.expression_parsing import ( AbsoluteValueNotationError, BracketNotationError, @@ -18,7 +18,7 @@ from app.context.symbolic import context as symbolic_context from app.feedback.symbolic import feedback_generators as symbolic_feedback_string_generators, internal_feedback from app.feedback.tags import FeedbackTag, FeedbackTagName -from app.frozen_dict import FrozenValuesDictionary +from app.evaluation_parameters import EvaluationParameters from app.result import EvaluationResult from app.types import Parameters @@ -87,7 +87,7 @@ def _reserved_keywords(parameters: Parameters) -> list[str]: def parse_reserved_expressions( context: Context, reserved_expressions: dict[str, dict[str, str]], parameters: Parameters, result: EvaluationResult -) -> tuple[bool, FrozenValuesDictionary]: +) -> tuple[bool, dict[str, dict[str, Any]]]: """ Input: context: the context used to preprocess and parse the expressions @@ -108,10 +108,10 @@ def parse_reserved_expressions( preprocess = context.expression_preprocess parsing_parameters = deepcopy(parameters["parsing_parameters"]) symbolic_comparison_internal_messages = symbolic_feedback_string_generators["INTERNAL"] - reserved_expressions_dict = FrozenValuesDictionary() + reserved_expressions_dict: dict[str, dict[str, Any]] = {} success = True for key in reserved_expressions.keys(): - reserved_expressions_dict.update({key: FrozenValuesDictionary()}) + reserved_expressions_dict.update({key: {}}) for (label, expr) in reserved_expressions[key].items(): expr_parsed = None original_expr = expr @@ -187,7 +187,7 @@ def split_criteria(criteria_string: str) -> list[str]: return criteria_string_list -def create_criteria_dict(context: Context, criteria_parser: SLRParser, parameters: Parameters) -> FrozenValuesDictionary: +def create_criteria_dict(context: Context, criteria_parser: SLRParser, parameters: Parameters) -> dict[str, ExprNode]: preprocess = context.expression_preprocess original_criteria_string = get_criteria_string(context, parameters) try: @@ -195,11 +195,14 @@ def create_criteria_dict(context: Context, criteria_parser: SLRParser, parameter except (BracketNotationError, AbsoluteValueNotationError) as e: feedback = notation_error_feedback(e, original_criteria_string) raise Exception(internal_feedback(feedback), str(feedback.tag)) from e - criteria_parsed = FrozenValuesDictionary() + criteria_parsed: dict[str, ExprNode] = {} for criterion in split_criteria(criteria_string): try: criterion_tokens = criteria_parser.scan(criterion) criterion_parsed = criteria_parser.parse(criterion_tokens)[0] + # A criterion that is given twice cannot be parsed + if criterion_parsed.content_string() in criteria_parsed: + raise TypeError(f"criterion {criterion_parsed.content_string()!r} is given more than once") criteria_parsed.update({criterion_parsed.content_string(): criterion_parsed}) except Exception as e: raise Exception("Cannot parse criteria: `"+criterion+"`.") from e @@ -213,12 +216,12 @@ def _disable_evaluation_nodes(graph: CriteriaGraph, disabled_evaluation_nodes: C evaluation.replacement = CriteriaGraph.END_LABEL -def generate_feedback(criteria_graphs: Mapping[str, CriteriaGraph], evaluation_parameters: Parameters) -> None: +def generate_feedback(criteria_graphs: Mapping[str, CriteriaGraph], evaluation_parameters: EvaluationParameters) -> None: # Generate feedback from criteria graphs - evaluation_result = evaluation_parameters["evaluation_result"] - response = evaluation_parameters["reserved_expressions"]["response"] + evaluation_result = evaluation_parameters.evaluation_result + response = evaluation_parameters.reserved_expressions["response"] is_correct = True - custom_feedback = evaluation_parameters.get("custom_feedback", {}) + custom_feedback = evaluation_parameters.custom_feedback for (criterion_identifier, graph) in criteria_graphs.items(): # TODO: Find better way to identify main criteria for criteria graph main_criteria = criterion_identifier+"_TRUE" @@ -338,22 +341,21 @@ def _set_preview_fields( def _build_evaluation_parameters( - parameters: Parameters, reserved_expressions: Mapping[str, Mapping[str, Any]], criteria: Mapping[str, Any], evaluation_result: EvaluationResult -) -> FrozenValuesDictionary: - return FrozenValuesDictionary( - { - "reserved_expressions_strings": parameters["reserved_expressions_strings"], - "reserved_expressions": {**reserved_expressions["learner"], **reserved_expressions["task"]}, - "criteria": criteria, - "disabled_evaluation_nodes": parameters.get("disabled_evaluation_nodes", set()), - "parsing_parameters": parameters["parsing_parameters"], - "evaluation_result": evaluation_result, - "multiple_answers_criteria": parameters.get("multiple_answers_criteria", "all"), - "numerical": parameters.get("numerical", False), - "atol": parameters.get("atol", 0), - "rtol": parameters.get("rtol", 0), - "custom_feedback": parameters.get("custom_feedback", {}), - } + parameters: Parameters, reserved_expressions: Mapping[str, Mapping[str, Any]], criteria: Mapping[str, ExprNode], + evaluation_result: EvaluationResult +) -> EvaluationParameters: + return EvaluationParameters( + reserved_expressions_strings=parameters["reserved_expressions_strings"], + reserved_expressions={**reserved_expressions["learner"], **reserved_expressions["task"]}, + criteria=criteria, + disabled_evaluation_nodes=parameters.get("disabled_evaluation_nodes", set()), + parsing_parameters=parameters["parsing_parameters"], + evaluation_result=evaluation_result, + multiple_answers_criteria=parameters.get("multiple_answers_criteria", "all"), + numerical=parameters.get("numerical", False), + atol=parameters.get("atol", 0), + rtol=parameters.get("rtol", 0), + custom_feedback=parameters.get("custom_feedback", {}), ) @@ -390,7 +392,7 @@ def evaluation_function(response: str, answer: str, params: dict[str, Any], incl # Performs evaluation of response feedback_procedures = context.feedback_procedure_generator(evaluation_parameters) for graph in feedback_procedures.values(): - _disable_evaluation_nodes(graph, evaluation_parameters["disabled_evaluation_nodes"]) + _disable_evaluation_nodes(graph, evaluation_parameters.disabled_evaluation_nodes) generate_feedback(feedback_procedures, evaluation_parameters) result = evaluation_result.serialise(include_test_data) diff --git a/app/evaluation_parameters.py b/app/evaluation_parameters.py new file mode 100644 index 0000000..50e945f --- /dev/null +++ b/app/evaluation_parameters.py @@ -0,0 +1,36 @@ +"""The `EvaluationParameters` that the contexts use to build and run the criteria graphs.""" + +from __future__ import annotations + +from collections.abc import Collection, Mapping +from dataclasses import dataclass +from typing import Any + +from lambdafeedback.slr_parsing import ExprNode + +from app.result import EvaluationResult + + +@dataclass(frozen=True) +class EvaluationParameters: + # The reserved expressions as strings, by group: `{"learner": {"response": ...}, "task": {"answer": ...}}` + reserved_expressions_strings: Mapping[str, Mapping[str, str]] + # The parsed reserved expressions by name (both groups together), as returned by the context's `expression_parse` + reserved_expressions: Mapping[str, Any] + # The parsed criteria by their content string + criteria: Mapping[str, ExprNode] + # Labels of the evaluation nodes that skip straight to the end of their graph + disabled_evaluation_nodes: Collection[str] + # The parsing parameters made by the context's `parsing_parameters_generator` + parsing_parameters: Any + # Holds the feedback that is returned + evaluation_result: EvaluationResult + # "all", "all_responses" or "all_answers": which elements must match when the response or answer is a set + multiple_answers_criteria: str + # Whether to compare numerically when symbolic comparison fails + numerical: bool + # The tolerances as given in the request (0 if not given) + atol: float + rtol: float + # Custom feedback strings by tag + custom_feedback: Mapping[str, Any] diff --git a/app/frozen_dict.py b/app/frozen_dict.py deleted file mode 100644 index bfc22a6..0000000 --- a/app/frozen_dict.py +++ /dev/null @@ -1,20 +0,0 @@ -from __future__ import annotations - -from collections import UserDict -from typing import Any - - -class FrozenValuesDictionary(UserDict[str, Any]): - """ - A dictionary where new key:value pairs can be added, - but changing the value for an existing key raises - a TypeError - - `UserDict` sends every write (including `__init__` and `update`) - through `__setitem__`, so that is the only method that needs to check. - """ - def __setitem__(self, key: str, value: Any) -> None: - if key in self: - msg = 'key {!r} already exists with value {!r}' - raise TypeError(msg.format(key, self[key])) - super().__setitem__(key, value) diff --git a/app/types.py b/app/types.py index 958a7fb..191a492 100644 --- a/app/types.py +++ b/app/types.py @@ -9,8 +9,10 @@ from lambdafeedback.slr_parsing import ExprNode from sympy import Basic -# The evaluation parameters passed to the contexts. -# CONSIDER: Replace with a dataclass so that the available parameters are explicit. +from app.evaluation_parameters import EvaluationParameters + +# The request parameters, completed with the context's defaults. +# The criteria graphs use `EvaluationParameters` instead. Parameters: TypeAlias = Mapping[str, Any] # A parsed response or answer: one expression, or a set of them for a list of answers. @@ -23,7 +25,7 @@ SubstitutionList: TypeAlias = list[tuple[str, Basic]] # Builds the criteria graph for a criterion. -GraphTemplate: TypeAlias = Callable[[ExprNode, Parameters], CriteriaGraph] +GraphTemplate: TypeAlias = Callable[[ExprNode, EvaluationParameters], CriteriaGraph] # Feedback text generators by group: `generators[group](tag)(inputs)` is the text (or None). FeedbackGenerators: TypeAlias = dict[str, Callable[[str], FeedbackStringGenerator]] From 79c465d5fa080971c3db8956d7b573c9a8bc6319 Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Fri, 16 Oct 2026 14:55:59 +0100 Subject: [PATCH 6/7] Refactor error reporting in `parse_reserved_expressions`: introduce `_report` helper to centralize feedback and exception handling logic, reduce redundancy, and streamline reserved expression evaluation. --- app/evaluation.py | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/app/evaluation.py b/app/evaluation.py index a4398df..5f7af5f 100644 --- a/app/evaluation.py +++ b/app/evaluation.py @@ -85,6 +85,21 @@ def _reserved_keywords(parameters: Parameters) -> list[str]: return list(reserved_keywords_codes) +def _report( + key: str, result: EvaluationResult, tag: str, message: str | None, error: Exception, cause: Exception | None = None +) -> None: + """ + Reports a problem with a reserved expression: for a learner expression it is added to `result` as feedback, + for a task expression it is an error in the task, so `error` is raised (from `cause`). + """ + if key == "learner": + result.add_feedback(tag, message) + elif cause is None: + raise error + else: + raise error from cause + + def parse_reserved_expressions( context: Context, reserved_expressions: dict[str, dict[str, str]], parameters: Parameters, result: EvaluationResult ) -> tuple[bool, dict[str, dict[str, Any]]]: @@ -120,22 +135,17 @@ def parse_reserved_expressions( except (BracketNotationError, AbsoluteValueNotationError) as e: feedback = notation_error_feedback(e, original_expr) expr = e.expression - if key == "learner": - # Preprocess can only create warnings for responses in this case - result.add_feedback(str(feedback.tag), internal_feedback(feedback)) - else: - raise Exception(internal_feedback(feedback), str(feedback.tag)) from e + # Preprocess can only create warnings for responses in this case + tag = str(feedback.tag) + message = internal_feedback(feedback) + _report(key, result, tag, message, Exception(message, tag), cause=e) reserved_expressions[key][label] = expr if not isinstance(expr, str): raise Exception(f"Reserved expression {label} must be given as a string.") if len(expr.strip()) == 0: - if key == "learner": - result.add_feedback( - f"RESERVED_EXPRESSION_MISSING_{label}", - messages["RESERVED_EXPRESSION_MISSING"](label) - ) - else: - raise Exception(messages["RESERVED_EXPRESSION_MISSING"](label), f"RESERVED_EXPRESSION_MISSING_{label}") + tag = f"RESERVED_EXPRESSION_MISSING_{label}" + message = messages["RESERVED_EXPRESSION_MISSING"](label) + _report(key, result, tag, message, Exception(message, tag)) success = False else: try: @@ -143,13 +153,9 @@ def parse_reserved_expressions( except Exception as e: result.is_correct = False success = False - if key == "learner": - result.add_feedback( - f"PARSE_ERROR_{label}", - symbolic_comparison_internal_messages("PARSE_ERROR")({'x': expr}) - ) - else: - raise Exception(symbolic_comparison_internal_messages("PARSE_ERROR")({'x': expr})) from e + message = symbolic_comparison_internal_messages("PARSE_ERROR")({'x': expr}) + # The error for a task expression has no tag + _report(key, result, f"PARSE_ERROR_{label}", message, Exception(message), cause=e) reserved_expressions_dict[key].update({label: expr_parsed}) return success, reserved_expressions_dict From b94af3ebe863a17a76db6db3906add78544ea2dc Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Fri, 16 Oct 2026 15:00:17 +0100 Subject: [PATCH 7/7] Refactor `parameters_dict` to `parameters`: update function signatures, internal references, and type hints across symbolic evaluation modules to streamline parameter usage and improve consistency. --- app/context/physical_quantity/__init__.py | 6 ++-- app/context/symbolic/__init__.py | 6 ++-- app/context/symbolic/comparison.py | 34 +++++++++--------- app/context/symbolic/graphs/equivalence.py | 36 +++++++++---------- app/context/symbolic/graphs/generic.py | 4 +-- .../symbolic/graphs/set_equivalence.py | 12 +++---- app/context/symbolic/graphs/substitution.py | 10 +++--- .../symbolic/graphs/syntactical_form.py | 6 ++-- app/context/symbolic/numerical.py | 16 ++++----- 9 files changed, 65 insertions(+), 65 deletions(-) diff --git a/app/context/physical_quantity/__init__.py b/app/context/physical_quantity/__init__.py index 4a7cdec..af35556 100644 --- a/app/context/physical_quantity/__init__.py +++ b/app/context/physical_quantity/__init__.py @@ -33,11 +33,11 @@ def expression_preprocess(name: str, expr: str, parameters: Parameters) -> str: } -def feedback_procedure_generator(parameters_dict: EvaluationParameters) -> dict[str, CriteriaGraph]: +def feedback_procedure_generator(parameters: EvaluationParameters) -> dict[str, CriteriaGraph]: graphs = dict() - for (label, criterion) in parameters_dict.criteria.items(): + for (label, criterion) in parameters.criteria.items(): graph_template = graph_templates.get(criterion.label.strip(), criterion_match_node) - graphs.update({label: graph_template(criterion, parameters_dict)}) + graphs.update({label: graph_template(criterion, parameters)}) return graphs diff --git a/app/context/symbolic/__init__.py b/app/context/symbolic/__init__.py index f6761d5..264c3f7 100644 --- a/app/context/symbolic/__init__.py +++ b/app/context/symbolic/__init__.py @@ -46,12 +46,12 @@ def expression_parse(name: str, expr: str, parameters: SympyParsingConfig, evalu } -def feedback_procedure_generator(parameters_dict: EvaluationParameters) -> dict[str, CriteriaGraph]: +def feedback_procedure_generator(parameters: EvaluationParameters) -> dict[str, CriteriaGraph]: graphs = dict() - criteria = set(parameters_dict.criteria.items()) + criteria = set(parameters.criteria.items()) for (label, criterion) in criteria: graph_template = graph_templates.get(criterion.label, criterion_eval_node) - graphs.update({label: graph_template(criterion, parameters_dict)}) + graphs.update({label: graph_template(criterion, parameters)}) return graphs diff --git a/app/context/symbolic/comparison.py b/app/context/symbolic/comparison.py index 30850a8..996bf58 100644 --- a/app/context/symbolic/comparison.py +++ b/app/context/symbolic/comparison.py @@ -41,26 +41,26 @@ def parse_where_substitutions(subs_node: ExprNode, parsing_params: SympyParsingC return local_subs -def check_criterion(criterion: ExprNode, parameters_dict: EvaluationParameters) -> bool | None: +def check_criterion(criterion: ExprNode, parameters: EvaluationParameters) -> bool | None: label = criterion.label.strip() if label in {"EQUALITY", "WRITTEN_AS"}: - result = check_equality(criterion, parameters_dict) + result = check_equality(criterion, parameters) elif label == "ORDER": - result = check_order(criterion, parameters_dict) + result = check_order(criterion, parameters) elif label == "CONTAINS": - result = check_contains_symbol(criterion, parameters_dict) + result = check_contains_symbol(criterion, parameters) elif label == "PROPORTIONAL_TO": - result = check_proportionality(criterion, parameters_dict) + result = check_proportionality(criterion, parameters) else: raise Exception(label) return result def create_expressions_for_comparison( - criterion: ExprNode, parameters_dict: EvaluationParameters, local_substitutions: Substitutions = () + criterion: ExprNode, parameters: EvaluationParameters, local_substitutions: Substitutions = () ) -> tuple[Basic, Basic]: - reserved_expressions = list(parameters_dict.reserved_expressions.items()) - parsing_params = unsimplified_with_unsplittable(parameters_dict.parsing_parameters, parameters_dict.reserved_expressions.keys()) + reserved_expressions = list(parameters.reserved_expressions.items()) + parsing_params = unsimplified_with_unsplittable(parameters.parsing_parameters, parameters.reserved_expressions.keys()) lhs = criterion.children[0].content_string() rhs = criterion.children[1].content_string() lhs_expr = parse_expression(lhs, parsing_params).subs(local_substitutions).subs(reserved_expressions).subs(local_substitutions) @@ -101,9 +101,9 @@ def do_comparison(comparison_symbol: str, expression: Basic) -> bool | None: def check_equality( - criterion: ExprNode, parameters_dict: EvaluationParameters, local_substitutions: Substitutions = () + criterion: ExprNode, parameters: EvaluationParameters, local_substitutions: Substitutions = () ) -> bool | None: - lhs_expr, rhs_expr = create_expressions_for_comparison(criterion, parameters_dict, local_substitutions) + lhs_expr, rhs_expr = create_expressions_for_comparison(criterion, parameters, local_substitutions) if isinstance(lhs_expr, Equality) and not isinstance(rhs_expr, Equality): return False elif not isinstance(lhs_expr, Equality) and isinstance(rhs_expr, Equality): @@ -138,17 +138,17 @@ def check_equality( if result is not False: return result - return compare_numerically(lhs_expr, rhs_expr, parameters_dict) + return compare_numerically(lhs_expr, rhs_expr, parameters) -def check_order(criterion: ExprNode, parameters_dict: EvaluationParameters) -> bool | None: - lhs_expr, rhs_expr = create_expressions_for_comparison(criterion, parameters_dict) +def check_order(criterion: ExprNode, parameters: EvaluationParameters) -> bool | None: + lhs_expr, rhs_expr = create_expressions_for_comparison(criterion, parameters) result = do_comparison(criterion.content, lhs_expr-rhs_expr) return result -def check_proportionality(criterion: ExprNode, parameters_dict: EvaluationParameters) -> bool | None: - lhs_expr, rhs_expr = create_expressions_for_comparison(criterion, parameters_dict) +def check_proportionality(criterion: ExprNode, parameters: EvaluationParameters) -> bool | None: + lhs_expr, rhs_expr = create_expressions_for_comparison(criterion, parameters) result = None if lhs_expr.cancel().simplify().simplify() != 0: result = (rhs_expr/lhs_expr).cancel().simplify() @@ -161,7 +161,7 @@ def check_proportionality(criterion: ExprNode, parameters_dict: EvaluationParame return result -def check_contains_symbol(criterion: ExprNode, parameters_dict: EvaluationParameters) -> bool: - lhs_expr, rhs_expr = create_expressions_for_comparison(criterion, parameters_dict) +def check_contains_symbol(criterion: ExprNode, parameters: EvaluationParameters) -> bool: + lhs_expr, rhs_expr = create_expressions_for_comparison(criterion, parameters) result = rhs_expr in lhs_expr.atoms() return result diff --git a/app/context/symbolic/graphs/equivalence.py b/app/context/symbolic/graphs/equivalence.py index b2c9de3..3597132 100644 --- a/app/context/symbolic/graphs/equivalence.py +++ b/app/context/symbolic/graphs/equivalence.py @@ -15,11 +15,11 @@ from app.feedback.symbolic import feedback_generators as symbolic_feedback_string_generators -def _equality_equivalence(label: str, parameters_dict: EvaluationParameters) -> Evaluate: +def _equality_equivalence(label: str, parameters: EvaluationParameters) -> Evaluate: def equality_equivalence(_response: Any) -> ReachedCriteria: result = False - res = parameters_dict.reserved_expressions["response"] - ans = parameters_dict.reserved_expressions["answer"] + res = parameters.reserved_expressions["response"] + ans = parameters.reserved_expressions["answer"] if (not isinstance(res, Equality)) and isinstance(ans, Equality): return { @@ -40,15 +40,15 @@ def equality_equivalence(_response: Any) -> ReachedCriteria: symbols_in_equality_ratio = res_difference.free_symbols else: symbols_in_equality_ratio = ((res.args[0]-res.args[1])/(ans.args[0]-ans.args[1])).simplify().free_symbols - result = {str(s) for s in symbols_in_equality_ratio}.issubset(parameters_dict.parsing_parameters.constants) + result = {str(s) for s in symbols_in_equality_ratio}.issubset(parameters.parsing_parameters.constants) return outcome_tag(label, result is True) return equality_equivalence -def _same_symbols(label: str, lhs: str, rhs: str, parameters_dict: EvaluationParameters) -> Evaluate: +def _same_symbols(label: str, lhs: str, rhs: str, parameters: EvaluationParameters) -> Evaluate: def same_symbols(_response: Any) -> ReachedCriteria: - local_substitutions = list(parameters_dict.reserved_expressions.items()) - parsing_params = unsimplified_with_unsplittable(parameters_dict.parsing_parameters, parameters_dict.reserved_expressions.keys()) + local_substitutions = list(parameters.reserved_expressions.items()) + parsing_params = unsimplified_with_unsplittable(parameters.parsing_parameters, parameters.reserved_expressions.keys()) lsym = parse_expression(lhs, parsing_params).subs(local_substitutions) rsym = parse_expression(rhs, parsing_params).subs(local_substitutions) result = lsym.free_symbols == rsym.free_symbols @@ -57,14 +57,14 @@ def same_symbols(_response: Any) -> ReachedCriteria: def add_equality_equivalence_nodes( - graph: CriteriaGraph, END: OutputNode, parameters_dict: EvaluationParameters, label: str, lhs: str, rhs: str + graph: CriteriaGraph, END: OutputNode, parameters: EvaluationParameters, label: str, lhs: str, rhs: str ) -> None: """Adds the nodes that check if the equalities `lhs` and `rhs` are equivalent to `graph`.""" graph.add_evaluation_node( label, summary=label, details="Checks if "+str(lhs)+" is equivalent to "+str(rhs)+".", - evaluate=_equality_equivalence(label, parameters_dict) + evaluate=_equality_equivalence(label, parameters) ) attach_terminal_criterion( graph, @@ -114,14 +114,14 @@ def add_equality_equivalence_nodes( def add_mathematical_equivalence_nodes( - graph: CriteriaGraph, END: OutputNode, criterion: ExprNode, parameters_dict: EvaluationParameters, label: str, lhs: str, rhs: str + graph: CriteriaGraph, END: OutputNode, criterion: ExprNode, parameters: EvaluationParameters, label: str, lhs: str, rhs: str ) -> None: """ Adds the nodes that check if `lhs` and `rhs` are mathematically equal to `graph`, and if they are, whether they have the same symbols. """ def mathematical_equivalence(_response: Any) -> ReachedCriteria: - return outcome_tag(label, check_equality(criterion, parameters_dict)) + return outcome_tag(label, check_equality(criterion, parameters)) graph.add_evaluation_node( label, @@ -141,7 +141,7 @@ def mathematical_equivalence(_response: Any) -> ReachedCriteria: label+"_SAME_SYMBOLS", summary=str(lhs)+" has the same symbols as "+str(rhs), details=str(lhs)+" has the same (free) symbols as "+str(rhs)+".", - evaluate=_same_symbols(label, lhs, rhs, parameters_dict) + evaluate=_same_symbols(label, lhs, rhs, parameters) ) attach_terminal_criterion( graph, @@ -181,7 +181,7 @@ def mathematical_equivalence(_response: Any) -> ReachedCriteria: ) -def criterion_equality_node(criterion: ExprNode, parameters_dict: EvaluationParameters, label: str | None = None) -> CriteriaGraph: +def criterion_equality_node(criterion: ExprNode, parameters: EvaluationParameters, label: str | None = None) -> CriteriaGraph: if label is None: label = criterion.content_string() @@ -190,14 +190,14 @@ def criterion_equality_node(criterion: ExprNode, parameters_dict: EvaluationPara lhs = criterion.children[0].content_string() rhs = criterion.children[1].content_string() - res = parameters_dict.reserved_expressions["response"] - ans = parameters_dict.reserved_expressions["answer"] + res = parameters.reserved_expressions["response"] + ans = parameters.reserved_expressions["answer"] use_set_equivalence, response_list, answer_list = as_sets(res, ans) if use_set_equivalence is True: - add_set_equivalence_nodes(graph, END, criterion, parameters_dict, label, lhs, rhs, response_list, answer_list) + add_set_equivalence_nodes(graph, END, criterion, parameters, label, lhs, rhs, response_list, answer_list) elif isinstance(res, Equality) or isinstance(ans, Equality): - add_equality_equivalence_nodes(graph, END, parameters_dict, label, lhs, rhs) + add_equality_equivalence_nodes(graph, END, parameters, label, lhs, rhs) else: - add_mathematical_equivalence_nodes(graph, END, criterion, parameters_dict, label, lhs, rhs) + add_mathematical_equivalence_nodes(graph, END, criterion, parameters, label, lhs, rhs) return graph diff --git a/app/context/symbolic/graphs/generic.py b/app/context/symbolic/graphs/generic.py index f8d38e7..6fc80bf 100644 --- a/app/context/symbolic/graphs/generic.py +++ b/app/context/symbolic/graphs/generic.py @@ -11,12 +11,12 @@ from app.feedback.symbolic import feedback_generators as symbolic_feedback_string_generators -def criterion_eval_node(criterion: ExprNode, parameters_dict: EvaluationParameters) -> CriteriaGraph: +def criterion_eval_node(criterion: ExprNode, parameters: EvaluationParameters) -> CriteriaGraph: feedback_string_generator_inputs = {'criterion': criterion} label = criterion.content_string() def evaluation_node_internal(_response: Any) -> ReachedCriteria: - result = check_criterion(criterion, parameters_dict) + result = check_criterion(criterion, parameters) return outcome_tag(label, result, feedback_string_generator_inputs) graph, END = outcome_graph(label, summary=label, details="Checks if "+label+" is true.", evaluate=evaluation_node_internal) diff --git a/app/context/symbolic/graphs/set_equivalence.py b/app/context/symbolic/graphs/set_equivalence.py index 48be7d8..ec063f7 100644 --- a/app/context/symbolic/graphs/set_equivalence.py +++ b/app/context/symbolic/graphs/set_equivalence.py @@ -29,7 +29,7 @@ def as_sets(response: Expression, answer: Expression) -> tuple[bool, Expression, def add_set_equivalence_nodes( - graph: CriteriaGraph, END: OutputNode, criterion: ExprNode, parameters_dict: EvaluationParameters, label: str, lhs: str, rhs: str, + graph: CriteriaGraph, END: OutputNode, criterion: ExprNode, parameters: EvaluationParameters, label: str, lhs: str, rhs: str, response_list: set[Basic], answer_list: set[Basic], ) -> None: """ @@ -45,30 +45,30 @@ def set_equivalence(_response: Any) -> ReachedCriteria: if isinstance(response, Equality) != isinstance(answer, Equality): result = False else: - result = check_equality(criterion, parameters_dict, local_substitutions=current_pair) + result = check_equality(criterion, parameters, local_substitutions=current_pair) if result is True: matches["responses"][i] = True matches["answers"][j] = True - if parameters_dict.multiple_answers_criteria == "all": + if parameters.multiple_answers_criteria == "all": is_correct = all(matches["responses"]) and all(matches["answers"]) if is_correct is False: return { label+"_MULTIPLE_ANSWER_FAIL_ALL": None } - elif parameters_dict.multiple_answers_criteria == "all_responses": + elif parameters.multiple_answers_criteria == "all_responses": is_correct = all(matches["responses"]) if is_correct is False: return { label+"_MULTIPLE_ANSWER_FAIL_RESPONSE": None } - elif parameters_dict.multiple_answers_criteria == "all_answers": + elif parameters.multiple_answers_criteria == "all_answers": is_correct = all(matches["answers"]) if is_correct is False: return { label+"_MULTIPLE_ANSWER_FAIL_ANSWER": None } else: - raise SyntaxWarning(f"Unknown multiple_answers_criteria: {parameters_dict.multiple_answers_criteria}") + raise SyntaxWarning(f"Unknown multiple_answers_criteria: {parameters.multiple_answers_criteria}") return { label+"_TRUE": None } diff --git a/app/context/symbolic/graphs/substitution.py b/app/context/symbolic/graphs/substitution.py index 884049b..8146b8e 100644 --- a/app/context/symbolic/graphs/substitution.py +++ b/app/context/symbolic/graphs/substitution.py @@ -140,8 +140,8 @@ def get_candidates(_response: Any) -> ReachedCriteria: ) -def criterion_where_node(criterion: ExprNode, parameters_dict: EvaluationParameters, label: str | None = None) -> CriteriaGraph: - parsing_params = parameters_dict.parsing_parameters +def criterion_where_node(criterion: ExprNode, parameters: EvaluationParameters, label: str | None = None) -> CriteriaGraph: + parsing_params = parameters.parsing_parameters expression = criterion.children[0] subs = where_substitution_nodes(criterion.children[1]) local_subs = parse_where_substitutions(criterion.children[1], parsing_params) @@ -149,7 +149,7 @@ def criterion_where_node(criterion: ExprNode, parameters_dict: EvaluationParamet label = criterion.content_string() def expression_check(_response: Any) -> ReachedCriteria: - result = check_equality(expression, parameters_dict, local_substitutions=local_subs) + result = check_equality(expression, parameters, local_substitutions=local_subs) return outcome_tag(label, result is True) subs_string = ", ".join([s.content_string() for s in subs]) @@ -180,8 +180,8 @@ def expression_check(_response: Any) -> ReachedCriteria: if expression_to_vary is None: return graph - reserved_expressions = list(parameters_dict.reserved_expressions.items()) - response_value = parameters_dict.reserved_expressions["response"].subs(local_subs) + reserved_expressions = list(parameters.reserved_expressions.items()) + response_value = parameters.reserved_expressions["response"].subs(local_subs) expression_to_vary = parse_expression(expression_to_vary.content_string(), parsing_params).subs(reserved_expressions) variation_groups = _variation_groups(criterion, expression_to_vary) values_and_expressions, values_and_variations_group = _group_values(expression_to_vary, variation_groups, local_subs) diff --git a/app/context/symbolic/graphs/syntactical_form.py b/app/context/symbolic/graphs/syntactical_form.py index b977e3c..c7d1732 100644 --- a/app/context/symbolic/graphs/syntactical_form.py +++ b/app/context/symbolic/graphs/syntactical_form.py @@ -60,7 +60,7 @@ def _add_known_form_nodes(graph: CriteriaGraph, label: str, lhs: str, rhs: str, graph.attach(str(lhs)+"_COMPARE_FORM_TO_"+form_label, label+"_FALSE") -def criterion_written_as_node(criterion: ExprNode, parameters_dict: EvaluationParameters, label: str | None = None) -> CriteriaGraph: +def criterion_written_as_node(criterion: ExprNode, parameters: EvaluationParameters, label: str | None = None) -> CriteriaGraph: if label is None: label = criterion.content_string() @@ -74,8 +74,8 @@ def criterion_written_as_node(criterion: ExprNode, parameters_dict: EvaluationPa return graph # TODO: Make a version that can compare the forms of any two reserved expressions - answer = parameters_dict.reserved_expressions_strings["task"]["answer"] - response = parameters_dict.reserved_expressions_strings["learner"]["response"] + answer = parameters.reserved_expressions_strings["task"]["answer"] + response = parameters.reserved_expressions_strings["learner"]["response"] graph.add_evaluation_node( str(rhs)+"_WRITTEN_AS_KNOWN_FORM", diff --git a/app/context/symbolic/numerical.py b/app/context/symbolic/numerical.py index c15207b..7adbd28 100644 --- a/app/context/symbolic/numerical.py +++ b/app/context/symbolic/numerical.py @@ -4,16 +4,16 @@ from app.evaluation_parameters import EvaluationParameters -def compare_numerically(lhs_expr: Basic, rhs_expr: Basic, parameters_dict: EvaluationParameters) -> bool: +def compare_numerically(lhs_expr: Basic, rhs_expr: Basic, parameters: EvaluationParameters) -> bool: """ Compares `lhs_expr` and `rhs_expr` numerically using the `atol` and `rtol` parameters. Returns False if numerical comparison is not enabled (`numerical` is not set and neither `atol` nor `rtol` is positive), otherwise whether the difference is within both tolerances. """ - if not (parameters_dict.numerical - or float(parameters_dict.rtol) > 0 - or float(parameters_dict.atol) > 0): + if not (parameters.numerical + or float(parameters.rtol) > 0 + or float(parameters.atol) > 0): return False # REMARK: 'pi' should be a reserved symbol but it is sometimes not treated as one, possibly because of input symbols. @@ -29,18 +29,18 @@ def replace_pi(expr: Basic) -> Basic: # Separates LHS and RHS, parses and evaluates them res = N(replace_pi(lhs_expr)) ans = N(replace_pi(rhs_expr)) - if float(parameters_dict.atol) > 0: + if float(parameters.atol) > 0: try: absolute_error = abs(float(ans-res)) - error_below_atol = bool(absolute_error < float(parameters_dict.atol)) + error_below_atol = bool(absolute_error < float(parameters.atol)) except TypeError: error_below_atol = None else: error_below_atol = True - if float(parameters_dict.rtol) > 0: + if float(parameters.rtol) > 0: try: relative_error = abs(float((ans-res)/ans)) - error_below_rtol = bool(relative_error < float(parameters_dict.rtol)) + error_below_rtol = bool(relative_error < float(parameters.rtol)) except TypeError: error_below_rtol = None else: