symmath_check.py 10.8 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 16
import traceback
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

#-----------------------------------------------------------------------------
# Check function interface, which takes pmathml input
145 146
#
# This is one of the main entry points to call.
147

148 149

def symmath_check(expect, ans, dynamath=None, options=None, debug=None):
150 151
    '''
    Check a symbolic mathematical expression using sympy.
152
    The input may be presentation MathML.  Uses formula.
153
    '''
154 155 156 157 158 159

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

    threshold = 1.0e-3
160 161 162 163 164 165
    DEBUG = debug

    # options
    do_matrix = 'matrix' in (options or '')
    do_qubit = 'qubit' in (options or '')
    do_imaginary = 'imaginary' in (options or '')
166 167 168

    # parse expected answer
    try:
169 170 171 172
        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}
173 174 175

    # if expected answer is a number, try parsing provided answer as a number also
    try:
176 177
        fans = my_sympify(str(ans), matrix=do_matrix, do_qubit=do_qubit)
    except Exception, err:
178 179
        fans = None

180 181 182
    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}
183 184
        else:
            msg += '<p>You entered: %s</p>' % to_latex(fans)
185
            return {'ok': False, 'msg': msg}
186

187
    if fexpect == fans:
188
        msg += '<p>You entered: %s</p>' % to_latex(fans)
189
        return {'ok': True, 'msg': msg}
190 191

    # convert mathml answer to formula
192 193 194
    try:
        if dynamath:
            mmlans = dynamath[0]
195
    except Exception, err:
196 197
        mmlans = None
    if not mmlans:
198 199 200
        return {'ok': False, 'msg': '[symmath_check] failed to get MathML for input; dynamath=%s' % dynamath}
    f = formula(mmlans, options=options)

201 202 203 204 205
    # 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)
206
    except Exception, err:
207
        log.exception("Error evaluating expression '%s' as a valid equation" % ans)
208
        msg += "<p>Error %s in evaluating your expression '%s' as a valid equation</p>" % (str(err).replace('<', '&lt;'),
209 210 211 212 213
                                                                                           ans)
        if DEBUG:
            msg += '<hr>'
            msg += '<p><font color="blue">DEBUG messages:</p>'
            msg += "<p><pre>%s</pre></p>" % traceback.format_exc()
214 215
            msg += '<p>cmathml=<pre>%s</pre></p>' % f.cmathml.replace('<', '&lt;')
            msg += '<p>pmathml=<pre>%s</pre></p>' % mmlans.replace('<', '&lt;')
216
            msg += '<hr>'
217
        return {'ok': False, 'msg': msg}
218 219

    # compare with expected
220 221 222 223 224
    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 226 227 228
        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;')
229
        return {'ok': False, 'msg': msg}
230

231 232
    if fexpect == fsym:
        return {'ok': True, 'msg': msg}
233

234
    if type(fexpect) == list:
235
        try:
236 237 238 239
            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}
240 241
        except sympy.ShapeError:
            msg += "<p>Error - your input vector or matrix has the wrong dimensions"
242 243 244
            return {'ok': False, 'msg': msg}
        except Exception, err:
            msg += "<p>Error %s in comparing expected (a list) and your answer</p>" % str(err).replace('<', '&lt;')
245
            if DEBUG: msg += "<p/><pre>%s</pre>" % traceback.format_exc()
246
            return {'ok': False, 'msg': msg}
247 248 249 250 251

    #diff = (fexpect-fsym).simplify()
    #fsym = fsym.simplify()
    #fexpect = fexpect.simplify()
    try:
252 253
        diff = (fexpect - fsym)
    except Exception, err:
254 255 256
        diff = None

    if DEBUG:
257 258
        msg += '<hr>'
        msg += '<p><font color="blue">DEBUG messages:</p>'
259 260
        msg += "<p>Got: %s</p>" % repr(fsym)
        # msg += "<p/>Got: %s" % str([type(x) for x in fsym.atoms()]).replace('<','&lt;')
261
        msg += "<p>Expecting: %s</p>" % repr(fexpect).replace('**', '^').replace('hat(I)', 'hat(i)')
262 263 264
        # msg += "<p/>Expecting: %s" % str([type(x) for x in fexpect.atoms()]).replace('<','&lt;')
        if diff:
            msg += "<p>Difference: %s</p>" % to_latex(diff)
265
        msg += '<hr>'
266

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

269 270 271
#-----------------------------------------------------------------------------
# tests

272

273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315
def sctest1():
    x = "1/2*(1+(k_e* Q* q)/(m *g *h^2))"
    y = '''
<math xmlns="http://www.w3.org/1998/Math/MathML">
  <mstyle displaystyle="true">
    <mfrac>
      <mn>1</mn>
      <mn>2</mn>
    </mfrac>
    <mrow>
      <mo>(</mo>
      <mn>1</mn>
      <mo>+</mo>
      <mfrac>
        <mrow>
          <msub>
            <mi>k</mi>
            <mi>e</mi>
          </msub>
          <mo>⋅</mo>
          <mi>Q</mi>
          <mo>⋅</mo>
          <mi>q</mi>
        </mrow>
        <mrow>
          <mi>m</mi>
          <mo>⋅</mo>
          <mrow>
            <mi>g</mi>
            <mo>⋅</mo>
          </mrow>
          <msup>
            <mi>h</mi>
            <mn>2</mn>
          </msup>
        </mrow>
      </mfrac>
      <mo>)</mo>
    </mrow>
  </mstyle>
</math>
'''.strip()
    z = "1/2(1+(k_e* Q* q)/(m *g *h^2))"
316
    r = sympy_check2(x, z, {'a': z, 'a_fromjs': y}, 'a')
317
    return r