symbolic math: add <symbolicresponse> type; improve error handling in
symmath library; add options for matrices, qubit to symmath_check
This commit is contained in:
@@ -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 = {"'": "'", """: '"'}
|
||||
|
||||
@@ -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>θ</mi>
|
||||
<mo>)</mo>
|
||||
</mrow>
|
||||
</mrow>
|
||||
<mo>⋅</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('<','<'))
|
||||
|
||||
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('<','<')
|
||||
@@ -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('<','<')
|
||||
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('<','<'),
|
||||
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('<','<')
|
||||
msg += '<p>pmathml=<pre>%s</pre></p>' % mmlans.replace('<','<')
|
||||
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('<','<')
|
||||
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('<','<')
|
||||
if diff:
|
||||
msg += "<p>Difference: %s</p>" % to_latex(diff)
|
||||
msg += '<hr>'
|
||||
|
||||
return {'ok':False,'msg':msg,'ex':fexpect,'got':fsym}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user