From 577d8815a36b72edf8f7614c6fb7139e4258340e Mon Sep 17 00:00:00 2001
From: Isaac Chuang
Date: Fri, 11 May 2012 16:55:02 -0400
Subject: [PATCH] Adding sympy check. Needs to be code reviewed before enabling
in prod.
---
lib/sympy_check/__init__.py | 0
lib/sympy_check/formula.py | 461 ++++++++++++++++++++++++++++++++
lib/sympy_check/sympy_check2.py | 271 +++++++++++++++++++
3 files changed, 732 insertions(+)
create mode 100644 lib/sympy_check/__init__.py
create mode 100644 lib/sympy_check/formula.py
create mode 100644 lib/sympy_check/sympy_check2.py
diff --git a/lib/sympy_check/__init__.py b/lib/sympy_check/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/lib/sympy_check/formula.py b/lib/sympy_check/formula.py
new file mode 100644
index 0000000000..01ced8650c
--- /dev/null
+++ b/lib/sympy_check/formula.py
@@ -0,0 +1,461 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# File: formula.py
+# Date: 04-May-12
+# Author: I. Chuang
+#
+# flexible python representation of a symbolic mathematical formula.
+# Acceptes Presentation MathML, Content MathML (and could also do OpenMath)
+# Provides sympy representation.
+
+import os, sys, string, re
+import operator
+import sympy
+from sympy.printing.latex import LatexPrinter
+from sympy.printing.str import StrPrinter
+from sympy import latex, sympify
+from sympy.physics.quantum.qubit import *
+from sympy.physics.quantum.state import *
+# from sympy import exp, pi, I
+# from sympy.core.operations import LatticeOp
+# import sympy.physics.quantum.qubit
+
+import urllib
+from xml.sax.saxutils import escape, unescape
+import sympy
+import unicodedata
+from lxml import etree
+#import subprocess
+import requests
+from copy import deepcopy
+
+print "Warning: Dark code. Needs review before enabling in prod."
+
+os.environ['PYTHONIOENCODING'] = 'utf-8'
+
+#-----------------------------------------------------------------------------
+
+class dot(sympy.operations.LatticeOp): # my dot product
+ zero = sympy.Symbol('dotzero')
+ identity = sympy.Symbol('dotidentity')
+
+#class dot(sympy.Mul): # my dot product
+# is_Mul = False
+
+def _print_dot(self,expr):
+ return '{((%s) \cdot (%s))}' % (expr.args[0],expr.args[1])
+
+LatexPrinter._print_dot = _print_dot
+
+#-----------------------------------------------------------------------------
+# unit vectors (for 8.02)
+
+def _print_hat(self,expr): return '\\hat{%s}' % str(expr.args[0]).lower()
+
+LatexPrinter._print_hat = _print_hat
+StrPrinter._print_hat = _print_hat
+
+#-----------------------------------------------------------------------------
+# helper routines
+
+def to_latex(x):
+ if x==None: return ''
+ # LatexPrinter._print_dot = _print_dot
+ xs = latex(x)
+ xs = xs.replace(r'\XI','XI') # workaround for strange greek
+ #return '' % (xs[1:-1])
+ if xs[0]=='$':
+ return '[mathjax]%s[/mathjax]
' % (xs[1:-1]) # for sympy v6
+ return '[mathjax]%s[/mathjax]
' % (xs) # for sympy v7
+
+def my_evalf(expr,chop=False):
+ if type(expr)==list:
+ try:
+ return [x.evalf(chop=chop) for x in expr]
+ except:
+ return expr
+ try:
+ return expr.evalf(chop=chop)
+ except:
+ return expr
+
+#-----------------------------------------------------------------------------
+# my version of sympify to import expression into sympy
+
+def my_sympify(expr,normphase=False,matrix=False,abcsym=False,do_qubit=False,symtab=None):
+ # make all lowercase real?
+ if symtab:
+ varset = symtab
+ else:
+ varset = {'p':sympy.Symbol('p'),
+ 'g':sympy.Symbol('g'),
+ 'e':sympy.E, # for exp
+ 'i':sympy.I, # lowercase i is also sqrt(-1)
+ 'Q':sympy.Symbol('Q'), # otherwise it is a sympy "ask key"
+ #'X':sympy.sympify('Matrix([[0,1],[1,0]])'),
+ #'Y':sympy.sympify('Matrix([[0,-I],[I,0]])'),
+ #'Z':sympy.sympify('Matrix([[1,0],[0,-1]])'),
+ 'ZZ':sympy.Symbol('ZZ'), # otherwise it is the PythonIntegerRing
+ 'XI':sympy.Symbol('XI'), # otherwise it is the capital \XI
+ 'hat':sympy.Function('hat'), # for unit vectors (8.02)
+ }
+ if do_qubit: # turn qubit(...) into Qubit instance
+ varset.update({'qubit':sympy.physics.quantum.qubit.Qubit,
+ 'Ket':sympy.physics.quantum.state.Ket,
+ 'dot':dot,
+ 'bit':sympy.Function('bit'),
+ })
+ if abcsym: # consider all lowercase letters as real symbols, in the parsing
+ for letter in string.lowercase:
+ if letter in varset: # exclude those already done
+ continue
+ varset.update({letter:sympy.Symbol(letter,real=True)})
+
+ sexpr = sympify(expr,locals=varset)
+ if normphase: # remove overall phase if sexpr is a list
+ if type(sexpr)==list:
+ if sexpr[0].is_number:
+ ophase = sympy.sympify('exp(-I*arg(%s))' % sexpr[0])
+ sexpr = [ sympy.Mul(x,ophase) for x in sexpr ]
+
+ def to_matrix(x): # if x is a list of lists, and is rectangular, then return Matrix(x)
+ if not type(x)==list:
+ return x
+ for row in x:
+ if (not type(row)==list):
+ return x
+ rdim = len(x[0])
+ for row in x:
+ if not len(row)==rdim:
+ return x
+ return sympy.Matrix(x)
+
+ if matrix:
+ sexpr = to_matrix(sexpr)
+ return sexpr
+
+#-----------------------------------------------------------------------------
+# class for symbolic mathematical formulas
+
+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=''):
+ self.expr = expr.strip()
+ self.asciimath = asciimath
+ self.the_cmathml = None
+ self.the_sympy = None
+
+ def is_presentation_mathml(self):
+ return 'f-2"
+ # this is really terrible for turning into cmathml.
+ # undo this here.
+ def fix_pmathml(xml):
+ for k in xml:
+ tag = gettag(k)
+ if tag=='mrow':
+ if len(k)==2:
+ if gettag(k[0])=='mi' and k[0].text in ['f','g'] and gettag(k[1])=='mo':
+ idx = xml.index(k)
+ xml.insert(idx,deepcopy(k[0])) # drop the container
+ xml.insert(idx+1,deepcopy(k[1]))
+ xml.remove(k)
+ fix_pmathml(k)
+
+ fix_pmathml(xml)
+
+ # hat i is turned into i^ ; mangle this into hat(f)
+ # hat i also somtimes turned into j ^
+
+ def fix_hat(xml):
+ for k in xml:
+ tag = gettag(k)
+ if tag=='mover':
+ if len(k)==2:
+ if gettag(k[0])=='mi' and gettag(k[1])=='mo' and str(k[1].text)=='^':
+ newk = etree.Element('mi')
+ newk.text = 'hat(%s)' % k[0].text
+ xml.replace(k,newk)
+ if gettag(k[0])=='mrow' and gettag(k[0][0])=='mi' and gettag(k[1])=='mo' and str(k[1].text)=='^':
+ newk = etree.Element('mi')
+ newk.text = 'hat(%s)' % k[0][0].text
+ xml.replace(k,newk)
+ fix_hat(k)
+ fix_hat(xml)
+
+ self.xml = xml
+ return self.xml
+
+ def get_content_mathml(self):
+ if self.the_cmathml: return self.the_cmathml
+
+ # pre-process the presentation mathml before sending it to snuggletex to convert to content mathml
+ xml = self.preprocess_pmathml(self.expr)
+ pmathml = etree.tostring(xml,pretty_print=True)
+ self.the_pmathml = pmathml
+
+ # convert to cmathml
+ self.the_cmathml = self.GetContentMathML(self.asciimath,pmathml)
+ return self.the_cmathml
+
+ cmathml = property(get_content_mathml,None,None,'content MathML representation')
+
+ def make_sympy(self,xml=None):
+ '''
+ Return sympy expression for the math formula
+ '''
+
+ if self.the_sympy: return self.the_sympy
+
+ if xml==None: # root
+ if not self.is_mathml():
+ return my_sympify(self.expr)
+ if self.is_presentation_mathml():
+ xml = etree.fromstring(str(self.cmathml))
+ xml = self.fix_greek_in_mathml(xml)
+ self.the_sympy = self.make_sympy(xml[0])
+ else:
+ xml = etree.fromstring(self.expr)
+ xml = self.fix_greek_in_mathml(xml)
+ self.the_sympy = self.make_sympy(xml[0])
+ return self.the_sympy
+
+ def gettag(x):
+ return re.sub('{http://[^}]+}','',x.tag)
+
+ # simple math
+ def op_divide(*args):
+ if not len(args)==2:
+ raise Exception,'divide given wrong number of arguments!'
+ # 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_times(*args): return reduce(operator.mul,args)
+
+ def op_minus(*args):
+ if len(args)==1:
+ return -args[0]
+ if not len(args)==2:
+ raise Exception,'minus given wrong number of arguments!'
+ #return sympy.Add(args[0],-args[1])
+ return args[0]-args[1]
+
+ opdict = {'plus': op_plus,
+ 'divide' : operator.div,
+ 'times' : op_times,
+ 'minus' : op_minus,
+ #'plus': sympy.Add,
+ #'divide' : op_divide,
+ #'times' : sympy.Mul,
+ 'minus' : op_minus,
+ 'root' : sympy.sqrt,
+ 'power' : sympy.Pow,
+ 'sin': sympy.sin,
+ 'cos': sympy.cos,
+ }
+
+ # simple sumbols
+ nums1dict = {'pi': sympy.pi,
+ }
+
+ def parsePresentationMathMLSymbol(xml):
+ '''
+ Parse , , , and
+ '''
+ tag = gettag(xml)
+ if tag=='mn': return xml.text
+ elif tag=='mi': return xml.text
+ elif tag=='msub': 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
+
+ # parser tree for content MathML
+ tag = gettag(xml)
+ print "tag = ",tag
+
+ # first do compound objects
+
+ if tag=='apply': # apply operator
+ opstr = gettag(xml[0])
+ if opstr in opdict:
+ op = opdict[opstr]
+ args = [ self.make_sympy(x) for x in xml[1:]]
+ return op(*args)
+ else:
+ raise Exception,'[formula]: unknown operator tag %s' % (opstr)
+
+ elif tag=='list': # square bracket list
+ if gettag(xml[0])=='matrix':
+ return self.make_sympy(xml[0])
+ else:
+ return [ self.make_sympy(x) for x in xml ]
+
+ elif tag=='matrix':
+ return sympy.Matrix([ self.make_sympy(x) for x in xml ])
+
+ elif tag=='vector':
+ return [ self.make_sympy(x) for x in xml ]
+
+ # atoms are below
+
+ elif tag=='cn': # number
+ return sympy.sympify(xml.text)
+ return float(xml.text)
+
+ elif tag=='ci': # variable (symbol)
+ if len(xml)>0 and (gettag(xml[0])=='msub' or gettag(xml[0])=='msup'):
+ usym = parsePresentationMathMLSymbol(xml[0])
+ sym = sympy.Symbol(str(usym))
+ else:
+ usym = unicode(xml.text)
+ if 'hat' in usym:
+ sym = my_sympify(usym)
+ else:
+ sym = sympy.Symbol(str(usym))
+ return sym
+
+ else: # unknown tag
+ raise Exception,'[formula] unknown tag %s' % tag
+
+ sympy = property(make_sympy,None,None,'sympy representation')
+
+ def GetContentMathML(self,asciimath,mathml):
+ # URL = 'http://192.168.1.2:8080/snuggletex-webapp-1.2.2/ASCIIMathMLUpConversionDemo'
+ URL = 'http://127.0.0.1:8080/snuggletex-webapp-1.2.2/ASCIIMathMLUpConversionDemo'
+
+ if 1:
+ payload = {'asciiMathInput':asciimath,
+ 'asciiMathML':mathml,
+ #'asciiMathML':unicode(mathml).encode('utf-8'),
+ }
+ headers = {'User-Agent':"Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13"}
+ r = requests.post(URL,data=payload,headers=headers)
+ r.encoding = 'utf-8'
+ ret = r.text
+ #print "encoding: ",r.encoding
+
+ # return ret
+
+ mode = 0
+ cmathml = []
+ for k in ret.split('\n'):
+ if 'conversion to Content MathML' in k:
+ mode = 1
+ continue
+ if mode==1:
+ if 'Maxima Input Form
' in k:
+ mode = 0
+ continue
+ cmathml.append(k)
+ # return '\n'.join(cmathml)
+ cmathml = '\n'.join(cmathml[2:])
+ cmathml = ''
+ # print cmathml
+ #return unicode(cmathml)
+ return cmathml
+
+#-----------------------------------------------------------------------------
+
+def test1():
+ xmlstr = '''
+
+ '''
+ return formula(xmlstr)
+
+def test2():
+ xmlstr = u'''
+
+ '''
+ return formula(xmlstr)
+
+def test3():
+ xmlstr = '''
+
+ '''
+ return formula(xmlstr)
+
+def test4():
+ xmlstr = u'''
+
+'''
+ return formula(xmlstr)
diff --git a/lib/sympy_check/sympy_check2.py b/lib/sympy_check/sympy_check2.py
new file mode 100644
index 0000000000..abb70ed165
--- /dev/null
+++ b/lib/sympy_check/sympy_check2.py
@@ -0,0 +1,271 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# File: sympy_check2.py
+# Date: 02-May-12
+# Author: I. Chuang
+#
+# Use sympy to check for expression equality
+#
+# Takes in math expressions given as Presentation MathML (from ASCIIMathML), converts to Content MathML using SnuggleTeX
+
+import os, sys, string, re
+import traceback
+from formula import *
+
+#-----------------------------------------------------------------------------
+# check function interface
+
+def sympy_check(expect,ans,adict={},symtab=None,extra_options=None):
+
+ options = {'__MATRIX__':False,'__ABC__':False,'__LOWER__':False}
+ if extra_options: options.update(extra_options)
+ for op in options: # find options in expect string
+ if op in expect:
+ expect = expect.replace(op,'')
+ options[op] = True
+ expect = expect.replace('__OR__','__or__') # backwards compatibility
+
+ if options['__LOWER__']:
+ expect = expect.lower()
+ ans = ans.lower()
+
+ try:
+ ret = check(expect,ans,
+ matrix=options['__MATRIX__'],
+ abcsym=options['__ABC__'],
+ symtab=symtab,
+ )
+ except Exception, err:
+ return {'ok': False,
+ 'msg': 'Error %s
Failed in evaluating check(%s,%s)' % (err,expect,ans)
+ }
+ return ret
+
+#-----------------------------------------------------------------------------
+# pretty generic checking function
+
+def check(expect,given,numerical=False,matrix=False,normphase=False,abcsym=False,do_qubit=True,symtab=None,dosimplify=False):
+ """
+ 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__"
+
+ """
+
+ if "__or__" in expect: # if multiple acceptable answers
+ eset = expect.split('__or__') # then see if any match
+ for eone in eset:
+ ret = check(eone,given,numerical,matrix,normphase,abcsym,do_qubit,symtab,dosimplify)
+ if ret['ok']:
+ return ret
+ return ret
+
+ flags = {}
+ if "__autonorm__" in expect:
+ flags['autonorm']=True
+ expect = expect.replace('__autonorm__','')
+ matrix = True
+
+ threshold = 1.0e-3
+ if "__threshold__" in expect:
+ (expect,st) = expect.split('__threshold__')
+ threshold = float(st)
+ numerical=True
+
+ if str(given)=='' and not (str(expect)==''):
+ return {'ok': False, 'msg': ''}
+
+ try:
+ xgiven = my_sympify(given,normphase,matrix,do_qubit=do_qubit,abcsym=abcsym,symtab=symtab)
+ except Exception,err:
+ return {'ok': False,'msg': 'Error %s
in evaluating your expression "%s"' % (err,given)}
+
+ try:
+ xexpect = my_sympify(expect,normphase,matrix,do_qubit=do_qubit,abcsym=abcsym,symtab=symtab)
+ except Exception,err:
+ return {'ok': False,'msg': 'Error %s
in evaluating OUR expression "%s"' % (err,expect)}
+
+ if 'autonorm' in flags: # normalize trace of matrices
+ try:
+ xgiven /= xgiven.trace()
+ except Exception, err:
+ return {'ok': False,'msg': 'Error %s
in normalizing trace of your expression %s' % (err,to_latex(xgiven))}
+ try:
+ xexpect /= xexpect.trace()
+ except Exception, err:
+ return {'ok': False,'msg': 'Error %s
in normalizing trace of OUR expression %s' % (err,to_latex(xexpect))}
+
+ msg = 'Your expression was evaluated as ' + to_latex(xgiven)
+ # msg += '
Expected ' + to_latex(xexpect)
+
+ # msg += "
flags=%s" % flags
+
+ if matrix and numerical:
+ xgiven = my_evalf(xgiven,chop=True)
+ dm = my_evalf(sympy.Matrix(xexpect)-sympy.Matrix(xgiven),chop=True)
+ msg += " = " + to_latex(xgiven)
+ if abs(dm.vec().norm().evalf())expect='%s', given='%s'" % (expect,given) # debugging
+ # msg += " dot test " + to_latex(dot(sympy.Symbol('x'),sympy.Symbol('y')))
+ return {'ok': False,'msg': msg }
+
+#-----------------------------------------------------------------------------
+# Check function interface, which takes pmathml input
+
+def sympy_check2(expect,ans,adict={},abname=''):
+
+ msg = ''
+ # msg += 'abname=%s' % abname
+ # msg += 'adict=%s' % (repr(adict).replace('<','<'))
+
+ threshold = 1.0e-3
+ DEBUG = True
+
+ # parse expected answer
+ try:
+ fexpect = my_sympify(str(expect))
+ except Exception,err:
+ msg += 'Error %s in parsing OUR expected answer "%s"
' % (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))
+ except Exception,err:
+ fans = None
+
+ if fexpect.is_number and fans and fans.is_number:
+ if abs(abs(fans-fexpect)/fexpect)You entered: %s
' % to_latex(fans)
+ return {'ok':False,'msg':msg}
+
+ if fexpect==fans:
+ msg += 'You entered: %s
' % to_latex(fans)
+ return {'ok':True,'msg':msg}
+
+ # convert mathml answer to formula
+ mmlbox = abname+'_fromjs'
+ if mmlbox in adict:
+ mmlans = adict[mmlbox]
+ f = formula(mmlans)
+
+ # get sympy representation of the formula
+ # if DEBUG: msg += ' mmlans=%s' % repr(mmlans).replace('<','<')
+ try:
+ fsym = f.sympy
+ msg += 'You entered: %s
' % to_latex(f.sympy)
+ except Exception,err:
+ msg += "Error %s in converting to sympy
" % str(err).replace('<','<')
+ if DEBUG: msg += "%s
" % traceback.format_exc()
+ return {'ok':False,'msg':msg}
+
+ # compare with expected
+ if fexpect.is_number:
+ if fsym.is_number:
+ if abs(abs(fsym-fexpect)/fexpect)Expecting a numerical answer!"
+ msg += "given = %s
" % repr(ans)
+ msg += "fsym = %s
" % repr(fsym)
+ # msg += "cmathml =
%s
" % str(f.cmathml).replace('<','<')
+ return {'ok':False,'msg':msg}
+
+ if fexpect==fsym:
+ return {'ok':True,'msg':msg}
+
+ if type(fexpect)==list:
+ try:
+ xgiven = my_evalf(fsym,chop=True)
+ dm = my_evalf(sympy.Matrix(fexpect)-sympy.Matrix(xgiven),chop=True)
+ if abs(dm.vec().norm().evalf())Error %s in comparing expected (a list) and your answer" % str(err).replace('<','<')
+ if DEBUG: msg += "%s
" % traceback.format_exc()
+ return {'ok':False,'msg':msg}
+
+ #diff = (fexpect-fsym).simplify()
+ #fsym = fsym.simplify()
+ #fexpect = fexpect.simplify()
+ try:
+ diff = (fexpect-fsym)
+ except Exception,err:
+ diff = None
+
+ if DEBUG:
+ msg += "Got: %s
" % repr(fsym)
+ # msg += "Got: %s" % str([type(x) for x in fsym.atoms()]).replace('<','<')
+ msg += "Expecting: %s
" % repr(fexpect).replace('**','^').replace('hat(I)','hat(i)')
+ # msg += "Expecting: %s" % str([type(x) for x in fexpect.atoms()]).replace('<','<')
+ if diff:
+ msg += "Difference: %s
" % to_latex(diff)
+
+ return {'ok':False,'msg':msg,'ex':fexpect,'got':fsym}
+
+def sctest1():
+ x = "1/2*(1+(k_e* Q* q)/(m *g *h^2))"
+ y = '''
+
+'''.strip()
+ z = "1/2(1+(k_e* Q* q)/(m *g *h^2))"
+ r = sympy_check2(x,z,{'a':z,'a_fromjs':y},'a')
+ return r
+