Skip to content

Commit 73dd982

Browse files
authored
Switched transforming prefix to standard to use the symbol instead of the name (#238)
* Added a test for mu, and switched transforming prefix to standard to use the symbol instead of the name * Reverted change * Removed old TODO statement * Fixed micro to be able to be used as a prefix * Fixed preview for physical quantities
1 parent 2cc0a77 commit 73dd982

File tree

6 files changed

+138
-107
lines changed

6 files changed

+138
-107
lines changed

app/context/physical_quantity.py

Lines changed: 1 addition & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from copy import deepcopy
77
from ..utility.physical_quantity_utilities import (
88
SLR_quantity_parser,
9-
SLR_quantity_parsing
9+
SLR_quantity_parsing, expression_preprocess
1010
)
1111
from ..preview_implementations.physical_quantity_preview import preview_function
1212
from ..feedback.physical_quantity import feedback_string_generators as physical_quantity_feedback_string_generators
@@ -476,107 +476,6 @@ def feedback_procedure_generator(parameters_dict):
476476
graphs.update({label: graph})
477477
return graphs
478478

479-
def preprocess_legacy(expr, parameters):
480-
prefix_data = {(p[0], p[1], tuple(), p[3]) for p in set_of_SI_prefixes}
481-
prefixes = []
482-
for prefix in prefix_data:
483-
prefixes = prefixes + [prefix[0]] + list(prefix[-1])
484-
prefix_short_forms = [prefix[1] for prefix in prefix_data]
485-
unit_data = set_of_SI_base_unit_dimensions \
486-
| set_of_derived_SI_units_in_SI_base_units \
487-
| set_of_common_units_in_SI \
488-
| set_of_very_common_units_in_SI \
489-
| set_of_imperial_units
490-
unit_long_forms = prefixes
491-
for unit in unit_data:
492-
unit_long_forms = unit_long_forms + [unit[0]] + list(unit[-2]) + list(unit[-1])
493-
unit_long_forms = "(" + "|".join(unit_long_forms) + ")"
494-
# Rewrite any expression on the form "*UNIT" (but not "**UNIT") as " UNIT"
495-
# Example: "newton*metre" ---> "newton metre"
496-
search_string = r"(?<!\*)\* *" + unit_long_forms
497-
match_content = re.search(search_string, expr[1:])
498-
while match_content is not None:
499-
expr = expr[0:match_content.span()[0] + 1] + match_content.group().replace("*", " ") + expr[
500-
match_content.span()[
501-
1] + 1:]
502-
match_content = re.search(search_string, expr[1:])
503-
prefixes = "(" + "|".join(prefixes) + ")"
504-
# Rewrite any expression on the form "PREFIX UNIT" as "PREFIXUNIT"
505-
# Example: "kilo metre" ---> "kilometre"
506-
search_string = prefixes + " " + unit_long_forms
507-
match_content = re.search(search_string, expr)
508-
while match_content is not None:
509-
expr = expr[0:match_content.span()[0]] + " " + "".join(match_content.group().split()) + expr[
510-
match_content.span()[
511-
1]:]
512-
match_content = re.search(search_string, expr)
513-
unit_short_forms = [u[1] for u in unit_data]
514-
short_forms = "(" + "|".join(list(set(prefix_short_forms + unit_short_forms))) + ")"
515-
# Add space before short forms of prefixes or unit names if they are preceded by numbers or multiplication
516-
# Example: "100Pa" ---> "100 Pa"
517-
search_string = r"[0-9\*\(\)]" + short_forms
518-
match_content = re.search(search_string, expr)
519-
while match_content is not None:
520-
expr = expr[0:match_content.span()[0] + 1] + " " + expr[match_content.span()[0] + 1:]
521-
match_content = re.search(search_string, expr)
522-
# Remove space after prefix short forms if they are preceded by numbers, multiplication or space
523-
# Example: "100 m Pa" ---> "100 mPa"
524-
prefix_short_forms = "(" + "|".join(prefix_short_forms) + ")"
525-
search_string = r"[0-9\*\(\) ]" + prefix_short_forms + " "
526-
match_content = re.search(search_string, expr)
527-
while match_content is not None:
528-
expr = expr[0:match_content.span()[0] + 1] + match_content.group()[0:-1] + expr[match_content.span()[1]:]
529-
match_content = re.search(search_string, expr)
530-
# Remove multiplication and space after prefix short forms if they are preceded by numbers, multiplication or space
531-
# Example: "100 m* Pa" ---> "100 mPa"
532-
search_string = r"[0-9\*\(\) ]" + prefix_short_forms + "\* "
533-
match_content = re.search(search_string, expr)
534-
while match_content is not None:
535-
expr = expr[0:match_content.span()[0] + 1] + match_content.group()[0:-2] + expr[match_content.span()[1]:]
536-
match_content = re.search(search_string, expr)
537-
# Replace multiplication followed by space before unit short forms with only spaces if they are preceded by numbers or space
538-
# Example: "100* Pa" ---> "100 Pa"
539-
unit_short_forms = "(" + "|".join(unit_short_forms) + ")"
540-
search_string = r"[0-9\(\) ]\* " + unit_short_forms
541-
match_content = re.search(search_string, expr)
542-
while match_content is not None:
543-
expr = expr[0:match_content.span()[0]] + match_content.group().replace("*", " ") + expr[
544-
match_content.span()[1]:]
545-
match_content = re.search(search_string, expr)
546-
547-
return expr
548-
549-
def transform_prefixes_to_standard(expr):
550-
"""
551-
Transform ONLY alternative prefix spellings to standard prefix names.
552-
Ensure there's exactly one space after the prefix before the unit.
553-
Works for both attached (e.g. 'km') and spaced (e.g. 'k m') forms.
554-
"""
555-
556-
for prefix_name, symbol, power, alternatives in set_of_SI_prefixes:
557-
for alt in alternatives:
558-
if not alt:
559-
continue
560-
561-
# Match the alternative prefix either attached to or followed by spaces before a unit
562-
# Examples matched: "km", "k m", "microsecond", "micro second"
563-
pattern = rf'(?<!\w){re.escape(alt)}\s*(?=[A-Za-zµΩ])'
564-
expr = re.sub(pattern, prefix_name + ' ', expr)
565-
566-
# Normalize spacing (no multiple spaces)
567-
expr = re.sub(r'\s{2,}', ' ', expr).strip()
568-
569-
return expr
570-
571-
def expression_preprocess(name, expr, parameters):
572-
if parameters.get("strictness", "natural") == "legacy":
573-
expr = preprocess_legacy(expr, parameters)
574-
return True, expr, None
575-
576-
expr = transform_prefixes_to_standard(expr)
577-
578-
return True, expr, None
579-
580479

581480
def feedback_string_generator(tags, graph, parameters_dict):
582481
strings = dict()

app/evaluation_tests.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,5 +169,29 @@ def test_multi_character_implicit_multi_variable(self):
169169
assert result["is_correct"] is True, "Response: a/bcd"
170170

171171

172+
def test_mu_preview_evaluate(self):
173+
response = "10 μA"
174+
params = Params(is_latex=False, elementary_functions=False, strict_syntax=False, physical_quantity=True)
175+
result = preview_function(response, params)
176+
assert "preview" in result.keys()
177+
178+
preview = result["preview"]
179+
assert preview["latex"] == "10~\\mathrm{microampere}"
180+
assert preview["sympy"] == "10 μA"
181+
182+
params = {
183+
"atol": 0.0,
184+
"rtol": 0.0,
185+
"strict_syntax": False,
186+
"physical_quantity": True,
187+
"elementary_functions": False,
188+
}
189+
190+
response = preview["sympy"]
191+
answer = "10 muA"
192+
result = evaluation_function(response, answer, params)
193+
assert result["is_correct"] is True
194+
195+
172196
if __name__ == "__main__":
173197
pytest.main(['-xk not slow', '--tb=short', '--durations=10', os.path.abspath(__file__)])

app/preview_implementations/physical_quantity_preview.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
)
1717

1818
from ..utility.expression_utilities import default_parameters as symbolic_default_parameters
19-
from ..utility.physical_quantity_utilities import SLR_quantity_parser as quantity_parser
19+
from ..utility.physical_quantity_utilities import SLR_quantity_parser as quantity_parser, expression_preprocess
2020
from ..utility.physical_quantity_utilities import SLR_quantity_parsing as quantity_parsing
2121

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

app/preview_tests.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ def test_natural_logarithm_notation(self):
9393
("e * x", True, False, "e * x", "E*x"),
9494
("E", True, False, "E", "E",),
9595
("ER_2", True, False, "ER_2", "E*R_2",),
96-
# TODO: add exp (0), (1), (2) and (x)
9796
("exp(1)", False, True, "e^{1}", "exp(1)"),
9897
("e**1", False, True, "e^{1}", "E**1"),
9998
("e^{1}", True, True, "e^{1}", "E"),

app/tests/physical_quantity_evaluation_tests.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,8 +378,8 @@ def test_answer_zero_value(self):
378378
"ans,res",
379379
[
380380
("10 ohm", "10 Ω"),
381-
("10 micro A", "10 μA"),
382-
("10 micro A", "10 μ A"),
381+
("10 microA", "10 μA"),
382+
("10 microA", "10 μ A"),
383383
("30 degree", "30 °"),
384384
]
385385
)

app/utility/physical_quantity_utilities.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,13 @@ def SLR_generate_unit_dictionaries(units_string, strictness):
273273
prefixes_long_to_short[prefix]+units_long_to_short[unit]: prefix+units[unit]
274274
}
275275
)
276+
if prefix + units_long_to_short[unit] not in units_short_to_long.keys():
277+
prefixed_units.update(
278+
{
279+
prefix + units_long_to_short[unit]: prefix + units[unit]
280+
}
281+
)
282+
276283

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

487494
tag_handler = set_tags(parameters.get("strictness", "strict"))
488495
return PhysicalQuantity(name, parameters, quantity[0], parser, messages=[], tag_handler=tag_handler)
496+
497+
def expression_preprocess(name, expr, parameters):
498+
if parameters.get("strictness", "natural") == "legacy":
499+
expr = preprocess_legacy(expr, parameters)
500+
return True, expr, None
501+
502+
expr = transform_prefixes_to_standard(expr)
503+
504+
return True, expr, None
505+
506+
def preprocess_legacy(expr, parameters):
507+
prefix_data = {(p[0], p[1], tuple(), p[3]) for p in set_of_SI_prefixes}
508+
prefixes = []
509+
for prefix in prefix_data:
510+
prefixes = prefixes + [prefix[0]] + list(prefix[-1])
511+
prefix_short_forms = [prefix[1] for prefix in prefix_data]
512+
unit_data = set_of_SI_base_unit_dimensions \
513+
| set_of_derived_SI_units_in_SI_base_units \
514+
| set_of_common_units_in_SI \
515+
| set_of_very_common_units_in_SI \
516+
| set_of_imperial_units
517+
unit_long_forms = prefixes
518+
for unit in unit_data:
519+
unit_long_forms = unit_long_forms + [unit[0]] + list(unit[-2]) + list(unit[-1])
520+
unit_long_forms = "(" + "|".join(unit_long_forms) + ")"
521+
# Rewrite any expression on the form "*UNIT" (but not "**UNIT") as " UNIT"
522+
# Example: "newton*metre" ---> "newton metre"
523+
search_string = r"(?<!\*)\* *" + unit_long_forms
524+
match_content = re.search(search_string, expr[1:])
525+
while match_content is not None:
526+
expr = expr[0:match_content.span()[0] + 1] + match_content.group().replace("*", " ") + expr[
527+
match_content.span()[
528+
1] + 1:]
529+
match_content = re.search(search_string, expr[1:])
530+
prefixes = "(" + "|".join(prefixes) + ")"
531+
# Rewrite any expression on the form "PREFIX UNIT" as "PREFIXUNIT"
532+
# Example: "kilo metre" ---> "kilometre"
533+
search_string = prefixes + " " + unit_long_forms
534+
match_content = re.search(search_string, expr)
535+
while match_content is not None:
536+
expr = expr[0:match_content.span()[0]] + " " + "".join(match_content.group().split()) + expr[
537+
match_content.span()[
538+
1]:]
539+
match_content = re.search(search_string, expr)
540+
unit_short_forms = [u[1] for u in unit_data]
541+
short_forms = "(" + "|".join(list(set(prefix_short_forms + unit_short_forms))) + ")"
542+
# Add space before short forms of prefixes or unit names if they are preceded by numbers or multiplication
543+
# Example: "100Pa" ---> "100 Pa"
544+
search_string = r"[0-9\*\(\)]" + short_forms
545+
match_content = re.search(search_string, expr)
546+
while match_content is not None:
547+
expr = expr[0:match_content.span()[0] + 1] + " " + expr[match_content.span()[0] + 1:]
548+
match_content = re.search(search_string, expr)
549+
# Remove space after prefix short forms if they are preceded by numbers, multiplication or space
550+
# Example: "100 m Pa" ---> "100 mPa"
551+
prefix_short_forms = "(" + "|".join(prefix_short_forms) + ")"
552+
search_string = r"[0-9\*\(\) ]" + prefix_short_forms + " "
553+
match_content = re.search(search_string, expr)
554+
while match_content is not None:
555+
expr = expr[0:match_content.span()[0] + 1] + match_content.group()[0:-1] + expr[match_content.span()[1]:]
556+
match_content = re.search(search_string, expr)
557+
# Remove multiplication and space after prefix short forms if they are preceded by numbers, multiplication or space
558+
# Example: "100 m* Pa" ---> "100 mPa"
559+
search_string = r"[0-9\*\(\) ]" + prefix_short_forms + "\* "
560+
match_content = re.search(search_string, expr)
561+
while match_content is not None:
562+
expr = expr[0:match_content.span()[0] + 1] + match_content.group()[0:-2] + expr[match_content.span()[1]:]
563+
match_content = re.search(search_string, expr)
564+
# Replace multiplication followed by space before unit short forms with only spaces if they are preceded by numbers or space
565+
# Example: "100* Pa" ---> "100 Pa"
566+
unit_short_forms = "(" + "|".join(unit_short_forms) + ")"
567+
search_string = r"[0-9\(\) ]\* " + unit_short_forms
568+
match_content = re.search(search_string, expr)
569+
while match_content is not None:
570+
expr = expr[0:match_content.span()[0]] + match_content.group().replace("*", " ") + expr[
571+
match_content.span()[1]:]
572+
match_content = re.search(search_string, expr)
573+
574+
return expr
575+
576+
def transform_prefixes_to_standard(expr):
577+
"""
578+
Transform ONLY alternative prefix spellings to standard prefix names.
579+
Ensure there's exactly one space after the prefix before the unit.
580+
Works for both attached (e.g. 'km') and spaced (e.g. 'k m') forms.
581+
"""
582+
583+
for prefix_name, symbol, power, alternatives in set_of_SI_prefixes:
584+
for alt in alternatives:
585+
if not alt:
586+
continue
587+
588+
# Match the alternative prefix either attached to or followed by spaces before a unit
589+
# Examples matched: "km", "k m", "microsecond", "micro second"
590+
pattern = rf'(?<!\w){re.escape(alt)}\s*(?=[A-Za-zµΩ])'
591+
expr = re.sub(pattern, prefix_name, expr)
592+
593+
# Normalize spacing (no multiple spaces)
594+
expr = re.sub(r'\s{2,}', ' ', expr).strip()
595+
596+
return expr

0 commit comments

Comments
 (0)