From fa6c0dbf28aebd7daf33cadc3c6edb7266c29746 Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Thu, 30 Oct 2025 09:26:56 +0000 Subject: [PATCH 01/11] Added tests as module --- app/tests/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/tests/__init__.py diff --git a/app/tests/__init__.py b/app/tests/__init__.py new file mode 100644 index 0000000..e69de29 From b10b83b7a7eac5a8ef25612b40c445382b3e79b4 Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Thu, 30 Oct 2025 11:07:08 +0000 Subject: [PATCH 02/11] Added unicode support to symbolic expressions --- app/tests/symbolic_evaluation_tests.py | 83 ++++++++++++++++++++++++++ app/utility/expression_utilities.py | 24 +++++--- 2 files changed, 98 insertions(+), 9 deletions(-) diff --git a/app/tests/symbolic_evaluation_tests.py b/app/tests/symbolic_evaluation_tests.py index 33fcbce..e211774 100644 --- a/app/tests/symbolic_evaluation_tests.py +++ b/app/tests/symbolic_evaluation_tests.py @@ -2009,5 +2009,88 @@ def test_abstract_integral(self): result = evaluation_function(response, answer, params) assert result["is_correct"] is True + @pytest.mark.parametrize("unicode_char,letter_name", [ + ("Α", "Alpha"), ("α", "alpha"), ("Β", "Beta"), ("β", "beta"), + ("Γ", "Gamma"), ("γ", "gamma"), ("Δ", "Delta"), ("δ", "delta"), + ("Ε", "Epsilon"), ("ε", "epsilon"), ("Ζ", "Zeta"), ("ζ", "zeta"), + ("Η", "Eta"), ("η", "eta"), ("Θ", "Theta"), ("θ", "theta"), + ("Ι", "Iota"), ("ι", "iota"), ("Κ", "Kappa"), ("κ", "kappa"), + ("Λ", "Lambda"), + ("Μ", "Mu"), ("μ", "mu"), ("Ν", "Nu"), ("ν", "nu"), + ("Ξ", "Xi"), ("ξ", "xi"), ("Ο", "Omicron"), ("ο", "omicron"), + ("Π", "Pi"), ("π", "pi"), ("Ρ", "Rho"), ("ρ", "rho"), + ("Σ", "Sigma"), ("σ", "sigma"), ("Τ", "Tau"), ("τ", "tau"), + ("Υ", "Upsilon"), ("υ", "upsilon"), ("Φ", "Phi"), ("φ", "phi"), + ("Χ", "Chi"), ("χ", "chi"), ("Ψ", "Psi"), ("ψ", "psi"), + ("Ω", "Omega"), ("ω", "omega") + ]) + def test_greek_unicode_letters(self, unicode_char, letter_name): + response = unicode_char + answer = letter_name + params = { + "strict_syntax": False, + "elementary_functions": True, + } + result = evaluation_function(response, answer, params) + assert result["is_correct"] is True + + @pytest.mark.parametrize("unicode_expr,letter_expr", [ + # Basic expressions with common variables + ("α + β", "alpha + beta"), + ("2μ + 3", "2*mu + 3"), + ("π*r^2", "pi*r^2"), + ("θ/2", "theta/2"), + ("σ^2", "sigma^2"), + + # Chi vs X confusion tests (CRITICAL) + ("χ + x", "chi + x"), + ("Χ + X", "Chi + X"), + ("χ*x", "chi*x"), + ("x^2 + χ", "x^2 + chi"), + ("χ^2 + x^2", "chi^2 + x^2"), + + # Xi vs X confusion tests + ("ξ + x", "xi + x"), + ("Ξ*X", "Xi*X"), + + # Rho vs P confusion tests + ("ρ + p", "rho + p"), + ("Ρ*P", "Rho*P"), + + # Nu vs V confusion tests + ("ν + v", "nu + v"), + ("Ν*V", "Nu*V"), + + # Omicron vs O confusion tests + ("ο + o", "omicron + o"), + ("Ο*O", "Omicron*O"), + + # Multiple Greek letters with Latin variables + ("α*x + β*y", "alpha*x + beta*y"), + ("μ*σ^2 + ν", "mu*sigma^2 + nu"), + ("γ*t + δ*s", "gamma*t + delta*s"), + ("Λ*x + μ*y + ν*z", "Lambda*x + mu*y + nu*z"), + + # Complex expressions + ("sin(θ) + cos(φ)", "sin(theta) + cos(phi)"), + ("e^(iπ)", "e^(i*pi)"), + + # Edge cases with similar-looking letters + ("ω*t + φ", "omega*t + phi"), + ("Ψ(x) + ψ(y)", "Psi(x) + psi(y)"), + ("Δx/Δt", "Delta*x/Delta*t"), + ("ε_0*μ_0", "epsilon_0*mu_0"), + ]) + def test_greek_letters_in_expressions(self, unicode_expr, letter_expr): + response = unicode_expr + answer = letter_expr + params = { + "strict_syntax": False, + "elementary_functions": True, + } + result = evaluation_function(response, answer, params) + assert result["is_correct"] is True + + if __name__ == "__main__": pytest.main(['-xk not slow', "--tb=line", '--durations=10', os.path.abspath(__file__)]) diff --git a/app/utility/expression_utilities.py b/app/utility/expression_utilities.py index 4e1208e..bc0d75b 100644 --- a/app/utility/expression_utilities.py +++ b/app/utility/expression_utilities.py @@ -74,14 +74,16 @@ def _print_log(self, expr, exp=None): upper_case_alternatives.append(alternative.upper()) data[1].extend(upper_case_alternatives) -greek_letters = [ - "Alpha", "alpha", "Beta", "beta", "Gamma", "gamma", "Delta", "delta", "Epsilon", "epsilon", "Zeta", "zeta", - "Eta", "eta", "Theta", "theta", "Iota", "iota", "Kappa", "kappa", "Lambda", # "lambda" removed to avoid collision with reserved keyword in python - "Mu", "mu", "Nu", "nu", - "Xi", "xi", "Omicron", "omicron", "Pi", "pi", "Rho", "rho", "Sigma", "sigma", "Tau", "tau", "Upsilon", "upsilon", - "Phi", "phi", "Chi", "chi", "Psi", "psi", "Omega", "omega" +special_symbols_names = [ + ("Alpha", ["Α"]), ("alpha", ["α"]), ("Beta", ["Β"]), ("beta", ["β"]), ("Gamma", ["Γ"]), ("gamma", ["γ"]), ("Delta", ["Δ"]), ("delta", ["δ"]), + ("Epsilon", ["Ε"]), ("epsilon", ["ε"]), ("Zeta", ["Ζ"]), ("zeta", ["ζ"]), ("Eta", ["Η"]), ("eta", ["η"]), ("Theta", ["Θ"]), ("theta", ["θ"]), + ("Iota", ["Ι"]), ("iota", ["ι"]), ("Kappa", ["Κ"]), ("kappa", ["κ"]), ("Lambda", ["Λ"]), # "lambda" removed to avoid collision with reserved keyword in python + ("Mu", ["Μ"]), ("mu", ["μ"]), ("Nu", ["Ν"]), ("nu", ["ν"]), ("Xi", ["Ξ"]), ("xi", ["ξ"]), ("Omicron", ["Ο"]), ("omicron", ["ο"]), ("Pi", ["Π"]), + ("pi", ["π"]), ("Rho", ["Ρ"]), ("rho", ["ρ"]), ("Sigma", ["Σ"]), ("sigma", ["σ"]), ("Tau", ["Τ"]), ("tau", ["τ"]), ("Upsilon", ["Υ"]), + ("upsilon", ["υ"]), ("Phi", ["Φ"]), ("phi", ["φ"]), ("Chi", ["Χ"]), ("chi", ["χ"]), ("Psi", ["Ψ"]), ("psi", ["ψ"]), ("Omega", ["Ω"]), + ("omega", ["ω"]) ] -special_symbols_names = [(x, []) for x in greek_letters] + # -------- String Manipulation Utilities @@ -578,11 +580,13 @@ def create_sympy_parsing_params(params, unsplittable_symbols=tuple(), symbol_ass unsplittable_symbols.append(symbol) if params.get("specialFunctions", False) is True: - from sympy import beta, gamma, zeta + from sympy import beta, gamma, zeta, Lambda, Chi else: beta = Symbol("beta") gamma = Symbol("gamma") zeta = Symbol("zeta") + Lambda = Symbol("Lambda") + Chi = Symbol("Chi") if params["complexNumbers"] is True: from sympy import I else: @@ -599,12 +603,14 @@ def create_sympy_parsing_params(params, unsplittable_symbols=tuple(), symbol_ass "beta": beta, "gamma": gamma, "zeta": zeta, + "Lambda": Lambda, + "Chi": Chi, "I": I, "N": N, "O": O, "Q": Q, "S": S, - "E": E + "E": E, } symbol_dict.update(sympy_symbols(unsplittable_symbols)) From 6c6bae7d2d5a75911b6996dc912e8a57d92e91eb Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Thu, 30 Oct 2025 13:02:39 +0000 Subject: [PATCH 03/11] Added lowercase lambda --- app/tests/symbolic_evaluation_tests.py | 3 ++- app/utility/expression_utilities.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/tests/symbolic_evaluation_tests.py b/app/tests/symbolic_evaluation_tests.py index e211774..d63f789 100644 --- a/app/tests/symbolic_evaluation_tests.py +++ b/app/tests/symbolic_evaluation_tests.py @@ -2015,7 +2015,7 @@ def test_abstract_integral(self): ("Ε", "Epsilon"), ("ε", "epsilon"), ("Ζ", "Zeta"), ("ζ", "zeta"), ("Η", "Eta"), ("η", "eta"), ("Θ", "Theta"), ("θ", "theta"), ("Ι", "Iota"), ("ι", "iota"), ("Κ", "Kappa"), ("κ", "kappa"), - ("Λ", "Lambda"), + ("Λ", "Lambda"), ("λ", "lambda"), ("Μ", "Mu"), ("μ", "mu"), ("Ν", "Nu"), ("ν", "nu"), ("Ξ", "Xi"), ("ξ", "xi"), ("Ο", "Omicron"), ("ο", "omicron"), ("Π", "Pi"), ("π", "pi"), ("Ρ", "Rho"), ("ρ", "rho"), @@ -2070,6 +2070,7 @@ def test_greek_unicode_letters(self, unicode_char, letter_name): ("μ*σ^2 + ν", "mu*sigma^2 + nu"), ("γ*t + δ*s", "gamma*t + delta*s"), ("Λ*x + μ*y + ν*z", "Lambda*x + mu*y + nu*z"), + ("λ*x + μ*y + ν*z", "lambda*x + mu*y + nu*z"), # Complex expressions ("sin(θ) + cos(φ)", "sin(theta) + cos(phi)"), diff --git a/app/utility/expression_utilities.py b/app/utility/expression_utilities.py index bc0d75b..105e708 100644 --- a/app/utility/expression_utilities.py +++ b/app/utility/expression_utilities.py @@ -77,7 +77,7 @@ def _print_log(self, expr, exp=None): special_symbols_names = [ ("Alpha", ["Α"]), ("alpha", ["α"]), ("Beta", ["Β"]), ("beta", ["β"]), ("Gamma", ["Γ"]), ("gamma", ["γ"]), ("Delta", ["Δ"]), ("delta", ["δ"]), ("Epsilon", ["Ε"]), ("epsilon", ["ε"]), ("Zeta", ["Ζ"]), ("zeta", ["ζ"]), ("Eta", ["Η"]), ("eta", ["η"]), ("Theta", ["Θ"]), ("theta", ["θ"]), - ("Iota", ["Ι"]), ("iota", ["ι"]), ("Kappa", ["Κ"]), ("kappa", ["κ"]), ("Lambda", ["Λ"]), # "lambda" removed to avoid collision with reserved keyword in python + ("Iota", ["Ι"]), ("iota", ["ι"]), ("Kappa", ["Κ"]), ("kappa", ["κ"]), ("Lambda", ["Λ"]), ("lambda", ["λ"]), ("Mu", ["Μ"]), ("mu", ["μ"]), ("Nu", ["Ν"]), ("nu", ["ν"]), ("Xi", ["Ξ"]), ("xi", ["ξ"]), ("Omicron", ["Ο"]), ("omicron", ["ο"]), ("Pi", ["Π"]), ("pi", ["π"]), ("Rho", ["Ρ"]), ("rho", ["ρ"]), ("Sigma", ["Σ"]), ("sigma", ["σ"]), ("Tau", ["Τ"]), ("tau", ["τ"]), ("Upsilon", ["Υ"]), ("upsilon", ["υ"]), ("Phi", ["Φ"]), ("phi", ["φ"]), ("Chi", ["Χ"]), ("chi", ["χ"]), ("Psi", ["Ψ"]), ("psi", ["ψ"]), ("Omega", ["Ω"]), From d33108b437129d9fd3adc4898d22c698ae12c233 Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Thu, 30 Oct 2025 14:03:17 +0000 Subject: [PATCH 04/11] Switched unicode parsing to be separate to elementary functions --- app/tests/symbolic_evaluation_tests.py | 4 ++-- app/utility/expression_utilities.py | 20 ++++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/tests/symbolic_evaluation_tests.py b/app/tests/symbolic_evaluation_tests.py index d63f789..959776d 100644 --- a/app/tests/symbolic_evaluation_tests.py +++ b/app/tests/symbolic_evaluation_tests.py @@ -2029,7 +2029,7 @@ def test_greek_unicode_letters(self, unicode_char, letter_name): answer = letter_name params = { "strict_syntax": False, - "elementary_functions": True, + "elementary_functions": False, } result = evaluation_function(response, answer, params) assert result["is_correct"] is True @@ -2087,7 +2087,7 @@ def test_greek_letters_in_expressions(self, unicode_expr, letter_expr): answer = letter_expr params = { "strict_syntax": False, - "elementary_functions": True, + "elementary_functions": False, } result = evaluation_function(response, answer, params) assert result["is_correct"] is True diff --git a/app/utility/expression_utilities.py b/app/utility/expression_utilities.py index 105e708..873fc19 100644 --- a/app/utility/expression_utilities.py +++ b/app/utility/expression_utilities.py @@ -77,7 +77,7 @@ def _print_log(self, expr, exp=None): special_symbols_names = [ ("Alpha", ["Α"]), ("alpha", ["α"]), ("Beta", ["Β"]), ("beta", ["β"]), ("Gamma", ["Γ"]), ("gamma", ["γ"]), ("Delta", ["Δ"]), ("delta", ["δ"]), ("Epsilon", ["Ε"]), ("epsilon", ["ε"]), ("Zeta", ["Ζ"]), ("zeta", ["ζ"]), ("Eta", ["Η"]), ("eta", ["η"]), ("Theta", ["Θ"]), ("theta", ["θ"]), - ("Iota", ["Ι"]), ("iota", ["ι"]), ("Kappa", ["Κ"]), ("kappa", ["κ"]), ("Lambda", ["Λ"]), ("lambda", ["λ"]), + ("Iota", ["Ι"]), ("iota", ["ι"]), ("Kappa", ["Κ"]), ("kappa", ["κ"]), ("Lambda", ["Λ"]), ("lamda", ["λ"]), # lambda mispelt here to minimise conflict with Python in-built ("Mu", ["Μ"]), ("mu", ["μ"]), ("Nu", ["Ν"]), ("nu", ["ν"]), ("Xi", ["Ξ"]), ("xi", ["ξ"]), ("Omicron", ["Ο"]), ("omicron", ["ο"]), ("Pi", ["Π"]), ("pi", ["π"]), ("Rho", ["Ρ"]), ("rho", ["ρ"]), ("Sigma", ["Σ"]), ("sigma", ["σ"]), ("Tau", ["Τ"]), ("tau", ["τ"]), ("Upsilon", ["Υ"]), ("upsilon", ["υ"]), ("Phi", ["Φ"]), ("phi", ["φ"]), ("Chi", ["Χ"]), ("chi", ["χ"]), ("Psi", ["Ψ"]), ("psi", ["ψ"]), ("Omega", ["Ω"]), @@ -238,10 +238,19 @@ def preprocess_according_to_chosen_convention(expression, parameters): expression = parser.parse(parser.scan(expression))[0].content_string() return expression +def transform_unicode_greek_symbols(expr): + alias_substitutions = [] + for (name, alias_list) in special_symbols_names: + if name in expr: + alias_substitutions += [(name, " "+name+" ")] + for alias in alias_list: + if alias in expr: + alias_substitutions += [(alias, " "+name+" ")] + return alias_substitutions def protect_elementary_functions_substitutions(expr): alias_substitutions = [] - for (name, alias_list) in elementary_functions_names+special_symbols_names: + for (name, alias_list) in elementary_functions_names: if name in expr: alias_substitutions += [(name, " "+name+" ")] for alias in alias_list: @@ -690,18 +699,25 @@ def parse_expression(expr_string, parsing_params): separate_unsplittable_symbols = [(x, " "+x) for x in unsplittable_symbols] substitutions = separate_unsplittable_symbols + + parsed_expr_set = set() for expr in expr_set: expr = preprocess_according_to_chosen_convention(expr, parsing_params) + substitutions = list(set(substitutions)) + substitutions += transform_unicode_greek_symbols(expr) substitutions.sort(key=substitutions_sort_key) + if parsing_params["elementary_functions"] is True: substitutions += protect_elementary_functions_substitutions(expr) + substitutions = list(set(substitutions)) substitutions.sort(key=substitutions_sort_key) expr = substitute(expr, substitutions) expr = " ".join(expr.split()) can_split = lambda x: False if x in unsplittable_symbols else _token_splittable(x) + if strict_syntax is True: transformations = parser_transformations[0:4]+extra_transformations else: From d48fd4ea0466925912f67b55cb1215e29eb01886 Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Thu, 30 Oct 2025 14:37:59 +0000 Subject: [PATCH 05/11] Implemented unicode parsing for physical quantities --- app/context/physical_quantity.py | 10 +++++++++- .../physical_quantity_evaluation_tests.py | 18 ++++++++++++++++++ app/utility/unit_system_conversions.py | 4 ++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/app/context/physical_quantity.py b/app/context/physical_quantity.py index 6639e18..df9e881 100644 --- a/app/context/physical_quantity.py +++ b/app/context/physical_quantity.py @@ -10,7 +10,8 @@ ) from ..preview_implementations.physical_quantity_preview import preview_function from ..feedback.physical_quantity import feedback_string_generators as physical_quantity_feedback_string_generators -from ..utility.expression_utilities import default_parameters as symbolic_default_parameters +from ..utility.expression_utilities import default_parameters as symbolic_default_parameters, \ + transform_unicode_greek_symbols, substitute from ..utility.expression_utilities import ( substitute_input_symbols, create_sympy_parsing_params, @@ -35,6 +36,7 @@ def parse_quantity(name, expr, parameters, evaluation_result): parser = SLR_quantity_parser(parameters) quantity = SLR_quantity_parsing(expr, parameters, parser, name) + for message in quantity.messages: evaluation_result.add_feedback(message) if quantity.standard_value is not None: @@ -476,6 +478,10 @@ def feedback_procedure_generator(parameters_dict): graphs.update({label: graph}) return graphs +def preprocess_unicode(expr): + unicode_transforms = transform_unicode_greek_symbols(expr) + expr = substitute(expr, unicode_transforms) + return expr def expression_preprocess(name, expr, parameters): if parameters.get("strictness", "natural") == "legacy": @@ -541,6 +547,8 @@ def expression_preprocess(name, expr, parameters): expr = expr[0:match_content.span()[0]]+match_content.group().replace("*", " ")+expr[match_content.span()[1]:] match_content = re.search(search_string, expr) + + expr = preprocess_unicode(expr) success = True return success, expr, None diff --git a/app/tests/physical_quantity_evaluation_tests.py b/app/tests/physical_quantity_evaluation_tests.py index a427ced..485f23c 100644 --- a/app/tests/physical_quantity_evaluation_tests.py +++ b/app/tests/physical_quantity_evaluation_tests.py @@ -374,5 +374,23 @@ def test_answer_zero_value(self): result = evaluation_function(res, ans, params, include_test_data=True) assert result["is_correct"] is False + @pytest.mark.parametrize( + "ans,res", + [ + ("10 ohm", "10 Ω"), + ("10 mu A", "10 μ A"), + ("30 degree", "30 θ"), + ] + ) + def test_greek_letter_units(self, ans, res): + params = { + 'strict_syntax': False, + 'physical_quantity': True, + 'elementary_functions': True, + } + result = evaluation_function(res, ans, params) + assert result["is_correct"] is True + + if __name__ == "__main__": pytest.main(['-xk not slow', "--no-header", os.path.abspath(__file__)]) diff --git a/app/utility/unit_system_conversions.py b/app/utility/unit_system_conversions.py index 7fc4926..03c4e56 100644 --- a/app/utility/unit_system_conversions.py +++ b/app/utility/unit_system_conversions.py @@ -60,7 +60,7 @@ ('coulomb', 'C', '(second*ampere)', ('Coulomb',), ('coulombs', 'Coulombs')), ('volt', 'V', '(metre**2*kilogram*second**(-3)*ampere**(-1))', ('Volt',), ('volts', 'Volts')), ('farad', 'F', '(metre**(-2)*(kilogram)**(-1)*second**4*ampere**2)', ('Farad',), ('farads', 'Farads')), - ('ohm', 'O', '(metre**2*kilogram*second**(-3)*ampere**(-2))', ('Ohm',), ('ohms', 'Ohms')), + ('ohm', 'O', '(metre**2*kilogram*second**(-3)*ampere**(-2))', ('Ohm', 'Omega'), ('ohms', 'Ohms')), ('siemens', 'S', '(metre**(-2)*kilogram**(-1)*second**3*ampere**2)', ('Siemens',), tuple()), ('weber', 'Wb', '(metre**2*kilogram*second**(-2)*ampere**(-1))', ('Weber',), ('webers', 'Webers')), ('tesla', 'T', '(kilogram*second**(-2)*ampere**(-1))', ('Tesla',), ('teslas', 'Teslas')), @@ -83,7 +83,7 @@ ('steradian', 'sr', '(1/(4*pi))', tuple(), ('steradians',)), ('minute', 'min', '(60*second)', tuple(), ('minutes',)), ('hour', 'h', '(3600*second)', tuple(), ('hours',)), - ('degree', 'deg', '(1/360)', tuple(), ('degrees',)), + ('degree', 'deg', '(1/360)', ('theta', ), ('degrees',)), ('litre', 'L', '(10**(-3)*metre**3)', ('liter',), ('litres,liters',)), ('metricton', 't', '(10**3*kilogram)', ('tonne',), ('tonnes',)), ('neper', 'Np', '(1)', ('Neper',), ('nepers', 'Nepers')), From e50f5f9a7e22ad5d55e8e4327f1be8738fade8dd Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Thu, 30 Oct 2025 14:51:42 +0000 Subject: [PATCH 06/11] Revert implementation --- app/context/physical_quantity.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/app/context/physical_quantity.py b/app/context/physical_quantity.py index df9e881..6639e18 100644 --- a/app/context/physical_quantity.py +++ b/app/context/physical_quantity.py @@ -10,8 +10,7 @@ ) from ..preview_implementations.physical_quantity_preview import preview_function from ..feedback.physical_quantity import feedback_string_generators as physical_quantity_feedback_string_generators -from ..utility.expression_utilities import default_parameters as symbolic_default_parameters, \ - transform_unicode_greek_symbols, substitute +from ..utility.expression_utilities import default_parameters as symbolic_default_parameters from ..utility.expression_utilities import ( substitute_input_symbols, create_sympy_parsing_params, @@ -36,7 +35,6 @@ def parse_quantity(name, expr, parameters, evaluation_result): parser = SLR_quantity_parser(parameters) quantity = SLR_quantity_parsing(expr, parameters, parser, name) - for message in quantity.messages: evaluation_result.add_feedback(message) if quantity.standard_value is not None: @@ -478,10 +476,6 @@ def feedback_procedure_generator(parameters_dict): graphs.update({label: graph}) return graphs -def preprocess_unicode(expr): - unicode_transforms = transform_unicode_greek_symbols(expr) - expr = substitute(expr, unicode_transforms) - return expr def expression_preprocess(name, expr, parameters): if parameters.get("strictness", "natural") == "legacy": @@ -547,8 +541,6 @@ def expression_preprocess(name, expr, parameters): expr = expr[0:match_content.span()[0]]+match_content.group().replace("*", " ")+expr[match_content.span()[1]:] match_content = re.search(search_string, expr) - - expr = preprocess_unicode(expr) success = True return success, expr, None From 1c0f835d40f396c643beed515fdf9af6506a5e76 Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Thu, 30 Oct 2025 15:37:17 +0000 Subject: [PATCH 07/11] Implemented unicode parsing for physical quantities --- .../physical_quantity_evaluation_tests.py | 3 +- app/utility/unit_system_conversions.py | 6 ++-- custom_comparison_with_criteria_order_1.md | 28 +++++++++---------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/app/tests/physical_quantity_evaluation_tests.py b/app/tests/physical_quantity_evaluation_tests.py index 485f23c..3eaf76d 100644 --- a/app/tests/physical_quantity_evaluation_tests.py +++ b/app/tests/physical_quantity_evaluation_tests.py @@ -378,7 +378,8 @@ def test_answer_zero_value(self): "ans,res", [ ("10 ohm", "10 Ω"), - ("10 mu A", "10 μ A"), + ("10 micro A", "10 μA"), + ("10 micro A", "10 μ A"), ("30 degree", "30 θ"), ] ) diff --git a/app/utility/unit_system_conversions.py b/app/utility/unit_system_conversions.py index 03c4e56..0713db3 100644 --- a/app/utility/unit_system_conversions.py +++ b/app/utility/unit_system_conversions.py @@ -21,7 +21,7 @@ ('deci', 'd', '(10**(-1))', tuple()), ('centi', 'c', '(10**(-2))', tuple()), ('milli', 'm', '(10**(-3))', tuple()), - ('micro', 'mu', '(10**(-6))', tuple()), + ('micro', 'mu', '(10**(-6))', ("μ", )), ('nano', 'n', '(10**(-9))', tuple()), ('pico', 'p', '(10**(-12))', tuple()), ('femto', 'f', '(10**(-15))', tuple()), @@ -60,7 +60,7 @@ ('coulomb', 'C', '(second*ampere)', ('Coulomb',), ('coulombs', 'Coulombs')), ('volt', 'V', '(metre**2*kilogram*second**(-3)*ampere**(-1))', ('Volt',), ('volts', 'Volts')), ('farad', 'F', '(metre**(-2)*(kilogram)**(-1)*second**4*ampere**2)', ('Farad',), ('farads', 'Farads')), - ('ohm', 'O', '(metre**2*kilogram*second**(-3)*ampere**(-2))', ('Ohm', 'Omega'), ('ohms', 'Ohms')), + ('ohm', 'O', '(metre**2*kilogram*second**(-3)*ampere**(-2))', ('Ohm', 'Ω'), ('ohms', 'Ohms')), ('siemens', 'S', '(metre**(-2)*kilogram**(-1)*second**3*ampere**2)', ('Siemens',), tuple()), ('weber', 'Wb', '(metre**2*kilogram*second**(-2)*ampere**(-1))', ('Weber',), ('webers', 'Webers')), ('tesla', 'T', '(kilogram*second**(-2)*ampere**(-1))', ('Tesla',), ('teslas', 'Teslas')), @@ -83,7 +83,7 @@ ('steradian', 'sr', '(1/(4*pi))', tuple(), ('steradians',)), ('minute', 'min', '(60*second)', tuple(), ('minutes',)), ('hour', 'h', '(3600*second)', tuple(), ('hours',)), - ('degree', 'deg', '(1/360)', ('theta', ), ('degrees',)), + ('degree', 'deg', '(1/360)', ('θ', ), ('degrees',)), ('litre', 'L', '(10**(-3)*metre**3)', ('liter',), ('litres,liters',)), ('metricton', 't', '(10**3*kilogram)', ('tonne',), ('tonnes',)), ('neper', 'Np', '(1)', ('Neper',), ('nepers', 'Nepers')), diff --git a/custom_comparison_with_criteria_order_1.md b/custom_comparison_with_criteria_order_1.md index b55bd38..e369be3 100644 --- a/custom_comparison_with_criteria_order_1.md +++ b/custom_comparison_with_criteria_order_1.md @@ -1,26 +1,26 @@ ```mermaid -flowchart TD - N_0_0(["answer <= response
---
Checks if answer <= response is true."]) - N_1_0["answer <= response_TRUE
---
answer <= response is true."] - N_1_1["answer <= response_FALSE
---
answer <= response is false."] - N_1_2["answer <= response_UNKNOWN
---
answer <= response is false."] - N_2_0{{"END
---
Evaluation completed."}} - N_0_0 --> N_1_2 - N_1_0 --> N_2_0 - N_1_1 --> N_2_0 - N_0_0 --> N_1_0 - N_0_0 --> N_1_1 - N_1_2 --> N_2_0 flowchart TD N_0_0(["2+answer > response
---
Checks if 2+answer > response is true."]) N_1_0["2+answer > response_TRUE
---
2+answer > response is true."] N_1_1["2+answer > response_FALSE
---
2+answer > response is false."] N_1_2["2+answer > response_UNKNOWN
---
2+answer > response is false."] N_2_0{{"END
---
Evaluation completed."}} + N_1_2 --> N_2_0 + N_1_1 --> N_2_0 + N_0_0 --> N_1_1 N_0_0 --> N_1_2 + N_0_0 --> N_1_0 N_1_0 --> N_2_0 +flowchart TD + N_0_0(["answer <= response
---
Checks if answer <= response is true."]) + N_1_0["answer <= response_TRUE
---
answer <= response is true."] + N_1_1["answer <= response_FALSE
---
answer <= response is false."] + N_1_2["answer <= response_UNKNOWN
---
answer <= response is false."] + N_2_0{{"END
---
Evaluation completed."}} + N_1_2 --> N_2_0 N_1_1 --> N_2_0 - N_0_0 --> N_1_0 N_0_0 --> N_1_1 - N_1_2 --> N_2_0 + N_0_0 --> N_1_2 + N_0_0 --> N_1_0 + N_1_0 --> N_2_0 ``` From b0638b0a535ddf54c3d8b54dec4f7a0863c9dbbc Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Thu, 30 Oct 2025 15:44:55 +0000 Subject: [PATCH 08/11] Refactored preproccess --- app/context/physical_quantity.py | 153 +++++++++++++++++++------------ 1 file changed, 93 insertions(+), 60 deletions(-) diff --git a/app/context/physical_quantity.py b/app/context/physical_quantity.py index 6639e18..ecd4f51 100644 --- a/app/context/physical_quantity.py +++ b/app/context/physical_quantity.py @@ -476,73 +476,106 @@ def feedback_procedure_generator(parameters_dict): graphs.update({label: graph}) return graphs - -def expression_preprocess(name, expr, parameters): - if parameters.get("strictness", "natural") == "legacy": - prefix_data = {(p[0], p[1], tuple(), p[3]) for p in set_of_SI_prefixes} - prefixes = [] - for prefix in prefix_data: - prefixes = prefixes+[prefix[0]] + list(prefix[-1]) - prefix_short_forms = [prefix[1] for prefix in prefix_data] - unit_data = set_of_SI_base_unit_dimensions \ - | set_of_derived_SI_units_in_SI_base_units \ - | set_of_common_units_in_SI \ - | set_of_very_common_units_in_SI \ - | set_of_imperial_units - unit_long_forms = prefixes - for unit in unit_data: - unit_long_forms = unit_long_forms+[unit[0]] + list(unit[-2]) + list(unit[-1]) - unit_long_forms = "("+"|".join(unit_long_forms)+")" - # Rewrite any expression on the form "*UNIT" (but not "**UNIT") as " UNIT" - # Example: "newton*metre" ---> "newton metre" - search_string = r"(? "newton metre" + search_string = r"(? "kilometre" - search_string = prefixes+" "+unit_long_forms + prefixes = "(" + "|".join(prefixes) + ")" + # Rewrite any expression on the form "PREFIX UNIT" as "PREFIXUNIT" + # Example: "kilo metre" ---> "kilometre" + search_string = prefixes + " " + unit_long_forms + match_content = re.search(search_string, expr) + while match_content is not None: + expr = expr[0:match_content.span()[0]] + " " + "".join(match_content.group().split()) + expr[ + match_content.span()[ + 1]:] match_content = re.search(search_string, expr) - while match_content is not None: - expr = expr[0:match_content.span()[0]]+" "+"".join(match_content.group().split())+expr[match_content.span()[1]:] - match_content = re.search(search_string, expr) - unit_short_forms = [u[1] for u in unit_data] - short_forms = "("+"|".join(list(set(prefix_short_forms+unit_short_forms)))+")" - # Add space before short forms of prefixes or unit names if they are preceded by numbers or multiplication - # Example: "100Pa" ---> "100 Pa" - search_string = r"[0-9\*\(\)]"+short_forms + unit_short_forms = [u[1] for u in unit_data] + short_forms = "(" + "|".join(list(set(prefix_short_forms + unit_short_forms))) + ")" + # Add space before short forms of prefixes or unit names if they are preceded by numbers or multiplication + # Example: "100Pa" ---> "100 Pa" + search_string = r"[0-9\*\(\)]" + short_forms + match_content = re.search(search_string, expr) + while match_content is not None: + expr = expr[0:match_content.span()[0] + 1] + " " + expr[match_content.span()[0] + 1:] match_content = re.search(search_string, expr) - while match_content is not None: - expr = expr[0:match_content.span()[0]+1]+" "+expr[match_content.span()[0]+1:] - match_content = re.search(search_string, expr) - # Remove space after prefix short forms if they are preceded by numbers, multiplication or space - # Example: "100 m Pa" ---> "100 mPa" - prefix_short_forms = "("+"|".join(prefix_short_forms)+")" - search_string = r"[0-9\*\(\) ]"+prefix_short_forms+" " + # Remove space after prefix short forms if they are preceded by numbers, multiplication or space + # Example: "100 m Pa" ---> "100 mPa" + prefix_short_forms = "(" + "|".join(prefix_short_forms) + ")" + search_string = r"[0-9\*\(\) ]" + prefix_short_forms + " " + match_content = re.search(search_string, expr) + while match_content is not None: + expr = expr[0:match_content.span()[0] + 1] + match_content.group()[0:-1] + expr[match_content.span()[1]:] match_content = re.search(search_string, expr) - while match_content is not None: - expr = expr[0:match_content.span()[0]+1]+match_content.group()[0:-1]+expr[match_content.span()[1]:] - match_content = re.search(search_string, expr) - # Remove multiplication and space after prefix short forms if they are preceded by numbers, multiplication or space - # Example: "100 m* Pa" ---> "100 mPa" - search_string = r"[0-9\*\(\) ]"+prefix_short_forms+"\* " + # Remove multiplication and space after prefix short forms if they are preceded by numbers, multiplication or space + # Example: "100 m* Pa" ---> "100 mPa" + search_string = r"[0-9\*\(\) ]" + prefix_short_forms + "\* " + match_content = re.search(search_string, expr) + while match_content is not None: + expr = expr[0:match_content.span()[0] + 1] + match_content.group()[0:-2] + expr[match_content.span()[1]:] match_content = re.search(search_string, expr) - while match_content is not None: - expr = expr[0:match_content.span()[0]+1]+match_content.group()[0:-2]+expr[match_content.span()[1]:] - match_content = re.search(search_string, expr) - # Replace multiplication followed by space before unit short forms with only spaces if they are preceded by numbers or space - # Example: "100* Pa" ---> "100 Pa" - unit_short_forms = "("+"|".join(unit_short_forms)+")" - search_string = r"[0-9\(\) ]\* "+unit_short_forms + # Replace multiplication followed by space before unit short forms with only spaces if they are preceded by numbers or space + # Example: "100* Pa" ---> "100 Pa" + unit_short_forms = "(" + "|".join(unit_short_forms) + ")" + search_string = r"[0-9\(\) ]\* " + unit_short_forms + match_content = re.search(search_string, expr) + while match_content is not None: + expr = expr[0:match_content.span()[0]] + match_content.group().replace("*", " ") + expr[ + match_content.span()[1]:] match_content = re.search(search_string, expr) - while match_content is not None: - expr = expr[0:match_content.span()[0]]+match_content.group().replace("*", " ")+expr[match_content.span()[1]:] - match_content = re.search(search_string, expr) - success = True - return success, expr, None + return expr + +def transform_prefixes_to_standard(expr): + """ + Transform ONLY alternative prefix spellings to standard prefix names. + Ensure there's exactly one space after the prefix before the unit. + Works for both attached (e.g. 'km') and spaced (e.g. 'k m') forms. + """ + + for prefix_name, symbol, power, alternatives in set_of_SI_prefixes: + for alt in alternatives: + if not alt: + continue + + # Match the alternative prefix either attached to or followed by spaces before a unit + # Examples matched: "km", "k m", "microsecond", "micro second" + pattern = rf'(? Date: Thu, 30 Oct 2025 18:39:34 +0000 Subject: [PATCH 09/11] Switched from theta to degree symbol --- app/tests/physical_quantity_evaluation_tests.py | 2 +- app/utility/unit_system_conversions.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/tests/physical_quantity_evaluation_tests.py b/app/tests/physical_quantity_evaluation_tests.py index 3eaf76d..43ff30f 100644 --- a/app/tests/physical_quantity_evaluation_tests.py +++ b/app/tests/physical_quantity_evaluation_tests.py @@ -380,7 +380,7 @@ def test_answer_zero_value(self): ("10 ohm", "10 Ω"), ("10 micro A", "10 μA"), ("10 micro A", "10 μ A"), - ("30 degree", "30 θ"), + ("30 degree", "30 °"), ] ) def test_greek_letter_units(self, ans, res): diff --git a/app/utility/unit_system_conversions.py b/app/utility/unit_system_conversions.py index 0713db3..4ef9d4c 100644 --- a/app/utility/unit_system_conversions.py +++ b/app/utility/unit_system_conversions.py @@ -83,7 +83,7 @@ ('steradian', 'sr', '(1/(4*pi))', tuple(), ('steradians',)), ('minute', 'min', '(60*second)', tuple(), ('minutes',)), ('hour', 'h', '(3600*second)', tuple(), ('hours',)), - ('degree', 'deg', '(1/360)', ('θ', ), ('degrees',)), + ('degree', 'deg', '(1/360)', ('°', ), ('degrees',)), ('litre', 'L', '(10**(-3)*metre**3)', ('liter',), ('litres,liters',)), ('metricton', 't', '(10**3*kilogram)', ('tonne',), ('tonnes',)), ('neper', 'Np', '(1)', ('Neper',), ('nepers', 'Nepers')), From 6e294be7bf8613d31eb5496d7ba2266b7c524751 Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Thu, 30 Oct 2025 18:41:50 +0000 Subject: [PATCH 10/11] Added other unicode variants of greek letters --- app/utility/expression_utilities.py | 55 +++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/app/utility/expression_utilities.py b/app/utility/expression_utilities.py index 873fc19..7017ae1 100644 --- a/app/utility/expression_utilities.py +++ b/app/utility/expression_utilities.py @@ -75,13 +75,54 @@ def _print_log(self, expr, exp=None): data[1].extend(upper_case_alternatives) special_symbols_names = [ - ("Alpha", ["Α"]), ("alpha", ["α"]), ("Beta", ["Β"]), ("beta", ["β"]), ("Gamma", ["Γ"]), ("gamma", ["γ"]), ("Delta", ["Δ"]), ("delta", ["δ"]), - ("Epsilon", ["Ε"]), ("epsilon", ["ε"]), ("Zeta", ["Ζ"]), ("zeta", ["ζ"]), ("Eta", ["Η"]), ("eta", ["η"]), ("Theta", ["Θ"]), ("theta", ["θ"]), - ("Iota", ["Ι"]), ("iota", ["ι"]), ("Kappa", ["Κ"]), ("kappa", ["κ"]), ("Lambda", ["Λ"]), ("lamda", ["λ"]), # lambda mispelt here to minimise conflict with Python in-built - ("Mu", ["Μ"]), ("mu", ["μ"]), ("Nu", ["Ν"]), ("nu", ["ν"]), ("Xi", ["Ξ"]), ("xi", ["ξ"]), ("Omicron", ["Ο"]), ("omicron", ["ο"]), ("Pi", ["Π"]), - ("pi", ["π"]), ("Rho", ["Ρ"]), ("rho", ["ρ"]), ("Sigma", ["Σ"]), ("sigma", ["σ"]), ("Tau", ["Τ"]), ("tau", ["τ"]), ("Upsilon", ["Υ"]), - ("upsilon", ["υ"]), ("Phi", ["Φ"]), ("phi", ["φ"]), ("Chi", ["Χ"]), ("chi", ["χ"]), ("Psi", ["Ψ"]), ("psi", ["ψ"]), ("Omega", ["Ω"]), - ("omega", ["ω"]) + ("Alpha", ["Α", "𝚨", "𝛢", "𝜜", "𝝖", "𝞐"]), + ("alpha", ["α", "𝛂", "𝛼", "𝜶", "𝝰", "𝞪"]), + ("Beta", ["Β", "𝚩", "𝛣", "𝜝", "𝝗", "𝞑"]), + ("beta", ["β", "ϐ", "𝛃", "𝛽", "𝜷", "𝝱", "𝞫"]), + ("Gamma", ["Γ", "𝚪", "𝛤", "𝜞", "𝝘", "𝞒"]), + ("gamma", ["γ", "𝛄", "𝛾", "𝜸", "𝝲", "𝞬"]), + ("Delta", ["Δ", "𝚫", "𝛥", "𝜟", "𝝙", "𝞓"]), + ("delta", ["δ", "𝛅", "𝛿", "𝜹", "𝝳", "𝞭"]), + ("Epsilon", ["Ε", "𝚬", "𝛦", "𝜠", "𝝚", "𝞔"]), + ("epsilon", ["ε", "ϵ", "𝛆", "𝜀", "𝜺", "𝝴", "𝞊", "𝞮"]), + ("Zeta", ["Ζ", "𝚭", "𝛧", "𝜡", "𝝛", "𝞕"]), + ("zeta", ["ζ", "𝛇", "𝜁", "𝜻", "𝝵", "𝞯"]), + ("Eta", ["Η", "𝚮", "𝛨", "𝜢", "𝝜", "𝞖"]), + ("eta", ["η", "𝛈", "𝜂", "𝜼", "𝝶", "𝞰"]), + ("Theta", ["Θ", "ϴ", "𝚯", "𝛩", "𝜣", "𝝝", "𝞗"]), + ("theta", ["θ", "ϑ", "𝛉", "𝜃", "𝜗", "𝜽", "𝝑", "𝝷", "𝞋", "𝞱"]), + ("Iota", ["Ι", "𝚰", "𝛪", "𝜤", "𝝞", "𝞘"]), + ("iota", ["ι", "𝛊", "𝜄", "𝜾", "𝝸", "𝞲"]), + ("Kappa", ["Κ", "𝚱", "𝛫", "𝜥", "𝝟", "𝞙"]), + ("kappa", ["κ", "ϰ", "𝛋", "𝜅", "𝜘", "𝜿", "𝝒", "𝝹", "𝞌", "𝞳"]), + ("Lambda", ["Λ", "𝚲", "𝛬", "𝜦", "𝝠", "𝞚"]), + ("lamda", ["λ", "𝛌", "𝜆", "𝝀", "𝝺", "𝞴"]), # lambda mispelt here to minimise conflict with Python in-built + ("Mu", ["Μ", "𝚳", "𝛭", "𝜧", "𝝡", "𝞛"]), + ("mu", ["μ", "µ", "𝛍", "𝜇", "𝝁", "𝝻", "𝞵"]), + ("Nu", ["Ν", "𝚴", "𝛮", "𝜨", "𝝢", "𝞜"]), + ("nu", ["ν", "𝛎", "𝜈", "𝝂", "𝝼", "𝞶"]), + ("Xi", ["Ξ", "𝚵", "𝛯", "𝜩", "𝝣", "𝞝"]), + ("xi", ["ξ", "𝛏", "𝜉", "𝝃", "𝝽", "𝞷"]), + ("Omicron", ["Ο", "𝚶", "𝛰", "𝜪", "𝝤", "𝞞"]), + ("omicron", ["ο", "𝛐", "𝜊", "𝝄", "𝝾", "𝞸"]), + ("Pi", ["Π", "𝚷", "𝛱", "𝜫", "𝝥", "𝞟"]), + ("pi", ["π", "ϖ", "𝛑", "𝜋", "𝝅", "𝝿", "𝞹"]), + ("Rho", ["Ρ", "𝚸", "𝛲", "𝜬", "𝝦", "𝞠"]), + ("rho", ["ρ", "ϱ", "𝛒", "𝜌", "𝝆", "𝞀", "𝞺"]), + ("Sigma", ["Σ", "𝚺", "𝛴", "𝜮", "𝝨", "𝞢"]), + ("sigma", ["σ", "ς", "𝛔", "𝜎", "𝝈", "𝞂", "𝞼"]), + ("Tau", ["Τ", "𝚻", "𝛵", "𝜯", "𝝩", "𝞣"]), + ("tau", ["τ", "𝛕", "𝜏", "𝝉", "𝞃", "𝞽"]), + ("Upsilon", ["Υ", "𝚼", "𝛶", "𝜰", "𝝪", "𝞤"]), + ("upsilon", ["υ", "𝛖", "𝜐", "𝝊", "𝞄", "𝞾"]), + ("Phi", ["Φ", "𝚽", "𝛷", "𝜱", "𝝫", "𝞥"]), + ("phi", ["φ", "ϕ", "𝛗", "𝜑", "𝜙", "𝝋", "𝝓", "𝞅", "𝞍", "𝞿", "𝟇"]), + ("Chi", ["Χ", "𝚾", "𝛸", "𝜲", "𝝬", "𝞦"]), + ("chi", ["χ", "𝛘", "𝜒", "𝝌", "𝞆", "𝟀"]), + ("Psi", ["Ψ", "𝚿", "𝛹", "𝜳", "𝝭", "𝞧"]), + ("psi", ["ψ", "𝛙", "𝜓", "𝝍", "𝞇", "𝟁"]), + ("Omega", ["Ω", "𝛀", "𝛺", "𝜴", "𝝮", "𝞨"]), + ("omega", ["ω", "𝛚", "𝜔", "𝝎", "𝞈", "𝟂"]) ] From 41d0baabd9954e057bddfd13dd4d0d900ea41499 Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Thu, 30 Oct 2025 18:43:34 +0000 Subject: [PATCH 11/11] Added variant unicode characters to physical quantities --- app/utility/unit_system_conversions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/utility/unit_system_conversions.py b/app/utility/unit_system_conversions.py index 4ef9d4c..884ddb5 100644 --- a/app/utility/unit_system_conversions.py +++ b/app/utility/unit_system_conversions.py @@ -21,7 +21,7 @@ ('deci', 'd', '(10**(-1))', tuple()), ('centi', 'c', '(10**(-2))', tuple()), ('milli', 'm', '(10**(-3))', tuple()), - ('micro', 'mu', '(10**(-6))', ("μ", )), + ('micro', 'mu', '(10**(-6))', ("μ", "µ", "𝛍", "𝜇", "𝝁", "𝝻", "𝞵")), ('nano', 'n', '(10**(-9))', tuple()), ('pico', 'p', '(10**(-12))', tuple()), ('femto', 'f', '(10**(-15))', tuple()), @@ -60,7 +60,7 @@ ('coulomb', 'C', '(second*ampere)', ('Coulomb',), ('coulombs', 'Coulombs')), ('volt', 'V', '(metre**2*kilogram*second**(-3)*ampere**(-1))', ('Volt',), ('volts', 'Volts')), ('farad', 'F', '(metre**(-2)*(kilogram)**(-1)*second**4*ampere**2)', ('Farad',), ('farads', 'Farads')), - ('ohm', 'O', '(metre**2*kilogram*second**(-3)*ampere**(-2))', ('Ohm', 'Ω'), ('ohms', 'Ohms')), + ('ohm', 'O', '(metre**2*kilogram*second**(-3)*ampere**(-2))', ('Ohm', "Ω", "𝛀", "𝛺", "𝜴", "𝝮", "𝞨"), ('ohms', 'Ohms')), ('siemens', 'S', '(metre**(-2)*kilogram**(-1)*second**3*ampere**2)', ('Siemens',), tuple()), ('weber', 'Wb', '(metre**2*kilogram*second**(-2)*ampere**(-1))', ('Weber',), ('webers', 'Webers')), ('tesla', 'T', '(kilogram*second**(-2)*ampere**(-1))', ('Tesla',), ('teslas', 'Teslas')),