Skip to content

Commit 27f35b0

Browse files
Merge pull request #118 from lambda-feedback/tr135-update-algorithm
Improved algorithm for checking groups, and reorganized to collect fe…
2 parents 6bb73a6 + fea515c commit 27f35b0

File tree

2 files changed

+213
-54
lines changed

2 files changed

+213
-54
lines changed

app/evaluation.py

Lines changed: 146 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from sympy.parsing.sympy_parser import parse_expr, split_symbols_custom
22
from sympy.parsing.sympy_parser import T as parser_transformations
3-
from sympy import simplify, latex, Matrix, Symbol, Integer, Add, Subs, pi, posify
3+
from sympy import simplify, latex, Matrix, Symbol, Integer, Add, Subs, pi, posify, prod
44
import sys, re
55

66
try:
@@ -14,6 +14,114 @@
1414

1515
parse_error_warning = lambda x: f"`{x}` could not be parsed as a valid mathematical expression. Ensure that correct notation is used, that the expression is unambiguous and that all parentheses are closed."
1616

17+
def feedback_not_dimensionless(groups):
18+
groups = list(groups)
19+
if len(groups) == 1:
20+
return f"The group {str(groups[0])} is not dimensionless."
21+
else:
22+
return f"The groups "+", ".join([str(g) for g in groups[0:-1]])+"and"+str(groups[-1])+"are not dimensionless."
23+
24+
parsing_feedback_responses = {
25+
"PARSE_ERROR_WARNING": lambda x: f"`{x}` could not be parsed as a valid mathematical expression. Ensure that correct notation is used, that the expression is unambiguous and that all parentheses are closed.",
26+
"PER_FOR_DIVISION": "Note that 'per' was interpreted as '/'. This can cause ambiguities. It is recommended to use parentheses to make your entry unambiguous.",
27+
"STRICT_SYNTAX_EXPONENTIATION": "Note that `^` cannot be used to denote exponentiation, use `**` instead.",
28+
"QUANTITIES_NOT_WRITTEN_CORRECTLY": "List of quantities not written correctly.",
29+
"SUBSTITUTIONS_NOT_WRITTEN_CORRECTLY": "List of substitutions not written correctly.",
30+
}
31+
32+
buckingham_pi_feedback_responses = {
33+
"VALID_CANDIDATE_SET": "",
34+
"NOT_DIMENSIONLESS": feedback_not_dimensionless,
35+
"MORE_GROUPS_THAN_REFERENCE_SET": "Response has more groups than necessary.",
36+
"CANDIDATE_GROUPS_NOT_INDEPENDENT": lambda r, n: f"Groups in response are not independent. It has {r} independent groups and contains {n} groups." ,
37+
"TOO_FEW_INDEPENDENT_GROUPS": lambda name, r, n: f"{name} contains too few independent groups. It has {r} independent products and needs at least {n} independent groups.",
38+
"UNKNOWN_SYMBOL": lambda symbols: "Unknown symbol(s): "+", ".join([str(s) for s in symbols])+".",
39+
"SUM_WITH_INDEPENDENT_TERMS": lambda s: f"Sum in {s} group contains more independent terms that there are groups in total. Group expressions should ideally be written as a comma-separated list where each item is an entry of the form `q_1**c_1*q_2**c_2*...*q_n**c_n`."
40+
}
41+
42+
def get_exponent_matrix(expressions, symbols):
43+
exponents_list = []
44+
for expression in expressions:
45+
exponents = []
46+
for symbol in symbols:
47+
exponent = expression.as_coeff_exponent(symbol)[1]
48+
if exponent == 0:
49+
exponent = -expression.subs(symbol,1/symbol).as_coeff_exponent(symbol)[1]
50+
exponents.append(exponent)
51+
exponents_list.append(exponents)
52+
return Matrix(exponents_list)
53+
54+
def string_to_expressions(string):
55+
beta = Symbol("beta")
56+
gamma = Symbol("gamma")
57+
zeta = Symbol("zeta")
58+
#e = E
59+
E = Symbol("E")
60+
I = Symbol("I")
61+
O = Symbol("O")
62+
N = Symbol("N")
63+
Q = Symbol("Q")
64+
S = Symbol("S")
65+
symbol_dict = {
66+
"beta": beta,
67+
"gamma": gamma,
68+
"zeta": zeta,
69+
"I": I,
70+
"N": N,
71+
"O": O,
72+
"Q": Q,
73+
"S": S,
74+
"E": E
75+
}
76+
expressions = [parse_expr(expr, local_dict=symbol_dict).expand(power_base=True, force=True) for expr in string.split(',')]
77+
symbols = set()
78+
for expression in expressions:
79+
expr = expression.simplify()
80+
expr = expr.expand(power_base=True, force=True)
81+
symbols = symbols.union(expression.free_symbols)
82+
return expressions, symbols
83+
84+
def create_power_product(exponents, symbols):
85+
return prod([s**i for (s,i) in zip(symbols, exponents)])
86+
87+
def determine_validity(reference_set, reference_symbols, candidate_set, candidate_symbols):
88+
symbols = set(reference_symbols).union(set(candidate_symbols))
89+
R = get_exponent_matrix(reference_set, symbols)
90+
C = get_exponent_matrix(candidate_set, symbols)
91+
D = R.col_join(C)
92+
valid = False
93+
feedback = []
94+
more_groups_than_reference_set = len(reference_set) >= len(candidate_set)
95+
candidate_groups_independent = C.rank() == len(candidate_set)
96+
rank_R_equal_to_rank_D = R.rank() == D.rank()
97+
rank_C_equal_to_rank_D = C.rank() == D.rank()
98+
if candidate_symbols.issubset(reference_symbols):
99+
if not more_groups_than_reference_set:
100+
feedback.append(buckingham_pi_feedback_responses["MORE_GROUPS_THAN_REFERENCE_SET"])
101+
if not candidate_groups_independent:
102+
feedback.append(buckingham_pi_feedback_responses["CANDIDATE_GROUPS_NOT_INDEPENDENT"](C.rank(), len(candidate_set)))
103+
if rank_R_equal_to_rank_D:
104+
if rank_C_equal_to_rank_D:
105+
valid = True
106+
feedback.append(buckingham_pi_feedback_responses["VALID_CANDIDATE_SET"])
107+
else:
108+
feedback.append(buckingham_pi_feedback_responses["TOO_FEW_INDEPENDENT_GROUPS"]("Response", C.rank(), D.rank()))
109+
else:
110+
dimensionless_groups = set()
111+
for i in range(len(candidate_set)):
112+
Ci = C.copy()
113+
exponents = Ci.row(i)
114+
Ci.row_del(i)
115+
Di = R.col_join(Ci)
116+
if R.rank() != Di.rank():
117+
dimensionless_groups.add(create_power_product(exponents, symbols))
118+
if len(dimensionless_groups) > 0:
119+
feedback.append(buckingham_pi_feedback_responses["NOT_DIMENSIONLESS"](dimensionless_groups))
120+
else:
121+
feedback.append(buckingham_pi_feedback_responses["UNKNOWN_SYMBOL"](candidate_symbols.difference(reference_symbols)))
122+
feedback = [elem.strip() for elem in feedback if len(elem.strip()) > 0]
123+
return valid, "<br>".join(feedback)
124+
17125
def evaluation_function(response, answer, params) -> dict:
18126
"""
19127
Function that provides some basic dimensional analysis functionality.
@@ -35,9 +143,9 @@ def evaluation_function(response, answer, params) -> dict:
35143
remark = ""
36144
if "per" not in sum([[x[0]]+x[1] for x in parameters.get("input_symbols",[])],[]):
37145
if (" per " in response):
38-
remark += "Note that 'per' was interpreted as '/'. This can cause ambiguities. It is recommended to use parentheses to make your entry unambiguous."
146+
remark += parsing_feedback_responses["PER_FOR_DIVISION"]
39147
if (" per " in answer):
40-
raise Exception("Note that 'per' is interpreted as '/'. This can cause ambiguities. Use '/' and parenthesis and ensure the answer is unambiguous.")
148+
raise Exception(parsing_feedback_responses["PER_FOR_DIVISION"])
41149
answer = substitute(answer+" ", convert_alternative_names_to_standard+[(" per ","/")])[0:-1]
42150
response = substitute(response+" ", convert_alternative_names_to_standard+[(" per ","/")])[0:-1]
43151

@@ -60,22 +168,24 @@ def evaluation_function(response, answer, params) -> dict:
60168
if parameters["strict_syntax"]:
61169
if "^" in response:
62170
separator = "" if len(remark) == 0 else "\n"
63-
remark += separator+"Note that `^` cannot be used to denote exponentiation, use `**` instead."
171+
remark += separator+parsing_feedback_responses["STRICT_SYNTAX_EXPONENTIATION"]
64172
if "^" in answer:
65-
raise Exception("Note that `^` cannot be used to denote exponentiation, use `**` instead.")
173+
raise Exception(parsing_feedback_responses["STRICT_SYNTAX_EXPONENTIATION"])
66174

67175
if parameters["comparison"] == "buckinghamPi":
68176
# Parse expressions for groups in response and answer
69177
response_strings = response.split(',')
70178
response_number_of_groups = len(response_strings)
179+
response_original_number_of_groups = len(response_strings)
71180
response_groups = []
181+
separator = "" if len(remark) == 0 else "\n"
72182
for res in response_strings:
73183
try:
74184
expr = parse_expression(res,parsing_params).simplify()
75185
expr = expr.expand(power_base=True, force=True)
76186
except Exception as e:
77187
separator = "" if len(remark) == 0 else "\n"
78-
return {"is_correct": False, "feedback": parse_error_warning(response)+separator+remark}
188+
return {"is_correct": False, "feedback": parsing_feedback_responses["PARSE_ERROR_WARNING"](response)+separator+remark}
79189
if isinstance(expr,Add):
80190
response_groups += list(expr.args)
81191
else:
@@ -88,18 +198,22 @@ def evaluation_function(response, answer, params) -> dict:
88198
answer_strings = []
89199
else:
90200
answer_strings = answer.split(',')
91-
answer_number_of_groups = len(answer_strings)
92201
answer_groups = []
202+
answer_number_of_groups = 0
203+
answer_original_number_of_groups = 0
93204
for ans in answer_strings:
94205
try:
95206
expr = parse_expression(ans,parsing_params).simplify()
96207
expr = expr.expand(power_base=True, force=True)
97208
except Exception as e:
98-
raise Exception("SymPy was unable to parse the answer") from e
209+
raise Exception(parsing_feedback_responses["PARSE_ERROR_WARNING"]("The answer")) from e
99210
if isinstance(expr,Add):
100211
answer_groups += list(expr.args)
212+
answer_number_of_groups += len(list(expr.args))
101213
else:
102214
answer_groups.append(expr)
215+
answer_number_of_groups += 1
216+
answer_original_number_of_groups += 1
103217

104218
remark = ""
105219

@@ -115,7 +229,7 @@ def evaluation_function(response, answer, params) -> dict:
115229
quantity = tuple(map(lambda x: parse_expression(x,parsing_params),quantity_strings))
116230
quantities.append(quantity)
117231
except Exception as e:
118-
raise Exception("List of quantities not written correctly.")
232+
raise Exception(parsing_feedback_responses["QUANTITIES_NOT_WRITTEN_CORRECTLY"])
119233
index = quantities_strings.find('(',index_match+1)
120234
response_symbols = list(map(lambda x: x[0], quantities))
121235
answer_symbols = response_symbols
@@ -157,12 +271,12 @@ def evaluation_function(response, answer, params) -> dict:
157271
# Check that answers are dimensionless
158272
for k,dimension in enumerate(answer_dimensions):
159273
if not dimension.is_constant():
160-
raise Exception(f"Answer ${latex(answer_groups[k])}$ is not dimensionless.")
161-
274+
raise Exception(buckingham_pi_feedback_responses["NOT_DIMENSIONLESS"]("$"+latex(answer_groups[k])+"$"))
275+
162276
# Check that there is a sufficient number of independent groups in the answer
163277
answer_matrix = get_exponent_matrix(answer_groups,answer_symbols)
164278
if answer_matrix.rank() < number_of_groups:
165-
raise Exception(f"Answer contains too few independent groups. It has {answer_matrix.rank()} independent groups and needs at least {number_of_groups} independent groups.")
279+
raise Exception(buckingham_pi_feedback_responses["TOO_FEW_INDEPENDENT_GROUPS"]("Answer", answer_matrix.rank(),number_of_groups))
166280

167281
response_symbols = set()
168282
for res in response_groups:
@@ -171,29 +285,9 @@ def evaluation_function(response, answer, params) -> dict:
171285
for ans in answer_groups:
172286
answer_symbols = answer_symbols.union(ans.free_symbols)
173287
if not response_symbols.issubset(answer_symbols):
174-
feedback.update({"feedback": f"The following symbols in the response were not expected {response_symbols.difference(answer_symbols)}."})
288+
feedback.update({"feedback": buckingham_pi_feedback_responses["UNKNOWN_SYMBOL"](response_symbols.difference(answer_symbols))})
175289
return {"is_correct": False, **feedback, **interp}
176290
answer_symbols = list(answer_symbols)
177-
178-
# Check that responses are dimensionless
179-
response_dimensions = []
180-
for group in response_groups:
181-
dimension = group
182-
for quantity in quantities:
183-
dimension = dimension.subs(quantity[0],quantity[1])
184-
response_dimensions.append(posify(dimension)[0].simplify())
185-
for k,dimension in enumerate(response_dimensions):
186-
if not dimension.is_constant():
187-
feedback.update({"feedback": f"Response ${response_latex[k]}$ is not dimensionless."})
188-
return {"is_correct": False, **feedback, **interp}
189-
190-
# Check that there is a sufficient number of independent groups in the response
191-
response_matrix = get_exponent_matrix(response_groups,response_symbols)
192-
if response_matrix.rank() < number_of_groups:
193-
feedback.update({"feedback": f"Response contains too few independent groups. It has {response_matrix.rank()} independent groups and needs at least {number_of_groups} independent groups."})
194-
return {"is_correct": False, **feedback, **interp}
195-
if response_matrix.rank() > number_of_groups or response_number_of_groups > number_of_groups:
196-
remark = "Response has more groups than necessary."
197291
else:
198292
response_symbols = set()
199293
for res in response_groups:
@@ -202,23 +296,27 @@ def evaluation_function(response, answer, params) -> dict:
202296
for ans in answer_groups:
203297
answer_symbols = answer_symbols.union(ans.free_symbols)
204298
if not response_symbols.issubset(answer_symbols):
205-
feedback.update({"feedback": f"The following symbols in the response were not expected {response_symbols.difference(answer_symbols)}."})
299+
feedback.update({"feedback": buckingham_pi_feedback_responses["UNKNOWN_SYMBOL"](response_symbols.difference(answer_symbols))})
206300
return {"is_correct": False, **feedback, **interp}
207301
answer_symbols = list(answer_symbols)
208-
209-
# Extract exponents from answers and responses and compare matrix ranks
210-
sum_add_independent = lambda s: f"Sum in {s} group contains more independent terms that there are groups in total. Group expressions should ideally be written as a comma-separated list where each item is an entry of the form `q_1**c_1*q_2**c_2*...*q_n**c_n`."
302+
303+
reference_set = set(answer_groups)
304+
reference_symbols = set(answer_symbols)
305+
candidate_set = set(response_groups)
306+
candidate_symbols = set(response_symbols)
307+
valid, feedback_string = determine_validity(reference_set, reference_symbols, candidate_set, candidate_symbols)
308+
feedback.update({"feedback": feedback_string})
309+
310+
# Check the special case where one groups expression contains several power products
211311
separator = "" if len(remark) == 0 else "\n"
212312
answer_matrix = get_exponent_matrix(answer_groups,answer_symbols)
213313
if answer_matrix.rank() > answer_number_of_groups:
214-
raise Exception(sum_add_independent("answer"))
215-
response_matrix = get_exponent_matrix(response_groups,answer_symbols)
216-
if response_matrix.rank() > response_number_of_groups:
217-
return {"is_correct": False, "feedback": sum_add_independent("response")+separator+remark, **interp}
218-
enhanced_matrix = answer_matrix.col_join(response_matrix)
219-
if answer_matrix.rank() == enhanced_matrix.rank() and response_matrix.rank() == enhanced_matrix.rank():
220-
return {"is_correct": True, "feedback": feedback.get("feedback","")+separator+remark, **interp}
221-
return {"is_correct": False, "feedback": feedback.get("feedback","")+separator+remark, **interp}
314+
raise Exception(buckingham_pi_feedback_responses["SUM_WITH_INDEPENDENT_TERMS"]("answer"))
315+
response_matrix = get_exponent_matrix(response_groups, answer_symbols)
316+
if response_matrix.rank() > response_original_number_of_groups:
317+
return {"is_correct": False, "feedback": buckingham_pi_feedback_responses["SUM_WITH_INDEPENDENT_TERMS"]("response")+separator+remark, **interp}
318+
319+
return {"is_correct": valid, "feedback": feedback.get("feedback","")+separator+remark, **interp}
222320

223321
list_of_substitutions_strings = parameters.get("substitutions",[])
224322
if isinstance(list_of_substitutions_strings,str):
@@ -228,13 +326,13 @@ def evaluation_function(response, answer, params) -> dict:
228326
list_of_substitutions_strings = [parameters["quantities"]]+list_of_substitutions_strings
229327

230328
if not (isinstance(list_of_substitutions_strings,list) and all(isinstance(element,str) for element in list_of_substitutions_strings)):
231-
raise Exception("List of substitutions not written correctly.")
329+
raise Exception(parsing_feedback_responses["SUBSTITUTIONS_NOT_WRITTEN_CORRECTLY"])
232330

233331
try:
234332
interp = {"response_latex": expression_to_latex(response,parameters,parsing_params,remark)}
235333
except Exception as e:
236334
separator = "" if len(remark) == 0 else "\n"
237-
return {"is_correct": False, "feedback": parse_error_warning(response)+separator+remark}
335+
return {"is_correct": False, "feedback": parsing_feedback_responses["PARSE_ERROR_WARNING"](response)+separator+remark}
238336

239337
substitutions = []
240338
for subs_strings in list_of_substitutions_strings:
@@ -246,7 +344,7 @@ def evaluation_function(response, answer, params) -> dict:
246344
try:
247345
sub_substitutions.append(eval(subs_strings[index:index_match+1]))
248346
except Exception as e:
249-
raise Exception("List of substitutions not written correctly.")
347+
raise Exception(parsing_feedback_responses["SUBSTITUTIONS_NOT_WRITTEN_CORRECTLY"])
250348
index = subs_strings.find('(',index_match+1)
251349
if index > -1 and subs_strings.find('|',index_match,index) > -1:
252350
# Substitutions are sorted so that the longest possible part of the original string will be substituted in each step

0 commit comments

Comments
 (0)