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
from util import contextualize_text
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 eia
......@@ -42,6 +42,7 @@ response_types = {'numericalresponse':NumericalResponse,
'truefalseresponse':TrueFalseResponse,
'imageresponse':ImageResponse,
'optionresponse':OptionResponse,
'symbolicresponse':SymbolicResponse,
}
entry_types = ['textline', 'schematic', 'choicegroup','textbox','imageinput','optioninput']
solution_types = ['solution'] # extra things displayed after "show answers" is pressed
......@@ -55,6 +56,7 @@ html_transforms = {'problem': {'tag':'div'},
"externalresponse": {'tag':'span'},
"schematicresponse": {'tag':'span'},
"formularesponse": {'tag':'span'},
"symbolicresponse": {'tag':'span'},
"multiplechoiceresponse": {'tag':'span'},
"text": {'tag':'span'},
"math": {'tag':'span'},
......@@ -70,7 +72,7 @@ global_context={'random':random,
# These should be removed from HTML output, including all subelements
html_problem_semantics = ["responseparam", "answer", "script"]
# 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
## These should be transformed
......@@ -218,8 +220,10 @@ class LoncapaProblem(object):
#for script in tree.xpath('/problem/script'):
for script in tree.findall('.//script'):
if 'javascript' in script.get('type'): continue # skip javascript
if 'perl' in script.get('type'): continue # skip perl
stype = script.get('type')
if stype:
if 'javascript' in stype: continue # skip javascript
if 'perl' in stype: continue # skip perl
# TODO: evaluate only python
code = script.text
XMLESC = {"&apos;": "'", "&quot;": '"'}
......
......@@ -221,7 +221,10 @@ class NumericalResponse(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>
<startouttext/>
......@@ -263,6 +266,7 @@ def sympy_check2():
'''
def __init__(self, xml, context, system=None):
self.xml = xml
self.system = system
## CRITICAL TODO: Should cover all entrytypes
## NOTE: xpath will look at root of XML tree, not just
## what's in xml. @id=id keeps us in the right customresponse.
......@@ -271,8 +275,8 @@ def sympy_check2():
self.answer_ids += [x.get('id') for x in xml.findall('textbox')] # also allow textbox inputs
self.context = context
# if <customresponse> has an "expect" attribute then save that
self.expect = xml.get('expect')
# if <customresponse> has an "expect" (or "answer") attribute then save that
self.expect = xml.get('expect') or xml.get('answer')
self.myid = xml.get('id')
if settings.DEBUG:
......@@ -351,9 +355,14 @@ def sympy_check2():
'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
'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',
})
# 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
if type(self.code)==str:
try:
......@@ -381,7 +390,7 @@ def sympy_check2():
if settings.DEBUG:
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)
except Exception,err:
......@@ -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):
"""
Grade the student's input using an external server.
......@@ -502,6 +537,30 @@ class StudentInputError(Exception):
#-----------------------------------------------------------------------------
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):
self.xml = xml
self.correct_answer = contextualize_text(xml.get('answer'), context)
......
......@@ -30,7 +30,7 @@ from lxml import etree
import requests
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'
......@@ -143,11 +143,12 @@ class formula(object):
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 (=).
'''
def __init__(self,expr,asciimath=''):
def __init__(self,expr,asciimath='',options=None):
self.expr = expr.strip()
self.asciimath = asciimath
self.the_cmathml = None
self.the_sympy = None
self.options = options
def is_presentation_mathml(self):
return '<mstyle' in self.expr
......@@ -249,7 +250,8 @@ class formula(object):
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
......@@ -258,7 +260,11 @@ class formula(object):
if not self.is_mathml():
return my_sympify(self.expr)
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)
self.the_sympy = self.make_sympy(xml[0])
else:
......@@ -277,7 +283,7 @@ class formula(object):
# print "divide: arg0=%s, arg1=%s" % (args[0],args[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_minus(*args):
......@@ -317,7 +323,7 @@ class formula(object):
elif tag=='msup': return '^'.join([parsePresentationMathMLSymbol(y) for y in xml])
raise Exception,'[parsePresentationMathMLSymbol] unknown tag %s' % tag
# parser tree for content MathML
# parser tree for Content MathML
tag = gettag(xml)
print "tag = ",tag
......@@ -328,7 +334,13 @@ class formula(object):
if opstr in opdict:
op = opdict[opstr]
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:
raise Exception,'[formula]: unknown operator tag %s' % (opstr)
......@@ -351,7 +363,7 @@ class formula(object):
return float(xml.text)
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])
sym = sympy.Symbol(str(usym))
else:
......@@ -359,7 +371,11 @@ class formula(object):
if 'hat' in usym:
sym = my_sympify(usym)
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
else: # unknown tag
......@@ -462,3 +478,78 @@ def test4():
</math>
'''
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
#
# 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.
The input may be presentation MathML. Uses formula.
......@@ -148,22 +148,27 @@ def symmath_check(expect,ans,adict={},abname=''):
# msg += '<p/>adict=%s' % (repr(adict).replace('<','&lt;'))
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
try:
fexpect = my_sympify(str(expect))
fexpect = my_sympify(str(expect),matrix=do_matrix,do_qubit=do_qubit)
except Exception,err:
msg += '<p>Error %s in parsing OUR expected answer "%s"</p>' % (err,expect)
return {'ok':False,'msg':msg}
# if expected answer is a number, try parsing provided answer as a number also
try:
fans = my_sympify(str(ans))
fans = my_sympify(str(ans),matrix=do_matrix,do_qubit=do_qubit)
except Exception,err:
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:
return {'ok':True,'msg':msg}
else:
......@@ -175,10 +180,12 @@ def symmath_check(expect,ans,adict={},abname=''):
return {'ok':True,'msg':msg}
# convert mathml answer to formula
mmlbox = abname+'_fromjs'
if mmlbox in adict:
mmlans = adict[mmlbox]
f = formula(mmlans)
try:
if dynamath:
mmlans = dynamath[0]
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
# if DEBUG: msg += '<p/> mmlans=%s' % repr(mmlans).replace('<','&lt;')
......@@ -186,13 +193,20 @@ def symmath_check(expect,ans,adict={},abname=''):
fsym = f.sympy
msg += '<p>You entered: %s</p>' % to_latex(f.sympy)
except Exception,err:
msg += "<p>Error %s in converting to sympy</p>" % str(err).replace('<','&lt;')
if DEBUG: msg += "<p><pre>%s</pre></p>" % traceback.format_exc()
msg += "<p>Error %s in evaluating your expression '%s' as a valid equation</p>" % (str(err).replace('<','&lt;'),
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}
# compare with expected
if fexpect.is_number:
if fsym.is_number:
if hasattr(fexpect,'is_number') and fexpect.is_number:
if hasattr(fsym,'is_number') and fsym.is_number:
if abs(abs(fsym-fexpect)/fexpect)<threshold:
return {'ok':True,'msg':msg}
return {'ok':False,'msg':msg}
......@@ -225,12 +239,15 @@ def symmath_check(expect,ans,adict={},abname=''):
diff = None
if DEBUG:
msg += '<hr>'
msg += '<p><font color="blue">DEBUG messages:</p>'
msg += "<p>Got: %s</p>" % repr(fsym)
# 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" % str([type(x) for x in fexpect.atoms()]).replace('<','&lt;')
if diff:
msg += "<p>Difference: %s</p>" % to_latex(diff)
msg += '<hr>'
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