Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 1 addition & 102 deletions app/context/physical_quantity.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from copy import deepcopy
from ..utility.physical_quantity_utilities import (
SLR_quantity_parser,
SLR_quantity_parsing
SLR_quantity_parsing, expression_preprocess
)
from ..preview_implementations.physical_quantity_preview import preview_function
from ..feedback.physical_quantity import feedback_string_generators as physical_quantity_feedback_string_generators
Expand Down Expand Up @@ -476,107 +476,6 @@ def feedback_procedure_generator(parameters_dict):
graphs.update({label: graph})
return graphs

def preprocess_legacy(expr, parameters):
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"(?<!\*)\* *" + unit_long_forms
match_content = re.search(search_string, expr[1:])
while match_content is not None:
expr = expr[0:match_content.span()[0] + 1] + match_content.group().replace("*", " ") + expr[
match_content.span()[
1] + 1:]
match_content = re.search(search_string, expr[1:])
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)
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)
# 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)
# 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)
# 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)

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'(?<!\w){re.escape(alt)}\s*(?=[A-Za-zµΩ])'
expr = re.sub(pattern, prefix_name + ' ', expr)

# Normalize spacing (no multiple spaces)
expr = re.sub(r'\s{2,}', ' ', expr).strip()

return expr

def expression_preprocess(name, expr, parameters):
if parameters.get("strictness", "natural") == "legacy":
expr = preprocess_legacy(expr, parameters)
return True, expr, None

expr = transform_prefixes_to_standard(expr)

return True, expr, None


def feedback_string_generator(tags, graph, parameters_dict):
strings = dict()
Expand Down
24 changes: 24 additions & 0 deletions app/evaluation_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,5 +169,29 @@ def test_multi_character_implicit_multi_variable(self):
assert result["is_correct"] is True, "Response: a/bcd"


def test_mu_preview_evaluate(self):
response = "10 μA"
params = Params(is_latex=False, elementary_functions=False, strict_syntax=False, physical_quantity=True)
result = preview_function(response, params)
assert "preview" in result.keys()

preview = result["preview"]
assert preview["latex"] == "10~\\mathrm{microampere}"
assert preview["sympy"] == "10 μA"

params = {
"atol": 0.0,
"rtol": 0.0,
"strict_syntax": False,
"physical_quantity": True,
"elementary_functions": False,
}

response = preview["sympy"]
answer = "10 muA"
result = evaluation_function(response, answer, params)
assert result["is_correct"] is True


if __name__ == "__main__":
pytest.main(['-xk not slow', '--tb=short', '--durations=10', os.path.abspath(__file__)])
5 changes: 3 additions & 2 deletions app/preview_implementations/physical_quantity_preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
)

from ..utility.expression_utilities import default_parameters as symbolic_default_parameters
from ..utility.physical_quantity_utilities import SLR_quantity_parser as quantity_parser
from ..utility.physical_quantity_utilities import SLR_quantity_parser as quantity_parser, expression_preprocess
from ..utility.physical_quantity_utilities import SLR_quantity_parsing as quantity_parsing

# CONSIDER: Move these to separate file so that they can be shared with
Expand Down Expand Up @@ -114,7 +114,8 @@ def preview_function(response: str, params: Params) -> Result:
unit_sympy = res_parsed.unit.content_string() if unit is not None else ""
sympy_out = value_sympy+separator_sympy+unit_sympy
else:
res_parsed = quantity_parsing(response, params, parser, "response")
_, res_pre_processed, _ = expression_preprocess("response", response, params)
res_parsed = quantity_parsing(res_pre_processed, params, parser, "response")
latex_out = res_parsed.latex_string
sympy_out = response

Expand Down
1 change: 0 additions & 1 deletion app/preview_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ def test_natural_logarithm_notation(self):
("e * x", True, False, "e * x", "E*x"),
("E", True, False, "E", "E",),
("ER_2", True, False, "ER_2", "E*R_2",),
# TODO: add exp (0), (1), (2) and (x)
("exp(1)", False, True, "e^{1}", "exp(1)"),
("e**1", False, True, "e^{1}", "E**1"),
("e^{1}", True, True, "e^{1}", "E"),
Expand Down
4 changes: 2 additions & 2 deletions app/tests/physical_quantity_evaluation_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,8 +378,8 @@ def test_answer_zero_value(self):
"ans,res",
[
("10 ohm", "10 Ω"),
("10 micro A", "10 μA"),
("10 micro A", "10 μ A"),
("10 microA", "10 μA"),
("10 microA", "10 μ A"),
("30 degree", "30 °"),
]
)
Expand Down
108 changes: 108 additions & 0 deletions app/utility/physical_quantity_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,13 @@ def SLR_generate_unit_dictionaries(units_string, strictness):
prefixes_long_to_short[prefix]+units_long_to_short[unit]: prefix+units[unit]
}
)
if prefix + units_long_to_short[unit] not in units_short_to_long.keys():
prefixed_units.update(
{
prefix + units_long_to_short[unit]: prefix + units[unit]
}
)


prefixed_units_end = {**units_end}
for unit in units_end.keys():
Expand Down Expand Up @@ -486,3 +493,104 @@ def SLR_quantity_parsing(expr, parameters, parser, name):

tag_handler = set_tags(parameters.get("strictness", "strict"))
return PhysicalQuantity(name, parameters, quantity[0], parser, messages=[], tag_handler=tag_handler)

def expression_preprocess(name, expr, parameters):
if parameters.get("strictness", "natural") == "legacy":
expr = preprocess_legacy(expr, parameters)
return True, expr, None

expr = transform_prefixes_to_standard(expr)

return True, expr, None

def preprocess_legacy(expr, parameters):
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"(?<!\*)\* *" + unit_long_forms
match_content = re.search(search_string, expr[1:])
while match_content is not None:
expr = expr[0:match_content.span()[0] + 1] + match_content.group().replace("*", " ") + expr[
match_content.span()[
1] + 1:]
match_content = re.search(search_string, expr[1:])
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)
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)
# 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)
# 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)
# 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)

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'(?<!\w){re.escape(alt)}\s*(?=[A-Za-zµΩ])'
expr = re.sub(pattern, prefix_name, expr)

# Normalize spacing (no multiple spaces)
expr = re.sub(r'\s{2,}', ' ', expr).strip()

return expr