11from sympy .parsing .sympy_parser import parse_expr , split_symbols_custom
22from 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
44import sys , re
55
66try :
1414
1515parse_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+
17125def 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