Commit cb27c471 by Piotr Mitros

Capa problems handle case sensitivity and give better errors

--HG--
branch : pmitros-capa-fixes
parent ffba3c59
...@@ -2,6 +2,7 @@ import copy ...@@ -2,6 +2,7 @@ import copy
import logging import logging
import math import math
import operator import operator
import re
import numpy import numpy
import scipy.constants import scipy.constants
...@@ -10,7 +11,7 @@ from pyparsing import Word, alphas, nums, oneOf, Literal ...@@ -10,7 +11,7 @@ from pyparsing import Word, alphas, nums, oneOf, Literal
from pyparsing import ZeroOrMore, OneOrMore, StringStart from pyparsing import ZeroOrMore, OneOrMore, StringStart
from pyparsing import StringEnd, Optional, Forward from pyparsing import StringEnd, Optional, Forward
from pyparsing import CaselessLiteral, Group, StringEnd from pyparsing import CaselessLiteral, Group, StringEnd
from pyparsing import NoMatch, stringEnd from pyparsing import NoMatch, stringEnd, alphanums
default_functions = {'sin' : numpy.sin, default_functions = {'sin' : numpy.sin,
'cos' : numpy.cos, 'cos' : numpy.cos,
...@@ -35,10 +36,40 @@ default_variables = {'j':numpy.complex(0,1), ...@@ -35,10 +36,40 @@ default_variables = {'j':numpy.complex(0,1),
log = logging.getLogger("mitx.courseware.capa") log = logging.getLogger("mitx.courseware.capa")
def evaluator(variables, functions, string): class UndefinedVariable(Exception):
def raiseself(self):
''' Helper so we can use inside of a lambda '''
raise self
general_whitespace = re.compile('[^\w]+')
def check_variables(string, variables):
''' Confirm the only variables in string are defined.
Pyparsing uses a left-to-right parser, which makes the more
elegant approach pretty hopeless.
achar = reduce(lambda a,b:a|b ,map(Literal,alphas)) # Any alphabetic character
undefined_variable = achar + Word(alphanums)
undefined_variable.setParseAction(lambda x:UndefinedVariable("".join(x)).raiseself())
varnames = varnames | undefined_variable'''
possible_variables = re.split(general_whitespace, string) # List of all alnums in string
bad_variables = list()
for v in possible_variables:
if len(v) == 0:
continue
if v[0] <= '9' and '0' <= 'v': # Skip things that begin with numbers
continue
if v not in variables:
bad_variables.append(v)
if len(bad_variables)>0:
raise UndefinedVariable(' '.join(bad_variables))
def evaluator(variables, functions, string, cs=False):
''' Evaluate an expression. Variables are passed as a dictionary ''' Evaluate an expression. Variables are passed as a dictionary
from string to value. Unary functions are passed as a dictionary from string to value. Unary functions are passed as a dictionary
from string to function. Variables must be floats. from string to function. Variables must be floats.
cs: Case sensitive
TODO: Fix it so we can pass integers and complex numbers in variables dict TODO: Fix it so we can pass integers and complex numbers in variables dict
''' '''
...@@ -51,6 +82,19 @@ def evaluator(variables, functions, string): ...@@ -51,6 +82,19 @@ def evaluator(variables, functions, string):
all_functions = copy.copy(default_functions) all_functions = copy.copy(default_functions)
all_functions.update(functions) all_functions.update(functions)
if not cs:
string_cs = string.lower()
for v in all_variables.keys():
all_variables[v.lower()]=all_variables[v]
for f in all_functions.keys():
all_functions[f.lower()]=all_functions[f]
CasedLiteral = CaselessLiteral
else:
string_cs = string
CasedLiteral = Literal
check_variables(string_cs, set(all_variables.keys()+all_functions.keys()))
if string.strip() == "": if string.strip() == "":
return float('nan') return float('nan')
ops = { "^" : operator.pow, ops = { "^" : operator.pow,
...@@ -137,19 +181,19 @@ def evaluator(variables, functions, string): ...@@ -137,19 +181,19 @@ def evaluator(variables, functions, string):
# We sort the list so that var names (like "e2") match before # We sort the list so that var names (like "e2") match before
# mathematical constants (like "e"). This is kind of a hack. # mathematical constants (like "e"). This is kind of a hack.
all_variables_keys = sorted(all_variables.keys(), key=len, reverse=True) all_variables_keys = sorted(all_variables.keys(), key=len, reverse=True)
varnames = sreduce(lambda x,y:x|y, map(lambda x: CaselessLiteral(x), all_variables_keys)) varnames = sreduce(lambda x,y:x|y, map(lambda x: CasedLiteral(x), all_variables_keys))
varnames.setParseAction(lambda x:map(lambda y:all_variables[y], x)) varnames.setParseAction(lambda x:map(lambda y:all_variables[y], x))
else: else:
varnames=NoMatch() varnames=NoMatch()
# Same thing for functions. # Same thing for functions.
if len(all_functions)>0: if len(all_functions)>0:
funcnames = sreduce(lambda x,y:x|y, map(lambda x: CaselessLiteral(x), all_functions.keys())) funcnames = sreduce(lambda x,y:x|y, map(lambda x: CasedLiteral(x), all_functions.keys()))
function = funcnames+lpar.suppress()+expr+rpar.suppress() function = funcnames+lpar.suppress()+expr+rpar.suppress()
function.setParseAction(func_parse_action) function.setParseAction(func_parse_action)
else: else:
function = NoMatch() function = NoMatch()
atom = number | varnames | lpar+expr+rpar | function atom = number | function | varnames | lpar+expr+rpar
factor << (atom + ZeroOrMore(exp+atom)).setParseAction(exp_parse_action) # 7^6 factor << (atom + ZeroOrMore(exp+atom)).setParseAction(exp_parse_action) # 7^6
paritem = factor + ZeroOrMore(Literal('||')+factor) # 5k || 4k paritem = factor + ZeroOrMore(Literal('||')+factor) # 5k || 4k
paritem=paritem.setParseAction(parallel) paritem=paritem.setParseAction(parallel)
......
...@@ -15,7 +15,7 @@ from mako.template import Template ...@@ -15,7 +15,7 @@ from mako.template import Template
from util import contextualize_text from util import contextualize_text
from inputtypes import textline, schematic from inputtypes import textline, schematic
from responsetypes import numericalresponse, formularesponse, customresponse, schematicresponse from responsetypes import numericalresponse, formularesponse, customresponse, schematicresponse, StudentInputError
import calc import calc
import eia import eia
......
...@@ -3,8 +3,9 @@ import math ...@@ -3,8 +3,9 @@ import math
import numpy import numpy
import random import random
import scipy import scipy
import traceback
from calc import evaluator from calc import evaluator, UndefinedVariable
from django.conf import settings from django.conf import settings
from util import contextualize_text from util import contextualize_text
...@@ -84,6 +85,9 @@ class customresponse(object): ...@@ -84,6 +85,9 @@ class customresponse(object):
# be handled by capa_problem # be handled by capa_problem
return {} return {}
class StudentInputError(Exception):
pass
class formularesponse(object): class formularesponse(object):
def __init__(self, xml, context): def __init__(self, xml, context):
self.xml = xml self.xml = xml
...@@ -95,6 +99,17 @@ class formularesponse(object): ...@@ -95,6 +99,17 @@ class formularesponse(object):
self.answer_id = xml.xpath('//*[@id=$id]//textline/@id', self.answer_id = xml.xpath('//*[@id=$id]//textline/@id',
id=xml.get('id'))[0] id=xml.get('id'))[0]
self.context = context self.context = context
ts = xml.get('type')
if ts == None:
typeslist = []
else:
typeslist = ts.split(',')
if 'ci' in typeslist: # Case insensitive
self.case_sensitive = False
elif 'cs' in typeslist: # Case sensitive
self.case_sensitive = True
else: # Default
self.case_sensitive = False
def grade(self, student_answers): def grade(self, student_answers):
...@@ -113,7 +128,16 @@ class formularesponse(object): ...@@ -113,7 +128,16 @@ class formularesponse(object):
instructor_variables[str(var)] = value instructor_variables[str(var)] = value
student_variables[str(var)] = value student_variables[str(var)] = value
instructor_result = evaluator(instructor_variables,dict(),self.correct_answer) instructor_result = evaluator(instructor_variables,dict(),self.correct_answer)
student_result = evaluator(student_variables,dict(),student_answers[self.answer_id]) try:
#print student_variables,dict(),student_answers[self.answer_id]
student_result = evaluator(student_variables,dict(),
student_answers[self.answer_id],
cs = self.case_sensitive)
except UndefinedVariable as uv:
raise StudentInputError('Undefined: '+uv.message)
except:
#traceback.print_exc()
raise StudentInputError("Syntax Error")
if math.isnan(student_result) or math.isinf(student_result): if math.isnan(student_result) or math.isinf(student_result):
return {self.answer_id:"incorrect"} return {self.answer_id:"incorrect"}
if not compare_with_tolerance(student_result, instructor_result, self.tolerance): if not compare_with_tolerance(student_result, instructor_result, self.tolerance):
......
...@@ -21,7 +21,7 @@ from mitxmako.shortcuts import render_to_response, render_to_string ...@@ -21,7 +21,7 @@ from mitxmako.shortcuts import render_to_response, render_to_string
from django.http import Http404 from django.http import Http404
from x_module import XModule from x_module import XModule
from courseware.capa.capa_problem import LoncapaProblem from courseware.capa.capa_problem import LoncapaProblem, StudentInputError
import courseware.content_parser as content_parser import courseware.content_parser as content_parser
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
...@@ -277,22 +277,27 @@ class Module(XModule): ...@@ -277,22 +277,27 @@ class Module(XModule):
lcp_id = self.lcp.problem_id lcp_id = self.lcp.problem_id
filename = self.lcp.filename filename = self.lcp.filename
correct_map = self.lcp.grade_answers(answers) correct_map = self.lcp.grade_answers(answers)
except StudentInputError as inst:
self.lcp = LoncapaProblem(filename, id=lcp_id, state=old_state)
traceback.print_exc()
# print {'error':sys.exc_info(),
# 'answers':answers,
# 'seed':self.lcp.seed,
# 'filename':self.lcp.filename}
return json.dumps({'success':inst.message})
except: except:
self.lcp = LoncapaProblem(filename, id=lcp_id, state=old_state) self.lcp = LoncapaProblem(filename, id=lcp_id, state=old_state)
traceback.print_exc() traceback.print_exc()
print {'error':sys.exc_info(), return json.dumps({'success':'Unknown Error'})
'answers':answers,
'seed':self.lcp.seed,
'filename':self.lcp.filename}
return json.dumps({'success':'syntax'})
self.attempts = self.attempts + 1 self.attempts = self.attempts + 1
self.lcp.done=True self.lcp.done=True
success = 'finished' success = 'correct'
for i in correct_map: for i in correct_map:
if correct_map[i]!='correct': if correct_map[i]!='correct':
success = 'errors' success = 'incorrect'
js=json.dumps({'correct_map' : correct_map, js=json.dumps({'correct_map' : correct_map,
'success' : success}) 'success' : success})
......
...@@ -32,8 +32,22 @@ class ModelsTest(unittest.TestCase): ...@@ -32,8 +32,22 @@ class ModelsTest(unittest.TestCase):
self.assertTrue(abs(calc.evaluator(variables, functions, "k*T/q-0.025"))<0.001) self.assertTrue(abs(calc.evaluator(variables, functions, "k*T/q-0.025"))<0.001)
exception_happened = False exception_happened = False
try: try:
evaluator({},{}, "5+7 QWSEKO") calc.evaluator({},{}, "5+7 QWSEKO")
except: except:
exception_happened = True exception_happened = True
self.assertTrue(exception_happened) self.assertTrue(exception_happened)
try:
calc.evaluator({'r1':5},{}, "r1+r2")
except calc.UndefinedVariable:
pass
self.assertEqual(calc.evaluator(variables, functions, "r1*r3"), 8.0)
exception_happened = False
try:
calc.evaluator(variables, functions, "r1*r3", cs=True)
except:
exception_happened = True
self.assertTrue(exception_happened)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment