Commit 2f33bce7 by ichuang

symbolic math: add <symbolicresponse> type; improve error handling in

symmath library; add options for matrices, qubit to symmath_check
parent 9dab2ce4
...@@ -26,7 +26,7 @@ from mako.template import Template ...@@ -26,7 +26,7 @@ from mako.template import Template
from util import contextualize_text from util import contextualize_text
import inputtypes import inputtypes
from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, StudentInputError, TrueFalseResponse, ExternalResponse,ImageResponse,OptionResponse from responsetypes import NumericalResponse, FormulaResponse, CustomResponse, SchematicResponse, MultipleChoiceResponse, StudentInputError, TrueFalseResponse, ExternalResponse,ImageResponse,OptionResponse, SymbolicResponse
import calc import calc
import eia import eia
...@@ -42,6 +42,7 @@ response_types = {'numericalresponse':NumericalResponse, ...@@ -42,6 +42,7 @@ response_types = {'numericalresponse':NumericalResponse,
'truefalseresponse':TrueFalseResponse, 'truefalseresponse':TrueFalseResponse,
'imageresponse':ImageResponse, 'imageresponse':ImageResponse,
'optionresponse':OptionResponse, 'optionresponse':OptionResponse,
'symbolicresponse':SymbolicResponse,
} }
entry_types = ['textline', 'schematic', 'choicegroup','textbox','imageinput','optioninput'] entry_types = ['textline', 'schematic', 'choicegroup','textbox','imageinput','optioninput']
solution_types = ['solution'] # extra things displayed after "show answers" is pressed solution_types = ['solution'] # extra things displayed after "show answers" is pressed
...@@ -55,6 +56,7 @@ html_transforms = {'problem': {'tag':'div'}, ...@@ -55,6 +56,7 @@ html_transforms = {'problem': {'tag':'div'},
"externalresponse": {'tag':'span'}, "externalresponse": {'tag':'span'},
"schematicresponse": {'tag':'span'}, "schematicresponse": {'tag':'span'},
"formularesponse": {'tag':'span'}, "formularesponse": {'tag':'span'},
"symbolicresponse": {'tag':'span'},
"multiplechoiceresponse": {'tag':'span'}, "multiplechoiceresponse": {'tag':'span'},
"text": {'tag':'span'}, "text": {'tag':'span'},
"math": {'tag':'span'}, "math": {'tag':'span'},
...@@ -70,7 +72,7 @@ global_context={'random':random, ...@@ -70,7 +72,7 @@ global_context={'random':random,
# These should be removed from HTML output, including all subelements # These should be removed from HTML output, including all subelements
html_problem_semantics = ["responseparam", "answer", "script"] html_problem_semantics = ["responseparam", "answer", "script"]
# These should be removed from HTML output, but keeping subelements # These should be removed from HTML output, but keeping subelements
html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formularesponse", "text","externalresponse"] html_skip = ["numericalresponse", "customresponse", "schematicresponse", "formularesponse", "text","externalresponse",'symbolicresponse']
# removed in MC # removed in MC
## These should be transformed ## These should be transformed
...@@ -218,8 +220,10 @@ class LoncapaProblem(object): ...@@ -218,8 +220,10 @@ class LoncapaProblem(object):
#for script in tree.xpath('/problem/script'): #for script in tree.xpath('/problem/script'):
for script in tree.findall('.//script'): for script in tree.findall('.//script'):
if 'javascript' in script.get('type'): continue # skip javascript stype = script.get('type')
if 'perl' in script.get('type'): continue # skip perl if stype:
if 'javascript' in stype: continue # skip javascript
if 'perl' in stype: continue # skip perl
# TODO: evaluate only python # TODO: evaluate only python
code = script.text code = script.text
XMLESC = {"&apos;": "'", "&quot;": '"'} XMLESC = {"&apos;": "'", "&quot;": '"'}
......
...@@ -221,7 +221,10 @@ class NumericalResponse(GenericResponse): ...@@ -221,7 +221,10 @@ class NumericalResponse(GenericResponse):
class CustomResponse(GenericResponse): class CustomResponse(GenericResponse):
''' '''
Custom response. The python code to be run should be in <answer>...</answer>. Example: Custom response. The python code to be run should be in <answer>...</answer>
or in a <script>...</script>
Example:
<customresponse> <customresponse>
<startouttext/> <startouttext/>
...@@ -263,6 +266,7 @@ def sympy_check2(): ...@@ -263,6 +266,7 @@ def sympy_check2():
''' '''
def __init__(self, xml, context, system=None): def __init__(self, xml, context, system=None):
self.xml = xml self.xml = xml
self.system = system
## CRITICAL TODO: Should cover all entrytypes ## CRITICAL TODO: Should cover all entrytypes
## NOTE: xpath will look at root of XML tree, not just ## NOTE: xpath will look at root of XML tree, not just
## what's in xml. @id=id keeps us in the right customresponse. ## what's in xml. @id=id keeps us in the right customresponse.
...@@ -271,8 +275,8 @@ def sympy_check2(): ...@@ -271,8 +275,8 @@ def sympy_check2():
self.answer_ids += [x.get('id') for x in xml.findall('textbox')] # also allow textbox inputs self.answer_ids += [x.get('id') for x in xml.findall('textbox')] # also allow textbox inputs
self.context = context self.context = context
# if <customresponse> has an "expect" attribute then save that # if <customresponse> has an "expect" (or "answer") attribute then save that
self.expect = xml.get('expect') self.expect = xml.get('expect') or xml.get('answer')
self.myid = xml.get('id') self.myid = xml.get('id')
if settings.DEBUG: if settings.DEBUG:
...@@ -351,9 +355,14 @@ def sympy_check2(): ...@@ -351,9 +355,14 @@ def sympy_check2():
'answers':student_answers, # dict of student's responses, with keys being entry box IDs 'answers':student_answers, # dict of student's responses, with keys being entry box IDs
'correct':correct, # the list to be filled in by the check function 'correct':correct, # the list to be filled in by the check function
'messages':messages, # the list of messages to be filled in by the check function 'messages':messages, # the list of messages to be filled in by the check function
'options':self.xml.get('options'), # any options to be passed to the cfn
'testdat':'hello world', 'testdat':'hello world',
}) })
# pass self.system.debug to cfn
# if hasattr(self.system,'debug'): self.context['debug'] = self.system.debug
self.context['debug'] = settings.DEBUG
# exec the check function # exec the check function
if type(self.code)==str: if type(self.code)==str:
try: try:
...@@ -381,7 +390,7 @@ def sympy_check2(): ...@@ -381,7 +390,7 @@ def sympy_check2():
if settings.DEBUG: if settings.DEBUG:
log.debug('[courseware.capa.responsetypes.customresponse] answer_given=%s' % answer_given) log.debug('[courseware.capa.responsetypes.customresponse] answer_given=%s' % answer_given)
# log.info('nargs=%d, args=%s' % (nargs,args)) log.info('nargs=%d, args=%s, kwargs=%s' % (nargs,args,kwargs))
ret = fn(*args[:nargs],**kwargs) ret = fn(*args[:nargs],**kwargs)
except Exception,err: except Exception,err:
...@@ -436,6 +445,32 @@ def sympy_check2(): ...@@ -436,6 +445,32 @@ def sympy_check2():
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
class SymbolicResponse(CustomResponse):
"""
Symbolic math response checking, using symmath library.
Example:
<problem>
<text>Compute \[ \exp\left(-i \frac{\theta}{2} \left[ \begin{matrix} 0 & 1 \\ 1 & 0 \end{matrix} \right] \right) \]
and give the resulting \(2\times 2\) matrix: <br/>
<symbolicresponse answer="">
<textline size="40" math="1" />
</symbolicresponse>
<br/>
Your input should be typed in as a list of lists, eg <tt>[[1,2],[3,4]]</tt>.
</text>
</problem>
"""
def __init__(self, xml, context, system=None):
xml.set('cfn','symmath_check')
code = "from symmath import *"
exec code in context,context
CustomResponse.__init__(self,xml,context,system)
#-----------------------------------------------------------------------------
class ExternalResponse(GenericResponse): class ExternalResponse(GenericResponse):
""" """
Grade the student's input using an external server. Grade the student's input using an external server.
...@@ -502,6 +537,30 @@ class StudentInputError(Exception): ...@@ -502,6 +537,30 @@ class StudentInputError(Exception):
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
class FormulaResponse(GenericResponse): class FormulaResponse(GenericResponse):
'''
Checking of symbolic math response using numerical sampling.
Example:
<problem>
<script type="loncapa/python">
I = "m*c^2"
</script>
<text>
<br/>
Give an equation for the relativistic energy of an object with mass m.
</text>
<formularesponse type="cs" samples="m,c@1,2:3,4#10" answer="$I">
<responseparam description="Numerical Tolerance" type="tolerance"
default="0.00001" name="tol" />
<textline size="40" math="1" />
</formularesponse>
</problem>
'''
def __init__(self, xml, context, system=None): def __init__(self, xml, context, system=None):
self.xml = xml self.xml = xml
self.correct_answer = contextualize_text(xml.get('answer'), context) self.correct_answer = contextualize_text(xml.get('answer'), context)
......
...@@ -30,7 +30,7 @@ from lxml import etree ...@@ -30,7 +30,7 @@ from lxml import etree
import requests import requests
from copy import deepcopy from copy import deepcopy
print "[lib.sympy_check.formula] Warning: Dark code. Needs review before enabling in prod." print "[lib.symmath.formula] Warning: Dark code. Needs review before enabling in prod."
os.environ['PYTHONIOENCODING'] = 'utf-8' os.environ['PYTHONIOENCODING'] = 'utf-8'
...@@ -143,11 +143,12 @@ class formula(object): ...@@ -143,11 +143,12 @@ class formula(object):
Representation of a mathematical formula object. Accepts mathml math expression for constructing, Representation of a mathematical formula object. Accepts mathml math expression for constructing,
and can produce sympy translation. The formula may or may not include an assignment (=). and can produce sympy translation. The formula may or may not include an assignment (=).
''' '''
def __init__(self,expr,asciimath=''): def __init__(self,expr,asciimath='',options=None):
self.expr = expr.strip() self.expr = expr.strip()
self.asciimath = asciimath self.asciimath = asciimath
self.the_cmathml = None self.the_cmathml = None
self.the_sympy = None self.the_sympy = None
self.options = options
def is_presentation_mathml(self): def is_presentation_mathml(self):
return '<mstyle' in self.expr return '<mstyle' in self.expr
...@@ -249,7 +250,8 @@ class formula(object): ...@@ -249,7 +250,8 @@ class formula(object):
def make_sympy(self,xml=None): def make_sympy(self,xml=None):
''' '''
Return sympy expression for the math formula Return sympy expression for the math formula.
The math formula is converted to Content MathML then that is parsed.
''' '''
if self.the_sympy: return self.the_sympy if self.the_sympy: return self.the_sympy
...@@ -258,7 +260,11 @@ class formula(object): ...@@ -258,7 +260,11 @@ class formula(object):
if not self.is_mathml(): if not self.is_mathml():
return my_sympify(self.expr) return my_sympify(self.expr)
if self.is_presentation_mathml(): if self.is_presentation_mathml():
xml = etree.fromstring(str(self.cmathml)) try:
cmml = self.cmathml
xml = etree.fromstring(str(cmml))
except Exception,err:
raise Exception,'Err %s while converting cmathml to xml; cmml=%s' % (err,cmml)
xml = self.fix_greek_in_mathml(xml) xml = self.fix_greek_in_mathml(xml)
self.the_sympy = self.make_sympy(xml[0]) self.the_sympy = self.make_sympy(xml[0])
else: else:
...@@ -277,7 +283,7 @@ class formula(object): ...@@ -277,7 +283,7 @@ class formula(object):
# print "divide: arg0=%s, arg1=%s" % (args[0],args[1]) # print "divide: arg0=%s, arg1=%s" % (args[0],args[1])
return sympy.Mul(args[0],sympy.Pow(args[1],-1)) return sympy.Mul(args[0],sympy.Pow(args[1],-1))
def op_plus(*args): return sum(args) def op_plus(*args): return args[0] if len(args)==1 else op_plus(*args[:-1])+args[-1]
def op_times(*args): return reduce(operator.mul,args) def op_times(*args): return reduce(operator.mul,args)
def op_minus(*args): def op_minus(*args):
...@@ -317,7 +323,7 @@ class formula(object): ...@@ -317,7 +323,7 @@ class formula(object):
elif tag=='msup': return '^'.join([parsePresentationMathMLSymbol(y) for y in xml]) elif tag=='msup': return '^'.join([parsePresentationMathMLSymbol(y) for y in xml])
raise Exception,'[parsePresentationMathMLSymbol] unknown tag %s' % tag raise Exception,'[parsePresentationMathMLSymbol] unknown tag %s' % tag
# parser tree for content MathML # parser tree for Content MathML
tag = gettag(xml) tag = gettag(xml)
print "tag = ",tag print "tag = ",tag
...@@ -328,7 +334,13 @@ class formula(object): ...@@ -328,7 +334,13 @@ class formula(object):
if opstr in opdict: if opstr in opdict:
op = opdict[opstr] op = opdict[opstr]
args = [ self.make_sympy(x) for x in xml[1:]] args = [ self.make_sympy(x) for x in xml[1:]]
return op(*args) try:
res = op(*args)
except Exception,err:
self.args = args
self.op = op
raise Exception,'[formula] error=%s failed to apply %s to args=%s' % (err,opstr,args)
return res
else: else:
raise Exception,'[formula]: unknown operator tag %s' % (opstr) raise Exception,'[formula]: unknown operator tag %s' % (opstr)
...@@ -351,7 +363,7 @@ class formula(object): ...@@ -351,7 +363,7 @@ class formula(object):
return float(xml.text) return float(xml.text)
elif tag=='ci': # variable (symbol) elif tag=='ci': # variable (symbol)
if len(xml)>0 and (gettag(xml[0])=='msub' or gettag(xml[0])=='msup'): if len(xml)>0 and (gettag(xml[0])=='msub' or gettag(xml[0])=='msup'): # subscript or superscript
usym = parsePresentationMathMLSymbol(xml[0]) usym = parsePresentationMathMLSymbol(xml[0])
sym = sympy.Symbol(str(usym)) sym = sympy.Symbol(str(usym))
else: else:
...@@ -359,7 +371,11 @@ class formula(object): ...@@ -359,7 +371,11 @@ class formula(object):
if 'hat' in usym: if 'hat' in usym:
sym = my_sympify(usym) sym = my_sympify(usym)
else: else:
sym = sympy.Symbol(str(usym)) if usym=='i': print "options=",self.options
if usym=='i' and 'imaginary' in self.options: # i = sqrt(-1)
sym = sympy.I
else:
sym = sympy.Symbol(str(usym))
return sym return sym
else: # unknown tag else: # unknown tag
...@@ -462,3 +478,78 @@ def test4(): ...@@ -462,3 +478,78 @@ def test4():
</math> </math>
''' '''
return formula(xmlstr) return formula(xmlstr)
def test5(): # sum of two matrices
xmlstr = u'''
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mrow>
<mi>cos</mi>
<mrow>
<mo>(</mo>
<mi>&#x3B8;</mi>
<mo>)</mo>
</mrow>
</mrow>
<mo>&#x22C5;</mo>
<mrow>
<mo>[</mo>
<mtable>
<mtr>
<mtd>
<mn>1</mn>
</mtd>
<mtd>
<mn>0</mn>
</mtd>
</mtr>
<mtr>
<mtd>
<mn>0</mn>
</mtd>
<mtd>
<mn>1</mn>
</mtd>
</mtr>
</mtable>
<mo>]</mo>
</mrow>
<mo>+</mo>
<mrow>
<mo>[</mo>
<mtable>
<mtr>
<mtd>
<mn>0</mn>
</mtd>
<mtd>
<mn>1</mn>
</mtd>
</mtr>
<mtr>
<mtd>
<mn>1</mn>
</mtd>
<mtd>
<mn>0</mn>
</mtd>
</mtr>
</mtable>
<mo>]</mo>
</mrow>
</mstyle>
</math>
'''
return formula(xmlstr)
def test6(): # imaginary numbers
xmlstr = u'''
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mstyle displaystyle="true">
<mn>1</mn>
<mo>+</mo>
<mi>i</mi>
</mstyle>
</math>
'''
return formula(xmlstr,options='imaginaryi')
...@@ -137,7 +137,7 @@ def check(expect,given,numerical=False,matrix=False,normphase=False,abcsym=False ...@@ -137,7 +137,7 @@ def check(expect,given,numerical=False,matrix=False,normphase=False,abcsym=False
# #
# This is one of the main entry points to call. # This is one of the main entry points to call.
def symmath_check(expect,ans,adict={},abname=''): def symmath_check(expect,ans,dynamath=None,options=None,debug=None):
''' '''
Check a symbolic mathematical expression using sympy. Check a symbolic mathematical expression using sympy.
The input may be presentation MathML. Uses formula. The input may be presentation MathML. Uses formula.
...@@ -148,22 +148,27 @@ def symmath_check(expect,ans,adict={},abname=''): ...@@ -148,22 +148,27 @@ def symmath_check(expect,ans,adict={},abname=''):
# msg += '<p/>adict=%s' % (repr(adict).replace('<','&lt;')) # msg += '<p/>adict=%s' % (repr(adict).replace('<','&lt;'))
threshold = 1.0e-3 threshold = 1.0e-3
DEBUG = True DEBUG = debug
# options
do_matrix = 'matrix' in (options or '')
do_qubit = 'qubit' in (options or '')
do_imaginary = 'imaginary' in (options or '')
# parse expected answer # parse expected answer
try: try:
fexpect = my_sympify(str(expect)) fexpect = my_sympify(str(expect),matrix=do_matrix,do_qubit=do_qubit)
except Exception,err: except Exception,err:
msg += '<p>Error %s in parsing OUR expected answer "%s"</p>' % (err,expect) msg += '<p>Error %s in parsing OUR expected answer "%s"</p>' % (err,expect)
return {'ok':False,'msg':msg} return {'ok':False,'msg':msg}
# if expected answer is a number, try parsing provided answer as a number also # if expected answer is a number, try parsing provided answer as a number also
try: try:
fans = my_sympify(str(ans)) fans = my_sympify(str(ans),matrix=do_matrix,do_qubit=do_qubit)
except Exception,err: except Exception,err:
fans = None fans = None
if fexpect.is_number and fans and fans.is_number: if hasattr(fexpect,'is_number') and fexpect.is_number and fans and hasattr(fans,'is_number') and fans.is_number:
if abs(abs(fans-fexpect)/fexpect)<threshold: if abs(abs(fans-fexpect)/fexpect)<threshold:
return {'ok':True,'msg':msg} return {'ok':True,'msg':msg}
else: else:
...@@ -175,10 +180,12 @@ def symmath_check(expect,ans,adict={},abname=''): ...@@ -175,10 +180,12 @@ def symmath_check(expect,ans,adict={},abname=''):
return {'ok':True,'msg':msg} return {'ok':True,'msg':msg}
# convert mathml answer to formula # convert mathml answer to formula
mmlbox = abname+'_fromjs' try:
if mmlbox in adict: if dynamath:
mmlans = adict[mmlbox] mmlans = dynamath[0]
f = formula(mmlans) except Exception,err:
return {'ok':False,'msg':'[symmath_check] failed to get MathML for input; dynamath=%s' % dynamath}
f = formula(mmlans,options=options)
# get sympy representation of the formula # get sympy representation of the formula
# if DEBUG: msg += '<p/> mmlans=%s' % repr(mmlans).replace('<','&lt;') # if DEBUG: msg += '<p/> mmlans=%s' % repr(mmlans).replace('<','&lt;')
...@@ -186,13 +193,20 @@ def symmath_check(expect,ans,adict={},abname=''): ...@@ -186,13 +193,20 @@ def symmath_check(expect,ans,adict={},abname=''):
fsym = f.sympy fsym = f.sympy
msg += '<p>You entered: %s</p>' % to_latex(f.sympy) msg += '<p>You entered: %s</p>' % to_latex(f.sympy)
except Exception,err: except Exception,err:
msg += "<p>Error %s in converting to sympy</p>" % str(err).replace('<','&lt;') msg += "<p>Error %s in evaluating your expression '%s' as a valid equation</p>" % (str(err).replace('<','&lt;'),
if DEBUG: msg += "<p><pre>%s</pre></p>" % traceback.format_exc() ans)
if DEBUG:
msg += '<hr>'
msg += '<p><font color="blue">DEBUG messages:</p>'
msg += "<p><pre>%s</pre></p>" % traceback.format_exc()
msg += '<p>cmathml=<pre>%s</pre></p>' % f.cmathml.replace('<','&lt;')
msg += '<p>pmathml=<pre>%s</pre></p>' % mmlans.replace('<','&lt;')
msg += '<hr>'
return {'ok':False,'msg':msg} return {'ok':False,'msg':msg}
# compare with expected # compare with expected
if fexpect.is_number: if hasattr(fexpect,'is_number') and fexpect.is_number:
if fsym.is_number: if hasattr(fsym,'is_number') and fsym.is_number:
if abs(abs(fsym-fexpect)/fexpect)<threshold: if abs(abs(fsym-fexpect)/fexpect)<threshold:
return {'ok':True,'msg':msg} return {'ok':True,'msg':msg}
return {'ok':False,'msg':msg} return {'ok':False,'msg':msg}
...@@ -225,12 +239,15 @@ def symmath_check(expect,ans,adict={},abname=''): ...@@ -225,12 +239,15 @@ def symmath_check(expect,ans,adict={},abname=''):
diff = None diff = None
if DEBUG: if DEBUG:
msg += '<hr>'
msg += '<p><font color="blue">DEBUG messages:</p>'
msg += "<p>Got: %s</p>" % repr(fsym) msg += "<p>Got: %s</p>" % repr(fsym)
# msg += "<p/>Got: %s" % str([type(x) for x in fsym.atoms()]).replace('<','&lt;') # msg += "<p/>Got: %s" % str([type(x) for x in fsym.atoms()]).replace('<','&lt;')
msg += "<p>Expecting: %s</p>" % repr(fexpect).replace('**','^').replace('hat(I)','hat(i)') msg += "<p>Expecting: %s</p>" % repr(fexpect).replace('**','^').replace('hat(I)','hat(i)')
# msg += "<p/>Expecting: %s" % str([type(x) for x in fexpect.atoms()]).replace('<','&lt;') # msg += "<p/>Expecting: %s" % str([type(x) for x in fexpect.atoms()]).replace('<','&lt;')
if diff: if diff:
msg += "<p>Difference: %s</p>" % to_latex(diff) msg += "<p>Difference: %s</p>" % to_latex(diff)
msg += '<hr>'
return {'ok':False,'msg':msg,'ex':fexpect,'got':fsym} return {'ok':False,'msg':msg,'ex':fexpect,'got':fsym}
......
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