symmath_check.py 12.3 KB
Newer Older
1 2 3
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
4 5
# File:   symmath_check.py
# Date:   02-May-12 (creation)
6
#
7
# Symbolic mathematical expression checker for edX.  Uses sympy to check for expression equality.
8 9 10
#
# Takes in math expressions given as Presentation MathML (from ASCIIMathML), converts to Content MathML using SnuggleTeX

11 12 13 14
import os
import sys
import string
import re
15
import traceback
16
from .formula import *
17 18 19
import logging

log = logging.getLogger(__name__)
20 21 22

#-----------------------------------------------------------------------------
# check function interface
23 24
#
# This is one of the main entry points to call.
25

26 27

def symmath_check_simple(expect, ans, adict={}, symtab=None, extra_options=None):
28 29
    '''
    Check a symbolic mathematical expression using sympy.
30
    The input is an ascii string (not MathML) converted to math using sympy.sympify.
31
    '''
32

33
    options = {'__MATRIX__': False, '__ABC__': False, '__LOWER__': False}
34 35 36
    if extra_options: options.update(extra_options)
    for op in options:				# find options in expect string
        if op in expect:
37
            expect = expect.replace(op, '')
38
            options[op] = True
39
    expect = expect.replace('__OR__', '__or__')	 # backwards compatibility
40 41 42 43 44 45

    if options['__LOWER__']:
        expect = expect.lower()
        ans = ans.lower()

    try:
46
        ret = check(expect, ans,
47 48 49 50 51 52
                    matrix=options['__MATRIX__'],
                    abcsym=options['__ABC__'],
                    symtab=symtab,
                    )
    except Exception, err:
        return {'ok': False,
53
                'msg': 'Error %s<br/>Failed in evaluating check(%s,%s)' % (err, expect, ans)
54 55 56 57 58 59
                }
    return ret

#-----------------------------------------------------------------------------
# pretty generic checking function

60 61

def check(expect, given, numerical=False, matrix=False, normphase=False, abcsym=False, do_qubit=True, symtab=None, dosimplify=False):
62 63 64 65 66 67 68
    """
    Returns dict with

      'ok': True if check is good, False otherwise
      'msg': response message (in HTML)

    "expect" may have multiple possible acceptable answers, separated by "__OR__"
69

70 71 72 73 74
    """

    if "__or__" in expect:			# if multiple acceptable answers
        eset = expect.split('__or__')		# then see if any match
        for eone in eset:
75
            ret = check(eone, given, numerical, matrix, normphase, abcsym, do_qubit, symtab, dosimplify)
76 77 78 79 80 81
            if ret['ok']:
                return ret
        return ret

    flags = {}
    if "__autonorm__" in expect:
82 83
        flags['autonorm'] = True
        expect = expect.replace('__autonorm__', '')
84 85 86 87
        matrix = True

    threshold = 1.0e-3
    if "__threshold__" in expect:
88
        (expect, st) = expect.split('__threshold__')
89
        threshold = float(st)
90
        numerical = True
91

92
    if str(given) == '' and not (str(expect) == ''):
93 94 95
        return {'ok': False, 'msg': ''}

    try:
96 97 98
        xgiven = my_sympify(given, normphase, matrix, do_qubit=do_qubit, abcsym=abcsym, symtab=symtab)
    except Exception, err:
        return {'ok': False, 'msg': 'Error %s<br/> in evaluating your expression "%s"' % (err, given)}
99 100

    try:
101 102 103
        xexpect = my_sympify(expect, normphase, matrix, do_qubit=do_qubit, abcsym=abcsym, symtab=symtab)
    except Exception, err:
        return {'ok': False, 'msg': 'Error %s<br/> in evaluating OUR expression "%s"' % (err, expect)}
104

105
    if 'autonorm' in flags:	 # normalize trace of matrices
106 107 108
        try:
            xgiven /= xgiven.trace()
        except Exception, err:
109
            return {'ok': False, 'msg': 'Error %s<br/> in normalizing trace of your expression %s' % (err, to_latex(xgiven))}
110 111 112
        try:
            xexpect /= xexpect.trace()
        except Exception, err:
113
            return {'ok': False, 'msg': 'Error %s<br/> in normalizing trace of OUR expression %s' % (err, to_latex(xexpect))}
114 115 116 117 118 119 120

    msg = 'Your expression was evaluated as ' + to_latex(xgiven)
    # msg += '<br/>Expected ' + to_latex(xexpect)

    # msg += "<br/>flags=%s" % flags

    if matrix and numerical:
121 122
        xgiven = my_evalf(xgiven, chop=True)
        dm = my_evalf(sympy.Matrix(xexpect) - sympy.Matrix(xgiven), chop=True)
123
        msg += " = " + to_latex(xgiven)
124 125
        if abs(dm.vec().norm().evalf()) < threshold:
            return {'ok': True, 'msg': msg}
126 127 128 129 130
        else:
            pass
            #msg += "dm = " + to_latex(dm) + " diff = " + str(abs(dm.vec().norm().evalf()))
            #msg += "expect = " + to_latex(xexpect)
    elif dosimplify:
131 132
        if (sympy.simplify(xexpect) == sympy.simplify(xgiven)):
            return {'ok': True, 'msg': msg}
133
    elif numerical:
134 135 136 137
        if (abs((xexpect - xgiven).evalf(chop=True)) < threshold):
            return {'ok': True, 'msg': msg}
    elif (xexpect == xgiven):
        return {'ok': True, 'msg': msg}
138 139 140

    #msg += "<p/>expect='%s', given='%s'" % (expect,given)	# debugging
    # msg += "<p/> dot test " + to_latex(dot(sympy.Symbol('x'),sympy.Symbol('y')))
141
    return {'ok': False, 'msg': msg}
142 143

#-----------------------------------------------------------------------------
144 145
# helper function to convert all <p> to <span class='inline-error'>

Calen Pennington committed
146

147 148 149 150 151
def make_error_message(msg):
    # msg = msg.replace('<p>','<p><span class="inline-error">').replace('</p>','</span></p>')
    msg = '<div class="capa_alert">%s</div>' % msg
    return msg

152 153 154 155 156 157
def is_within_tolerance(expected, actual, tolerance):
    if expected == 0:
        return (abs(actual) < tolerance)
    else:
        return (abs(abs(actual - expected) / expected) < tolerance)

158
#-----------------------------------------------------------------------------
159
# Check function interface, which takes pmathml input
160 161
#
# This is one of the main entry points to call.
162

Calen Pennington committed
163

164
def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None):
165 166
    '''
    Check a symbolic mathematical expression using sympy.
167
    The input may be presentation MathML.  Uses formula.
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187

    This is the default Symbolic Response checking function

    Desc of args:
    expect is a sympy string representing the correct answer. It is interpreted
     using my_sympify (from formula.py), which reads strings as sympy input
     (e.g. 'integrate(x^2, (x,1,2))' would be valid, and evaluate to give 1.5)

    ans is student-typed answer. It is expected to be ascii math, but the code
     below would support a sympy string.

    dynamath is the PMathML string converted by MathJax. It is used if
     evaluation with ans is not sufficient.

    options is a string with these possible substrings, set as an xml property
     of the problem:
     -matrix - make a sympy matrix, rather than a list of lists, if possible
     -qubit - passed to my_sympify
     -imaginary - used in formla, presumably to signal to use i as sqrt(-1)?
     -numerical - force numerical comparison.
188
    '''
189 190 191 192 193

    msg = ''
    # msg += '<p/>abname=%s' % abname
    # msg += '<p/>adict=%s' % (repr(adict).replace('<','&lt;'))

Calen Pennington committed
194
    threshold = 1.0e-3   # for numerical comparison (also with matrices)
195 196
    DEBUG = debug

197
    if xml is not None:
Calen Pennington committed
198 199
        DEBUG = xml.get('debug', False)  	# override debug flag using attribute in symbolicmath xml
        if DEBUG in ['0', 'False']:
200 201
            DEBUG = False

202 203 204 205
    # options
    do_matrix = 'matrix' in (options or '')
    do_qubit = 'qubit' in (options or '')
    do_imaginary = 'imaginary' in (options or '')
206
    do_numerical = 'numerical' in (options or '')
207 208 209

    # parse expected answer
    try:
210 211 212
        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)
213
        return {'ok': False, 'msg': make_error_message(msg)}
214

215 216

    ###### Sympy input #######
217 218
    # if expected answer is a number, try parsing provided answer as a number also
    try:
219 220
        fans = my_sympify(str(ans), matrix=do_matrix, do_qubit=do_qubit)
    except Exception, err:
221 222
        fans = None

223
    # do a numerical comparison if both expected and answer are numbers
224
    if (hasattr(fexpect, 'is_number') and fexpect.is_number 
225
        and hasattr(fans, 'is_number') and fans.is_number):
226
        if is_within_tolerance(fexpect, fans, threshold):
227
            return {'ok': True, 'msg': msg}
228 229
        else:
            msg += '<p>You entered: %s</p>' % to_latex(fans)
230
            return {'ok': False, 'msg': msg}
231

232
    if do_numerical:		# numerical answer expected - force numerical comparison
233
        if is_within_tolerance(fexpect, fans, threshold):
234 235 236 237 238
            return {'ok': True, 'msg': msg}
        else:
            msg += '<p>You entered: %s (note that a numerical answer is expected)</p>' % to_latex(fans)
            return {'ok': False, 'msg': msg}

239
    if fexpect == fans:
240
        msg += '<p>You entered: %s</p>' % to_latex(fans)
241
        return {'ok': True, 'msg': msg}
242

243 244

    ###### PMathML input ######
245
    # convert mathml answer to formula
246
    try:
247
        mmlans = dynamath[0] if dynamath else None
248
    except Exception, err:
249 250
        mmlans = None
    if not mmlans:
251
        return {'ok': False, 'msg': '[symmath_check] failed to get MathML for input; dynamath=%s' % dynamath}
252

253 254
    f = formula(mmlans, options=options)

255 256 257 258 259
    # get sympy representation of the formula
    # if DEBUG: msg += '<p/> mmlans=%s' % repr(mmlans).replace('<','&lt;')
    try:
        fsym = f.sympy
        msg += '<p>You entered: %s</p>' % to_latex(f.sympy)
260
    except Exception, err:
261
        log.exception("Error evaluating expression '%s' as a valid equation" % ans)
262 263 264
        msg += "<p>Error in evaluating your expression '%s' as a valid equation</p>" % (ans)
        if "Illegal math" in str(err):
            msg += "<p>Illegal math expression</p>"
265
        if DEBUG:
266
            msg += 'Error: %s' % str(err).replace('<', '&lt;')
267 268 269
            msg += '<hr>'
            msg += '<p><font color="blue">DEBUG messages:</p>'
            msg += "<p><pre>%s</pre></p>" % traceback.format_exc()
270 271
            msg += '<p>cmathml=<pre>%s</pre></p>' % f.cmathml.replace('<', '&lt;')
            msg += '<p>pmathml=<pre>%s</pre></p>' % mmlans.replace('<', '&lt;')
272
            msg += '<hr>'
273
        return {'ok': False, 'msg': make_error_message(msg)}
274

275
    # do numerical comparison with expected
276 277 278 279 280
    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}
281 282 283 284
        msg += "<p>Expecting a numerical answer!</p>"
        msg += "<p>given = %s</p>" % repr(ans)
        msg += "<p>fsym = %s</p>" % repr(fsym)
        # msg += "<p>cmathml = <pre>%s</pre></p>" % str(f.cmathml).replace('<','&lt;')
285
        return {'ok': False, 'msg': make_error_message(msg)}
286

287 288 289 290
    # Here is a good spot for adding calls to X.simplify() or X.expand(),
    #  allowing equivalence over binomial expansion or trig identities

    # exactly the same?
291 292
    if fexpect == fsym:
        return {'ok': True, 'msg': msg}
293

294
    if type(fexpect) == list:
295
        try:
296 297 298 299
            xgiven = my_evalf(fsym, chop=True)
            dm = my_evalf(sympy.Matrix(fexpect) - sympy.Matrix(xgiven), chop=True)
            if abs(dm.vec().norm().evalf()) < threshold:
                return {'ok': True, 'msg': msg}
300 301
        except sympy.ShapeError:
            msg += "<p>Error - your input vector or matrix has the wrong dimensions"
302
            return {'ok': False, 'msg': make_error_message(msg)}
303 304
        except Exception, err:
            msg += "<p>Error %s in comparing expected (a list) and your answer</p>" % str(err).replace('<', '&lt;')
305
            if DEBUG: msg += "<p/><pre>%s</pre>" % traceback.format_exc()
306
            return {'ok': False, 'msg': make_error_message(msg)}
307 308 309 310 311

    #diff = (fexpect-fsym).simplify()
    #fsym = fsym.simplify()
    #fexpect = fexpect.simplify()
    try:
312 313
        diff = (fexpect - fsym)
    except Exception, err:
314 315 316
        diff = None

    if DEBUG:
317 318
        msg += '<hr>'
        msg += '<p><font color="blue">DEBUG messages:</p>'
319 320
        msg += "<p>Got: %s</p>" % repr(fsym)
        # msg += "<p/>Got: %s" % str([type(x) for x in fsym.atoms()]).replace('<','&lt;')
321
        msg += "<p>Expecting: %s</p>" % repr(fexpect).replace('**', '^').replace('hat(I)', 'hat(i)')
322 323 324
        # msg += "<p/>Expecting: %s" % str([type(x) for x in fexpect.atoms()]).replace('<','&lt;')
        if diff:
            msg += "<p>Difference: %s</p>" % to_latex(diff)
325
        msg += '<hr>'
326

327
    return {'ok': False, 'msg': msg, 'ex': fexpect, 'got': fsym}