Carlos muniz/symmath removal unrevert (#29912)
* Revert "Merge pull request #29909 from openedx/revert-29869-Carlos-Muniz/symmath-removal" This reverts commit8c0db8ddff, reversing changes made to1156c62014. * fix: Remove misplaced `-e` `-e` was wrongfully placed in front of `common/lib/sandbox-packages`, which may have most likely been causing the edxAPP to break. * fix: Change regex to apply to right dirs `py38.txt` does not include `common/lib/xmodule` so it doesn't match the regex. Therefore, it never got its lines in `common/lib/sandbox-packages` fixed. If we change the regex to match any `common/lib/<packagename>` it should work correctly.
This commit is contained in:
@@ -1,11 +0,0 @@
|
||||
# lint-amnesty, pylint: disable=missing-module-docstring
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name="symmath",
|
||||
version="0.3",
|
||||
packages=["symmath"],
|
||||
install_requires=[
|
||||
"sympy",
|
||||
],
|
||||
)
|
||||
@@ -1,30 +0,0 @@
|
||||
(Originally written by Ike.)
|
||||
|
||||
At a high level, the main challenges of checking symbolic math expressions are
|
||||
(1) making sure the expression is mathematically legal, and (2) simplifying the
|
||||
expression for comparison with what is expected.
|
||||
|
||||
(1) Generation (and testing) of legal input is done by using MathJax to provide
|
||||
input math in an XML format known as Presentation MathML (PMathML). Such
|
||||
expressions typeset correctly, but may not be mathematically legal, like "5 /
|
||||
(1 = 2)". The PMathML is converted into "Content MathML" (CMathML), which is
|
||||
by definition mathematically legal, using an XSLT 2.0 stylesheet, via a module
|
||||
in SnuggleTeX. CMathML is then converted into a sympy expression. This work is
|
||||
all done in `symmath/formula.py`.
|
||||
|
||||
(2) Simplifying the expression and checking against what is expected is done by
|
||||
using sympy, and a set of heuristics based on options flags provided by the
|
||||
problem author. For example, the problem author may specify that the expected
|
||||
expression is a matrix, in which case the dimensionality of the input
|
||||
expression is checked. Other options include specifying that the comparison be
|
||||
checked numerically in addition to symbolically. The checking is done in
|
||||
stages, first with no simplification, then with increasing levels of testing;
|
||||
if a match is found at any stage, then an "ok" is returned. Helpful messages
|
||||
are also returned, eg if the input expression is of a different type than the
|
||||
expected. This work is all done in `symmath/symmath_check.py`.
|
||||
|
||||
Links:
|
||||
|
||||
SnuggleTex: http://www2.ph.ed.ac.uk/snuggletex/documentation/overview-and-features.html
|
||||
MathML: http://www.w3.org/TR/MathML2/overview.html
|
||||
SymPy: http://sympy.org/en/index.html
|
||||
@@ -1,3 +0,0 @@
|
||||
# lint-amnesty, pylint: disable=missing-module-docstring
|
||||
from .formula import *
|
||||
from .symmath_check import *
|
||||
@@ -1,588 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Flexible python representation of a symbolic mathematical formula.
|
||||
Acceptes Presentation MathML, Content MathML (and could also do OpenMath).
|
||||
Provides sympy representation.
|
||||
"""
|
||||
#
|
||||
# File: formula.py
|
||||
# Date: 04-May-12 (creation)
|
||||
# Author: I. Chuang <ichuang@mit.edu>
|
||||
#
|
||||
|
||||
|
||||
import logging
|
||||
import operator
|
||||
import os
|
||||
import re
|
||||
import string
|
||||
import unicodedata
|
||||
#import subprocess
|
||||
from copy import deepcopy
|
||||
from functools import reduce
|
||||
|
||||
import six # lint-amnesty, pylint: disable=unused-import
|
||||
import sympy
|
||||
from lxml import etree
|
||||
from sympy import latex, sympify
|
||||
from sympy.physics.quantum.qubit import Qubit
|
||||
from sympy.physics.quantum.state import Ket
|
||||
from sympy.printing.latex import LatexPrinter
|
||||
from sympy.printing.str import StrPrinter
|
||||
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
log.warning("Dark code. Needs review before enabling in prod.")
|
||||
|
||||
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class dot(sympy.operations.LatticeOp): # pylint: disable=invalid-name, no-member
|
||||
"""my dot product"""
|
||||
zero = sympy.Symbol('dotzero')
|
||||
identity = sympy.Symbol('dotidentity')
|
||||
|
||||
|
||||
def _print_dot(_self, expr):
|
||||
"""Print statement used for LatexPrinter"""
|
||||
return r'{((%s) \cdot (%s))}' % (expr.args[0], expr.args[1])
|
||||
|
||||
LatexPrinter._print_dot = _print_dot # pylint: disable=protected-access
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# unit vectors (for 8.02)
|
||||
|
||||
|
||||
def _print_hat(_self, expr):
|
||||
"""Print statement used for LatexPrinter"""
|
||||
return '\\hat{%s}' % str(expr.args[0]).lower()
|
||||
|
||||
LatexPrinter._print_hat = _print_hat # pylint: disable=protected-access
|
||||
StrPrinter._print_hat = _print_hat # pylint: disable=protected-access
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# helper routines
|
||||
|
||||
|
||||
def to_latex(expr):
|
||||
"""
|
||||
Convert expression to latex mathjax format
|
||||
"""
|
||||
if expr is None:
|
||||
return ''
|
||||
expr_s = latex(expr)
|
||||
expr_s = expr_s.replace(r'\XI', 'XI') # workaround for strange greek
|
||||
|
||||
# substitute back into latex form for scripts
|
||||
# literally something of the form
|
||||
# 'scriptN' becomes '\\mathcal{N}'
|
||||
# note: can't use something akin to the _print_hat method above because we
|
||||
# sometimes get 'script(N)__B' or more complicated terms
|
||||
expr_s = re.sub(
|
||||
r'script([a-zA-Z0-9]+)',
|
||||
r'\\mathcal{\\1}',
|
||||
expr_s
|
||||
)
|
||||
|
||||
#return '<math>%s{}{}</math>' % (xs[1:-1])
|
||||
if expr_s[0] == '$':
|
||||
return HTML('[mathjax]{expression}[/mathjax]<br>').format(expression=expr_s[1:-1]) # for sympy v6
|
||||
return HTML('[mathjax]{expression}[/mathjax]<br>').format(expression=expr_s) # for sympy v7
|
||||
|
||||
|
||||
def my_evalf(expr, chop=False):
|
||||
"""
|
||||
Enhanced sympy evalf to handle lists of expressions
|
||||
and catch eval failures without dropping out.
|
||||
"""
|
||||
if isinstance(expr, list):
|
||||
try:
|
||||
return [x.evalf(chop=chop) for x in expr]
|
||||
except Exception: # pylint: disable=broad-except
|
||||
return expr
|
||||
try:
|
||||
return expr.evalf(chop=chop)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
return expr
|
||||
|
||||
|
||||
def my_sympify(expr, normphase=False, matrix=False, abcsym=False, do_qubit=False, symtab=None):
|
||||
"""
|
||||
Version of sympify to import expression into sympy
|
||||
"""
|
||||
# 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"
|
||||
'I': sympy.Symbol('I'), # otherwise it is sqrt(-1)
|
||||
'N': sympy.Symbol('N'), # or it is some kind of sympy function
|
||||
'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': Qubit,
|
||||
'Ket': Ket,
|
||||
'dot': dot,
|
||||
'bit': sympy.Function('bit'),
|
||||
})
|
||||
if abcsym: # consider all lowercase letters as real symbols, in the parsing
|
||||
for letter in string.ascii_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 isinstance(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(expr):
|
||||
"""
|
||||
Convert a list, or list of lists to a matrix.
|
||||
"""
|
||||
# if expr is a list of lists, and is rectangular, then return Matrix(expr)
|
||||
if not isinstance(expr, list):
|
||||
return expr
|
||||
for row in expr:
|
||||
if not isinstance(row, list):
|
||||
return expr
|
||||
rdim = len(expr[0])
|
||||
for row in expr:
|
||||
if not len(row) == rdim:
|
||||
return expr
|
||||
return sympy.Matrix(expr)
|
||||
|
||||
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='', 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):
|
||||
"""
|
||||
Check if formula is in mathml presentation format.
|
||||
"""
|
||||
return '<mstyle' in self.expr
|
||||
|
||||
def is_mathml(self):
|
||||
"""
|
||||
Check if formula is in mathml format.
|
||||
"""
|
||||
return '<math ' in self.expr
|
||||
|
||||
def fix_greek_in_mathml(self, xml):
|
||||
"""
|
||||
Recursively fix greek letters in passed in xml.
|
||||
"""
|
||||
def gettag(expr):
|
||||
return re.sub('{http://[^}]+}', '', expr.tag)
|
||||
|
||||
for k in xml:
|
||||
tag = gettag(k)
|
||||
if tag == 'mi' or tag == 'ci': # lint-amnesty, pylint: disable=consider-using-in
|
||||
usym = six.text_type(k.text)
|
||||
try:
|
||||
udata = unicodedata.name(usym)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
udata = None
|
||||
# print "usym = %s, udata=%s" % (usym,udata)
|
||||
if udata: # eg "GREEK SMALL LETTER BETA"
|
||||
if 'GREEK' in udata:
|
||||
usym = udata.split(' ')[-1]
|
||||
if 'SMALL' in udata:
|
||||
usym = usym.lower()
|
||||
#print "greek: ",usym
|
||||
k.text = usym
|
||||
self.fix_greek_in_mathml(k)
|
||||
return xml
|
||||
|
||||
def preprocess_pmathml(self, xml): # lint-amnesty, pylint: disable=too-many-statements
|
||||
r"""
|
||||
Pre-process presentation MathML from ASCIIMathML to make it more
|
||||
acceptable for SnuggleTeX, and also to accomodate some sympy
|
||||
conventions (eg hat(i) for \hat{i}).
|
||||
|
||||
This method would be a good spot to look for an integral and convert
|
||||
it, if possible...
|
||||
"""
|
||||
|
||||
if isinstance(xml, (str, six.text_type)):
|
||||
xml = etree.fromstring(xml) # TODO: wrap in try
|
||||
|
||||
xml = self.fix_greek_in_mathml(xml) # convert greek utf letters to greek spelled out in ascii
|
||||
|
||||
def gettag(expr):
|
||||
return re.sub('{http://[^}]+}', '', expr.tag)
|
||||
|
||||
def fix_pmathml(xml):
|
||||
"""
|
||||
f and g are processed as functions by asciimathml, eg "f-2" turns
|
||||
into "<mrow><mi>f</mi><mo>-</mo></mrow><mn>2</mn>" this is
|
||||
really terrible for turning into cmathml. undo this here.
|
||||
"""
|
||||
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 <mrow> container
|
||||
xml.insert(idx + 1, deepcopy(k[1]))
|
||||
xml.remove(k)
|
||||
fix_pmathml(k)
|
||||
|
||||
fix_pmathml(xml)
|
||||
|
||||
def fix_hat(xml):
|
||||
"""
|
||||
hat i is turned into <mover><mi>i</mi><mo>^</mo></mover> ; mangle
|
||||
this into <mi>hat(f)</mi> hat i also somtimes turned into
|
||||
<mover><mrow> <mi>j</mi> </mrow><mo>^</mo></mover>
|
||||
"""
|
||||
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)
|
||||
|
||||
def flatten_pmathml(xml):
|
||||
"""
|
||||
Give the text version of certain PMathML elements
|
||||
|
||||
Sometimes MathML will be given with each letter separated (it
|
||||
doesn't know if its implicit multiplication or what). From an xml
|
||||
node, find the (text only) variable name it represents. So it takes
|
||||
<mrow>
|
||||
<mi>m</mi>
|
||||
<mi>a</mi>
|
||||
<mi>x</mi>
|
||||
</mrow>
|
||||
and returns 'max', for easier use later on.
|
||||
"""
|
||||
tag = gettag(xml)
|
||||
if tag == 'mn':
|
||||
return xml.text
|
||||
elif tag == 'mi':
|
||||
return xml.text
|
||||
elif tag == 'mrow':
|
||||
return ''.join([flatten_pmathml(y) for y in xml])
|
||||
raise Exception('[flatten_pmathml] unknown tag %s' % tag)
|
||||
|
||||
def fix_mathvariant(parent):
|
||||
"""
|
||||
Fix certain kinds of math variants
|
||||
|
||||
Literally replace <mstyle mathvariant="script"><mi>N</mi></mstyle>
|
||||
with 'scriptN'. There have been problems using script_N or script(N)
|
||||
"""
|
||||
for child in parent:
|
||||
if gettag(child) == 'mstyle' and child.get('mathvariant') == 'script':
|
||||
newchild = etree.Element('mi')
|
||||
newchild.text = 'script%s' % flatten_pmathml(child[0])
|
||||
parent.replace(child, newchild)
|
||||
fix_mathvariant(child)
|
||||
fix_mathvariant(xml)
|
||||
|
||||
# find "tagged" superscripts
|
||||
# they have the character \u200b in the superscript
|
||||
# replace them with a__b so snuggle doesn't get confused
|
||||
def fix_superscripts(xml):
|
||||
""" Look for and replace sup elements with 'X__Y' or 'X_Y__Z'
|
||||
|
||||
In the javascript, variables with '__X' in them had an invisible
|
||||
character inserted into the sup (to distinguish from powers)
|
||||
E.g. normal:
|
||||
<msubsup>
|
||||
<mi>a</mi>
|
||||
<mi>b</mi>
|
||||
<mi>c</mi>
|
||||
</msubsup>
|
||||
to be interpreted '(a_b)^c' (nothing done by this method)
|
||||
|
||||
And modified:
|
||||
<msubsup>
|
||||
<mi>b</mi>
|
||||
<mi>x</mi>
|
||||
<mrow>
|
||||
<mo>​</mo>
|
||||
<mi>d</mi>
|
||||
</mrow>
|
||||
</msubsup>
|
||||
to be interpreted 'a_b__c'
|
||||
|
||||
also:
|
||||
<msup>
|
||||
<mi>x</mi>
|
||||
<mrow>
|
||||
<mo>​</mo>
|
||||
<mi>B</mi>
|
||||
</mrow>
|
||||
</msup>
|
||||
to be 'x__B'
|
||||
"""
|
||||
for k in xml:
|
||||
tag = gettag(k)
|
||||
|
||||
# match things like the last example--
|
||||
# the second item in msub is an mrow with the first
|
||||
# character equal to \u200b
|
||||
if (
|
||||
tag == 'msup' and
|
||||
len(k) == 2 and gettag(k[1]) == 'mrow' and
|
||||
gettag(k[1][0]) == 'mo' and k[1][0].text == '\u200b' # whew
|
||||
):
|
||||
|
||||
# replace the msup with 'X__Y'
|
||||
k[1].remove(k[1][0])
|
||||
newk = etree.Element('mi')
|
||||
newk.text = '%s__%s' % (flatten_pmathml(k[0]), flatten_pmathml(k[1]))
|
||||
xml.replace(k, newk)
|
||||
|
||||
# match things like the middle example-
|
||||
# the third item in msubsup is an mrow with the first
|
||||
# character equal to \u200b
|
||||
if (
|
||||
tag == 'msubsup' and
|
||||
len(k) == 3 and gettag(k[2]) == 'mrow' and
|
||||
gettag(k[2][0]) == 'mo' and k[2][0].text == '\u200b' # whew
|
||||
):
|
||||
|
||||
# replace the msubsup with 'X_Y__Z'
|
||||
k[2].remove(k[2][0])
|
||||
newk = etree.Element('mi')
|
||||
newk.text = '%s_%s__%s' % (flatten_pmathml(k[0]), flatten_pmathml(k[1]), flatten_pmathml(k[2]))
|
||||
xml.replace(k, newk)
|
||||
|
||||
fix_superscripts(k)
|
||||
fix_superscripts(xml)
|
||||
|
||||
def fix_msubsup(parent):
|
||||
"""
|
||||
Snuggle returns an error when it sees an <msubsup> replace such
|
||||
elements with an <msup>, except the first element is of
|
||||
the form a_b. I.e. map a_b^c => (a_b)^c
|
||||
"""
|
||||
for child in parent:
|
||||
# fix msubsup
|
||||
if gettag(child) == 'msubsup' and len(child) == 3:
|
||||
newchild = etree.Element('msup')
|
||||
newbase = etree.Element('mi')
|
||||
newbase.text = '%s_%s' % (flatten_pmathml(child[0]), flatten_pmathml(child[1]))
|
||||
newexp = child[2]
|
||||
newchild.append(newbase)
|
||||
newchild.append(newexp)
|
||||
parent.replace(child, newchild)
|
||||
|
||||
fix_msubsup(child)
|
||||
fix_msubsup(xml)
|
||||
|
||||
self.xml = xml # pylint: disable=attribute-defined-outside-init
|
||||
return self.xml
|
||||
|
||||
def get_content_mathml(self): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
if self.the_cmathml:
|
||||
return self.the_cmathml
|
||||
|
||||
# pre-process the presentation mathml before sending it to snuggletex to convert to content mathml
|
||||
try:
|
||||
xml = self.preprocess_pmathml(self.expr).decode('utf-8')
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
log.warning('Err %s while preprocessing; expr=%s', err, self.expr)
|
||||
return "<html>Error! Cannot process pmathml</html>"
|
||||
pmathml = etree.tostring(xml, pretty_print=True)
|
||||
self.the_pmathml = pmathml # pylint: disable=attribute-defined-outside-init
|
||||
return self.the_pmathml
|
||||
|
||||
cmathml = property(get_content_mathml, None, None, 'content MathML representation')
|
||||
|
||||
def make_sympy(self, xml=None): # lint-amnesty, pylint: disable=too-many-statements
|
||||
"""
|
||||
Return sympy expression for the math formula.
|
||||
The math formula is converted to Content MathML then that is parsed.
|
||||
|
||||
This is a recursive function, called on every CMML node. Support for
|
||||
more functions can be added by modifying opdict, abould halfway down
|
||||
"""
|
||||
|
||||
if self.the_sympy:
|
||||
return self.the_sympy
|
||||
|
||||
if xml is None: # root
|
||||
if not self.is_mathml():
|
||||
return my_sympify(self.expr)
|
||||
if self.is_presentation_mathml():
|
||||
cmml = None
|
||||
try:
|
||||
cmml = self.cmathml
|
||||
xml = etree.fromstring(str(cmml))
|
||||
except Exception as err:
|
||||
if 'conversion from Presentation MathML to Content MathML was not successful' in cmml: # lint-amnesty, pylint: disable=unsupported-membership-test
|
||||
msg = "Illegal math expression"
|
||||
else:
|
||||
msg = 'Err %s while converting cmathml to xml; cmml=%s' % (err, cmml)
|
||||
raise Exception(msg) # lint-amnesty, pylint: disable=raise-missing-from
|
||||
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(expr):
|
||||
return re.sub('{http://[^}]+}', '', expr.tag)
|
||||
|
||||
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):
|
||||
if len(args) == 1:
|
||||
return -args[0]
|
||||
if not len(args) == 2: # lint-amnesty, pylint: disable=unneeded-not
|
||||
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, # lint-amnesty, pylint: disable=no-member
|
||||
'times': op_times,
|
||||
'minus': op_minus,
|
||||
'root': sympy.sqrt,
|
||||
'power': sympy.Pow,
|
||||
'sin': sympy.sin,
|
||||
'cos': sympy.cos,
|
||||
'tan': sympy.tan,
|
||||
'cot': sympy.cot,
|
||||
'sinh': sympy.sinh,
|
||||
'cosh': sympy.cosh,
|
||||
'coth': sympy.coth,
|
||||
'tanh': sympy.tanh,
|
||||
'asin': sympy.asin,
|
||||
'acos': sympy.acos,
|
||||
'atan': sympy.atan,
|
||||
'atan2': sympy.atan2,
|
||||
'acot': sympy.acot,
|
||||
'asinh': sympy.asinh,
|
||||
'acosh': sympy.acosh,
|
||||
'atanh': sympy.atanh,
|
||||
'acoth': sympy.acoth,
|
||||
'exp': sympy.exp,
|
||||
'log': sympy.log,
|
||||
'ln': sympy.ln,
|
||||
}
|
||||
|
||||
def parse_presentation_symbol(xml):
|
||||
"""
|
||||
Parse <msub>, <msup>, <mi>, and <mn>
|
||||
"""
|
||||
tag = gettag(xml)
|
||||
if tag == 'mn':
|
||||
return xml.text
|
||||
elif tag == 'mi':
|
||||
return xml.text
|
||||
elif tag == 'msub':
|
||||
return '_'.join([parse_presentation_symbol(y) for y in xml])
|
||||
elif tag == 'msup':
|
||||
return '^'.join([parse_presentation_symbol(y) for y in xml])
|
||||
raise Exception('[parse_presentation_symbol] unknown tag %s' % tag)
|
||||
|
||||
# parser tree for Content MathML
|
||||
tag = gettag(xml)
|
||||
|
||||
# first do compound objects
|
||||
|
||||
if tag == 'apply': # apply operator
|
||||
opstr = gettag(xml[0])
|
||||
if opstr in opdict:
|
||||
op = opdict[opstr] # pylint: disable=invalid-name
|
||||
args = [self.make_sympy(expr) for expr in xml[1:]]
|
||||
try:
|
||||
res = op(*args)
|
||||
except Exception as err:
|
||||
self.args = args # pylint: disable=attribute-defined-outside-init
|
||||
self.op = op # pylint: disable=attribute-defined-outside-init, invalid-name
|
||||
raise Exception('[formula] error=%s failed to apply %s to args=%s' % (err, opstr, args)) # lint-amnesty, pylint: disable=raise-missing-from
|
||||
return res
|
||||
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(expr) for expr in xml]
|
||||
|
||||
elif tag == 'matrix':
|
||||
return sympy.Matrix([self.make_sympy(expr) for expr in xml])
|
||||
|
||||
elif tag == 'vector':
|
||||
return [self.make_sympy(expr) for expr in xml]
|
||||
|
||||
# atoms are below
|
||||
|
||||
elif tag == 'cn': # number
|
||||
return sympy.sympify(xml.text)
|
||||
|
||||
elif tag == 'ci': # variable (symbol)
|
||||
if len(xml) > 0 and (gettag(xml[0]) == 'msub' or gettag(xml[0]) == 'msup'): # subscript or superscript
|
||||
usym = parse_presentation_symbol(xml[0])
|
||||
sym = sympy.Symbol(str(usym))
|
||||
else:
|
||||
usym = six.text_type(xml.text)
|
||||
if 'hat' in usym:
|
||||
sym = my_sympify(usym)
|
||||
else:
|
||||
if usym == 'i' and self.options is not None and 'imaginary' in self.options: # i = sqrt(-1)
|
||||
sym = sympy.I
|
||||
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')
|
||||
@@ -1,337 +0,0 @@
|
||||
# lint-amnesty, pylint: disable=missing-module-docstring
|
||||
# !/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# File: symmath_check.py
|
||||
# Date: 02-May-12 (creation)
|
||||
#
|
||||
# Symbolic mathematical expression checker for edX. Uses sympy to check for expression equality.
|
||||
#
|
||||
# Takes in math expressions given as Presentation MathML (from ASCIIMathML), converts to Content MathML using SnuggleTeX
|
||||
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
from markupsafe import escape
|
||||
|
||||
from openedx.core.djangolib.markup import HTML
|
||||
|
||||
from .formula import * # lint-amnesty, pylint: disable=wildcard-import
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# check function interface
|
||||
#
|
||||
# This is one of the main entry points to call.
|
||||
|
||||
|
||||
def symmath_check_simple(expect, ans, adict={}, symtab=None, extra_options=None): # lint-amnesty, pylint: disable=dangerous-default-value, unused-argument
|
||||
"""
|
||||
Check a symbolic mathematical expression using sympy.
|
||||
The input is an ascii string (not MathML) converted to math using sympy.sympify.
|
||||
"""
|
||||
|
||||
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 as err: # lint-amnesty, pylint: disable=broad-except
|
||||
return {'ok': False,
|
||||
'msg': HTML('Error {err}<br/>Failed in evaluating check({expect},{ans})').format(
|
||||
err=err, expect=expect, ans=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): # lint-amnesty, pylint: disable=line-too-long
|
||||
"""
|
||||
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) == '': # lint-amnesty, pylint: disable=unneeded-not
|
||||
return {'ok': False, 'msg': ''}
|
||||
|
||||
try:
|
||||
xgiven = my_sympify(given, normphase, matrix, do_qubit=do_qubit, abcsym=abcsym, symtab=symtab)
|
||||
except Exception as err: # lint-amnesty, pylint: disable=broad-except
|
||||
return {'ok': False, 'msg': HTML('Error {err}<br/> in evaluating your expression "{given}"').format(
|
||||
err=err, given=given
|
||||
)}
|
||||
|
||||
try:
|
||||
xexpect = my_sympify(expect, normphase, matrix, do_qubit=do_qubit, abcsym=abcsym, symtab=symtab)
|
||||
except Exception as err: # lint-amnesty, pylint: disable=broad-except
|
||||
return {'ok': False, 'msg': HTML('Error {err}<br/> in evaluating OUR expression "{expect}"').format(
|
||||
err=err, expect=expect
|
||||
)}
|
||||
|
||||
if 'autonorm' in flags: # normalize trace of matrices
|
||||
try:
|
||||
xgiven /= xgiven.trace()
|
||||
except Exception as err: # lint-amnesty, pylint: disable=broad-except
|
||||
return {'ok': False, 'msg': HTML('Error {err}<br/> in normalizing trace of your expression {xgiven}').
|
||||
format(err=err, xgiven=to_latex(xgiven))}
|
||||
try:
|
||||
xexpect /= xexpect.trace()
|
||||
except Exception as err: # lint-amnesty, pylint: disable=broad-except
|
||||
return {'ok': False, 'msg': HTML('Error {err}<br/> in normalizing trace of OUR expression {xexpect}').
|
||||
format(err=err, xexpect=to_latex(xexpect))}
|
||||
|
||||
msg = 'Your expression was evaluated as ' + to_latex(xgiven)
|
||||
# msg += '<br/>Expected ' + to_latex(xexpect)
|
||||
|
||||
# msg += "<br/>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()) < threshold:
|
||||
return {'ok': True, 'msg': msg}
|
||||
else:
|
||||
pass
|
||||
#msg += "dm = " + to_latex(dm) + " diff = " + str(abs(dm.vec().norm().evalf()))
|
||||
#msg += "expect = " + to_latex(xexpect)
|
||||
elif dosimplify:
|
||||
if sympy.simplify(xexpect) == sympy.simplify(xgiven):
|
||||
return {'ok': True, 'msg': msg}
|
||||
elif numerical:
|
||||
if abs((xexpect - xgiven).evalf(chop=True)) < threshold:
|
||||
return {'ok': True, 'msg': msg}
|
||||
elif xexpect == xgiven:
|
||||
return {'ok': True, 'msg': msg}
|
||||
|
||||
#msg += "<p/>expect='%s', given='%s'" % (expect,given) # debugging
|
||||
# msg += "<p/> dot test " + to_latex(dot(sympy.Symbol('x'),sympy.Symbol('y')))
|
||||
return {'ok': False, 'msg': msg}
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# helper function to convert all <p> to <span class='inline-error'>
|
||||
|
||||
|
||||
def make_error_message(msg):
|
||||
# msg = msg.replace('<p>','<p><span class="inline-error">').replace('</p>','</span></p>')
|
||||
msg = HTML('<div class="capa_alert">{msg}</div>').format(msg=msg)
|
||||
return msg
|
||||
|
||||
|
||||
def is_within_tolerance(expected, actual, tolerance):
|
||||
if expected == 0:
|
||||
return abs(actual) < tolerance
|
||||
else:
|
||||
return abs(abs(actual - expected) / expected) < tolerance
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Check function interface, which takes pmathml input
|
||||
#
|
||||
# This is one of the main entry points to call.
|
||||
|
||||
|
||||
def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None): # lint-amnesty, pylint: disable=too-many-statements
|
||||
"""
|
||||
Check a symbolic mathematical expression using sympy.
|
||||
The input may be presentation MathML. Uses formula.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
msg = ''
|
||||
# msg += '<p/>abname=%s' % abname
|
||||
# msg += '<p/>adict=%s' % (repr(adict).replace('<','<'))
|
||||
|
||||
threshold = 1.0e-3 # for numerical comparison (also with matrices)
|
||||
DEBUG = debug
|
||||
|
||||
if xml is not None:
|
||||
DEBUG = xml.get('debug', False) # override debug flag using attribute in symbolicmath xml
|
||||
if DEBUG in ['0', 'False']:
|
||||
DEBUG = False
|
||||
|
||||
# options
|
||||
if options is None:
|
||||
options = ''
|
||||
do_matrix = 'matrix' in options
|
||||
do_qubit = 'qubit' in options
|
||||
do_numerical = 'numerical' in options
|
||||
|
||||
# parse expected answer
|
||||
try:
|
||||
fexpect = my_sympify(str(expect), matrix=do_matrix, do_qubit=do_qubit)
|
||||
except Exception as err: # lint-amnesty, pylint: disable=broad-except
|
||||
msg += HTML('<p>Error {err} in parsing OUR expected answer "{expect}"</p>').format(err=err, expect=expect)
|
||||
return {'ok': False, 'msg': make_error_message(msg)}
|
||||
|
||||
###### Sympy input #######
|
||||
# if expected answer is a number, try parsing provided answer as a number also
|
||||
try:
|
||||
fans = my_sympify(str(ans), matrix=do_matrix, do_qubit=do_qubit)
|
||||
except Exception as err: # lint-amnesty, pylint: disable=broad-except, unused-variable
|
||||
fans = None
|
||||
|
||||
# do a numerical comparison if both expected and answer are numbers
|
||||
if hasattr(fexpect, 'is_number') and fexpect.is_number \
|
||||
and hasattr(fans, 'is_number') and fans.is_number:
|
||||
if is_within_tolerance(fexpect, fans, threshold):
|
||||
return {'ok': True, 'msg': msg}
|
||||
else:
|
||||
msg += HTML('<p>You entered: {fans}</p>').format(fans=to_latex(fans))
|
||||
return {'ok': False, 'msg': msg}
|
||||
|
||||
if do_numerical: # numerical answer expected - force numerical comparison
|
||||
if is_within_tolerance(fexpect, fans, threshold):
|
||||
return {'ok': True, 'msg': msg}
|
||||
else:
|
||||
msg += HTML('<p>You entered: {fans} (note that a numerical answer is expected)</p>').\
|
||||
format(fans=to_latex(fans))
|
||||
return {'ok': False, 'msg': msg}
|
||||
|
||||
if fexpect == fans:
|
||||
msg += HTML('<p>You entered: {fans}</p>').format(fans=to_latex(fans))
|
||||
return {'ok': True, 'msg': msg}
|
||||
|
||||
###### PMathML input ######
|
||||
# convert mathml answer to formula
|
||||
try:
|
||||
mmlans = dynamath[0] if dynamath else None
|
||||
except Exception as err: # lint-amnesty, pylint: disable=broad-except
|
||||
mmlans = None
|
||||
if not mmlans:
|
||||
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('<','<')
|
||||
try:
|
||||
fsym = f.sympy
|
||||
msg += HTML('<p>You entered: {sympy}</p>').format(sympy=to_latex(f.sympy))
|
||||
except Exception as err: # lint-amnesty, pylint: disable=broad-except
|
||||
log.exception("Error evaluating expression '%s' as a valid equation", ans)
|
||||
msg += HTML("<p>Error in evaluating your expression '{ans}' as a valid equation</p>").format(ans=ans)
|
||||
if "Illegal math" in str(err):
|
||||
msg += HTML("<p>Illegal math expression</p>")
|
||||
if DEBUG:
|
||||
msg += HTML('Error: {err}<hr><p><font color="blue">DEBUG messages:</p><p><pre>{format_exc}</pre></p>'
|
||||
'<p>cmathml=<pre>{cmathml}</pre></p><p>pmathml=<pre>{pmathml}</pre></p><hr>').format(
|
||||
err=escape(str(err)), format_exc=traceback.format_exc(), cmathml=escape(f.cmathml),
|
||||
pmathml=escape(mmlans)
|
||||
)
|
||||
return {'ok': False, 'msg': make_error_message(msg)}
|
||||
|
||||
# do numerical comparison with expected
|
||||
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}
|
||||
msg += HTML("<p>Expecting a numerical answer!</p><p>given = {ans}</p><p>fsym = {fsym}</p>").format(
|
||||
ans=repr(ans), fsym=repr(fsym)
|
||||
)
|
||||
# msg += "<p>cmathml = <pre>%s</pre></p>" % str(f.cmathml).replace('<','<')
|
||||
return {'ok': False, 'msg': make_error_message(msg)}
|
||||
|
||||
# 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?
|
||||
if fexpect == fsym:
|
||||
return {'ok': True, 'msg': msg}
|
||||
|
||||
if isinstance(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()) < threshold:
|
||||
return {'ok': True, 'msg': msg}
|
||||
except sympy.ShapeError:
|
||||
msg += HTML("<p>Error - your input vector or matrix has the wrong dimensions")
|
||||
return {'ok': False, 'msg': make_error_message(msg)}
|
||||
except Exception as err: # lint-amnesty, pylint: disable=broad-except
|
||||
msg += HTML("<p>Error %s in comparing expected (a list) and your answer</p>").format(escape(str(err)))
|
||||
if DEBUG:
|
||||
msg += HTML("<p/><pre>{format_exc}</pre>").format(format_exc=traceback.format_exc())
|
||||
return {'ok': False, 'msg': make_error_message(msg)}
|
||||
|
||||
#diff = (fexpect-fsym).simplify()
|
||||
#fsym = fsym.simplify()
|
||||
#fexpect = fexpect.simplify()
|
||||
try:
|
||||
diff = (fexpect - fsym)
|
||||
except Exception as err: # lint-amnesty, pylint: disable=broad-except
|
||||
diff = None
|
||||
|
||||
if DEBUG:
|
||||
msg += HTML('<hr><p><font color="blue">DEBUG messages:</p><p>Got: {fsym}</p><p>Expecting: {fexpect}</p>')\
|
||||
.format(fsym=repr(fsym), fexpect=repr(fexpect).replace('**', '^').replace('hat(I)', 'hat(i)'))
|
||||
# msg += "<p/>Got: %s" % str([type(x) for x in fsym.atoms()]).replace('<','<')
|
||||
# msg += "<p/>Expecting: %s" % str([type(x) for x in fexpect.atoms()]).replace('<','<')
|
||||
if diff:
|
||||
msg += HTML("<p>Difference: {diff}</p>").format(diff=to_latex(diff))
|
||||
msg += HTML('<hr>')
|
||||
|
||||
# Used to return more keys: 'ex': fexpect, 'got': fsym
|
||||
return {'ok': False, 'msg': msg}
|
||||
@@ -1,117 +0,0 @@
|
||||
"""
|
||||
Tests of symbolic math
|
||||
"""
|
||||
|
||||
import re
|
||||
import unittest
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from . import formula
|
||||
|
||||
|
||||
def stripXML(xml):
|
||||
xml = xml.replace('\n', '')
|
||||
xml = re.sub(r'\> +\<', '><', xml)
|
||||
return xml
|
||||
|
||||
|
||||
class FormulaTest(unittest.TestCase): # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
# for readability later
|
||||
mathml_start = '<math xmlns="http://www.w3.org/1998/Math/MathML"><mstyle displaystyle="true">'
|
||||
mathml_end = '</mstyle></math>'
|
||||
|
||||
def setUp(self):
|
||||
super(FormulaTest, self).setUp() # lint-amnesty, pylint: disable=super-with-arguments
|
||||
self.formulaInstance = formula('')
|
||||
|
||||
def test_replace_mathvariants(self):
|
||||
expr = '''
|
||||
<mstyle mathvariant="script">
|
||||
<mi>N</mi>
|
||||
</mstyle>'''
|
||||
|
||||
expected = '<mi>scriptN</mi>'
|
||||
|
||||
# wrap
|
||||
expr = stripXML(self.mathml_start + expr + self.mathml_end)
|
||||
expected = stripXML(self.mathml_start + expected + self.mathml_end)
|
||||
|
||||
# process the expression
|
||||
xml = etree.fromstring(expr)
|
||||
xml = self.formulaInstance.preprocess_pmathml(xml)
|
||||
test = etree.tostring(xml)
|
||||
|
||||
# success?
|
||||
assert test.decode('utf-8') == expected
|
||||
|
||||
def test_fix_simple_superscripts(self):
|
||||
expr = '''
|
||||
<msup>
|
||||
<mi>a</mi>
|
||||
<mrow>
|
||||
<mo>​</mo>
|
||||
<mi>b</mi>
|
||||
</mrow>
|
||||
</msup>'''
|
||||
|
||||
expected = '<mi>a__b</mi>'
|
||||
|
||||
# wrap
|
||||
expr = stripXML(self.mathml_start + expr + self.mathml_end)
|
||||
expected = stripXML(self.mathml_start + expected + self.mathml_end)
|
||||
|
||||
# process the expression
|
||||
xml = etree.fromstring(expr)
|
||||
xml = self.formulaInstance.preprocess_pmathml(xml)
|
||||
test = etree.tostring(xml)
|
||||
|
||||
# success?
|
||||
assert test.decode('utf-8') == expected
|
||||
|
||||
def test_fix_complex_superscripts(self):
|
||||
expr = '''
|
||||
<msubsup>
|
||||
<mi>a</mi>
|
||||
<mi>b</mi>
|
||||
<mrow>
|
||||
<mo>​</mo>
|
||||
<mi>c</mi>
|
||||
</mrow>
|
||||
</msubsup>'''
|
||||
|
||||
expected = '<mi>a_b__c</mi>'
|
||||
|
||||
# wrap
|
||||
expr = stripXML(self.mathml_start + expr + self.mathml_end)
|
||||
expected = stripXML(self.mathml_start + expected + self.mathml_end)
|
||||
|
||||
# process the expression
|
||||
xml = etree.fromstring(expr)
|
||||
xml = self.formulaInstance.preprocess_pmathml(xml)
|
||||
test = etree.tostring(xml)
|
||||
|
||||
# success?
|
||||
assert test.decode('utf-8') == expected
|
||||
|
||||
def test_fix_msubsup(self):
|
||||
expr = '''
|
||||
<msubsup>
|
||||
<mi>a</mi>
|
||||
<mi>b</mi>
|
||||
<mi>c</mi>
|
||||
</msubsup>'''
|
||||
|
||||
expected = '<msup><mi>a_b</mi><mi>c</mi></msup>' # which is (a_b)^c
|
||||
|
||||
# wrap
|
||||
expr = stripXML(self.mathml_start + expr + self.mathml_end)
|
||||
expected = stripXML(self.mathml_start + expected + self.mathml_end)
|
||||
|
||||
# process the expression
|
||||
xml = etree.fromstring(expr)
|
||||
xml = self.formulaInstance.preprocess_pmathml(xml)
|
||||
test = etree.tostring(xml)
|
||||
|
||||
# success?
|
||||
assert test.decode('utf-8') == expected
|
||||
@@ -1,89 +0,0 @@
|
||||
# lint-amnesty, pylint: disable=missing-module-docstring
|
||||
from unittest import TestCase
|
||||
|
||||
from six.moves import range
|
||||
|
||||
from .symmath_check import symmath_check
|
||||
|
||||
|
||||
class SymmathCheckTest(TestCase): # lint-amnesty, pylint: disable=missing-class-docstring
|
||||
def test_symmath_check_integers(self):
|
||||
number_list = range(-100, 100)
|
||||
self._symmath_check_numbers(number_list)
|
||||
|
||||
def test_symmath_check_floats(self):
|
||||
number_list = [i + 0.01 for i in range(-100, 100)]
|
||||
self._symmath_check_numbers(number_list)
|
||||
|
||||
def test_symmath_check_same_symbols(self):
|
||||
expected_str = "x+2*y"
|
||||
dynamath = '''
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mstyle displaystyle="true">
|
||||
<mrow>
|
||||
<mi>x</mi>
|
||||
<mo>+</mo>
|
||||
<mn>2</mn>
|
||||
<mo>*</mo>
|
||||
<mi>y</mi>
|
||||
</mrow>
|
||||
</mstyle>
|
||||
</math>'''.strip()
|
||||
|
||||
# Expect that the exact same symbolic string is marked correct
|
||||
result = symmath_check(expected_str, expected_str, dynamath=[dynamath])
|
||||
assert (('ok' in result) and result['ok'])
|
||||
|
||||
def test_symmath_check_equivalent_symbols(self):
|
||||
expected_str = "x+2*y"
|
||||
input_str = "x+y+y"
|
||||
dynamath = '''
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mstyle displaystyle="true">
|
||||
<mrow>
|
||||
<mi>x</mi>
|
||||
<mo>+</mo>
|
||||
<mi>y</mi>
|
||||
<mo>+</mo>
|
||||
<mi>y</mi>
|
||||
</mrow>
|
||||
</mstyle>
|
||||
</math>'''.strip()
|
||||
|
||||
# Expect that equivalent symbolic strings are marked correct
|
||||
result = symmath_check(expected_str, input_str, dynamath=[dynamath])
|
||||
assert (('ok' in result) and result['ok'])
|
||||
|
||||
def test_symmath_check_different_symbols(self):
|
||||
expected_str = "0"
|
||||
input_str = "x+y"
|
||||
dynamath = '''
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mstyle displaystyle="true">
|
||||
<mrow>
|
||||
<mi>x</mi>
|
||||
<mo>+</mo>
|
||||
<mi>y</mi>
|
||||
</mrow>
|
||||
</mstyle>
|
||||
</math>'''.strip()
|
||||
|
||||
# Expect that an incorrect response is marked incorrect
|
||||
result = symmath_check(expected_str, input_str, dynamath=[dynamath])
|
||||
assert (('ok' in result) and (not result['ok']))
|
||||
assert 'fail' not in result['msg']
|
||||
|
||||
def _symmath_check_numbers(self, number_list): # lint-amnesty, pylint: disable=missing-function-docstring
|
||||
|
||||
for n in number_list:
|
||||
|
||||
# expect = ans, so should say the answer is correct
|
||||
expect = n
|
||||
ans = n
|
||||
result = symmath_check(str(expect), str(ans))
|
||||
assert (('ok' in result) and result['ok']), ('%f should == %f' % (expect, ans))
|
||||
|
||||
# Change expect so that it != ans
|
||||
expect += 0.1
|
||||
result = symmath_check(str(expect), str(ans))
|
||||
assert (('ok' in result) and (not result['ok'])), ('%f should != %f' % (expect, ans))
|
||||
@@ -22,7 +22,6 @@ sys.path.insert(0, root)
|
||||
sys.path.append(root / "docs/guides")
|
||||
sys.path.append(root / "common/lib/capa")
|
||||
sys.path.append(root / "common/lib/safe_lxml")
|
||||
sys.path.append(root / "common/lib/symmath")
|
||||
sys.path.append(root / "common/lib/xmodule")
|
||||
|
||||
# Use a settings module that allows all LMS and Studio code to be imported
|
||||
@@ -225,7 +224,6 @@ modules = {
|
||||
'cms': 'cms',
|
||||
'common/lib/capa/capa': 'common/lib/capa',
|
||||
'common/lib/safe_lxml/safe_lxml': 'common/lib/safe_lxml',
|
||||
'common/lib/symmath/symmath': 'common/lib/symmath',
|
||||
'common/lib/xmodule/xmodule': 'common/lib/xmodule',
|
||||
'lms': 'lms',
|
||||
'openedx': 'openedx',
|
||||
|
||||
@@ -10,5 +10,4 @@ out from edx-platform into separate packages at some point.
|
||||
|
||||
common/lib/capa/modules
|
||||
common/lib/safe_lxml/modules
|
||||
common/lib/symmath/modules
|
||||
common/lib/xmodule/modules
|
||||
|
||||
@@ -97,8 +97,14 @@ py2neo<2022
|
||||
# Sphinx requires docutils<0.18. This pin can be removed once https://github.com/sphinx-doc/sphinx/issues/9777 is closed.
|
||||
docutils<0.18
|
||||
|
||||
# Temporary constraint on openedx-calc. Latest openedx-calc also contains symmath, which may upend symmath package version
|
||||
openedx-calc==2.0.1
|
||||
|
||||
# scipy version 1.8 requires numpy>=1.17.3, we've pinned numpy to <1.17.0 in requirements/edx-sandbox/py38.in
|
||||
scipy<1.8.0
|
||||
|
||||
# mistune is a dependency of m2r (which is a dependency of sphinxcontrib-openapi)
|
||||
# m2r fails to specify the version of mistune that it needs leading to the error message:
|
||||
# AttributeError: module 'mistune' has no attribute 'BlockGrammar'
|
||||
# See Issue: https://github.com/miyakogi/m2r/issues/66
|
||||
# m2r is no longer actively maintained: https://github.com/miyakogi/m2r/pull/43
|
||||
# This will be fixed when sphinxcontrib-openapi depends on m2r2 instead of m2r
|
||||
# See issue: https://github.com/sphinx-contrib/openapi/issues/123
|
||||
mistune<2.0.0
|
||||
|
||||
@@ -20,4 +20,3 @@ numpy>=1.16.0,<1.17.0
|
||||
# NOTE: if you change code in these packages, you MUST change the version
|
||||
# number in its setup.py or the code WILL NOT be installed during deploy.
|
||||
-e common/lib/sandbox-packages
|
||||
-e common/lib/symmath
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
#
|
||||
common/lib/sandbox-packages
|
||||
# via -r requirements/edx-sandbox/py38.in
|
||||
common/lib/symmath
|
||||
# via -r requirements/edx-sandbox/py38.in
|
||||
cffi==1.15.0
|
||||
# via cryptography
|
||||
chem==1.2.0
|
||||
@@ -30,8 +28,11 @@ lxml==4.5.0
|
||||
# via
|
||||
# -c requirements/edx-sandbox/../constraints.txt
|
||||
# -r requirements/edx-sandbox/py38.in
|
||||
# openedx-calc
|
||||
markupsafe==2.0.1
|
||||
# via chem
|
||||
# via
|
||||
# chem
|
||||
# openedx-calc
|
||||
matplotlib==3.3.4
|
||||
# via
|
||||
# -c requirements/edx-sandbox/../constraints.txt
|
||||
@@ -53,10 +54,8 @@ numpy==1.16.6
|
||||
# matplotlib
|
||||
# openedx-calc
|
||||
# scipy
|
||||
openedx-calc==2.0.1
|
||||
# via
|
||||
# -c requirements/edx-sandbox/../constraints.txt
|
||||
# -r requirements/edx-sandbox/py38.in
|
||||
openedx-calc==3.0.1
|
||||
# via -r requirements/edx-sandbox/py38.in
|
||||
pillow==9.0.1
|
||||
# via matplotlib
|
||||
pycparser==2.21
|
||||
@@ -89,6 +88,6 @@ sympy==1.6.2
|
||||
# via
|
||||
# -c requirements/edx-sandbox/../constraints.txt
|
||||
# -r requirements/edx-sandbox/py38.in
|
||||
# symmath
|
||||
# openedx-calc
|
||||
tqdm==4.62.3
|
||||
# via nltk
|
||||
|
||||
@@ -24,8 +24,6 @@
|
||||
# via -r requirements/edx/local.in
|
||||
-e common/lib/sandbox-packages
|
||||
# via -r requirements/edx/local.in
|
||||
-e common/lib/symmath
|
||||
# via -r requirements/edx/local.in
|
||||
-e openedx/core/lib/xblock_builtin/xblock_discussion
|
||||
# via -r requirements/edx/local.in
|
||||
-e git+https://github.com/edx-solutions/xblock-google-drive.git@2d176468e33c0713c911b563f8f65f7cf232f5b6#egg=xblock-google-drive
|
||||
@@ -628,6 +626,7 @@ lxml==4.5.0
|
||||
# edxval
|
||||
# lti-consumer-xblock
|
||||
# olxcleaner
|
||||
# openedx-calc
|
||||
# ora2
|
||||
# safe-lxml
|
||||
# xblock
|
||||
@@ -655,6 +654,7 @@ markupsafe==2.0.1
|
||||
# chem
|
||||
# jinja2
|
||||
# mako
|
||||
# openedx-calc
|
||||
# xblock
|
||||
maxminddb==2.2.0
|
||||
# via geoip2
|
||||
@@ -702,10 +702,8 @@ oauthlib==3.0.1
|
||||
# lti-consumer-xblock
|
||||
# requests-oauthlib
|
||||
# social-auth-core
|
||||
openedx-calc==2.0.1
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/base.in
|
||||
openedx-calc==3.0.1
|
||||
# via -r requirements/edx/base.in
|
||||
openedx-events==0.7.1
|
||||
# via -r requirements/edx/base.in
|
||||
openedx-filters==0.4.3
|
||||
@@ -1004,7 +1002,7 @@ super-csv==2.1.4
|
||||
sympy==1.6.2
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# symmath
|
||||
# openedx-calc
|
||||
tableauserverclient==0.17.0
|
||||
# via edx-enterprise
|
||||
testfixtures==6.18.3
|
||||
|
||||
@@ -24,8 +24,6 @@
|
||||
# via -r requirements/edx/testing.txt
|
||||
-e common/lib/sandbox-packages
|
||||
# via -r requirements/edx/testing.txt
|
||||
-e common/lib/symmath
|
||||
# via -r requirements/edx/testing.txt
|
||||
-e openedx/core/lib/xblock_builtin/xblock_discussion
|
||||
# via -r requirements/edx/testing.txt
|
||||
-e git+https://github.com/edx-solutions/xblock-google-drive.git@2d176468e33c0713c911b563f8f65f7cf232f5b6#egg=xblock-google-drive
|
||||
@@ -838,6 +836,7 @@ lxml==4.5.0
|
||||
# edxval
|
||||
# lti-consumer-xblock
|
||||
# olxcleaner
|
||||
# openedx-calc
|
||||
# ora2
|
||||
# pyquery
|
||||
# safe-lxml
|
||||
@@ -870,6 +869,7 @@ markupsafe==2.0.1
|
||||
# chem
|
||||
# jinja2
|
||||
# mako
|
||||
# openedx-calc
|
||||
# xblock
|
||||
maxminddb==2.2.0
|
||||
# via
|
||||
@@ -879,8 +879,10 @@ mccabe==0.6.1
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
# pylint
|
||||
mistune==2.0.2
|
||||
# via m2r
|
||||
mistune==0.8.4
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# m2r
|
||||
mock==4.0.3
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
@@ -934,10 +936,8 @@ oauthlib==3.0.1
|
||||
# lti-consumer-xblock
|
||||
# requests-oauthlib
|
||||
# social-auth-core
|
||||
openedx-calc==2.0.1
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
openedx-calc==3.0.1
|
||||
# via -r requirements/edx/testing.txt
|
||||
openedx-events==0.7.1
|
||||
# via -r requirements/edx/testing.txt
|
||||
openedx-filters==0.4.3
|
||||
@@ -1422,7 +1422,7 @@ sympy==1.6.2
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/testing.txt
|
||||
# symmath
|
||||
# openedx-calc
|
||||
tableauserverclient==0.17.0
|
||||
# via
|
||||
# -r requirements/edx/testing.txt
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
-e common/lib/capa
|
||||
-e common/lib/safe_lxml
|
||||
-e common/lib/sandbox-packages
|
||||
-e common/lib/symmath
|
||||
-e common/lib/xmodule
|
||||
|
||||
-e openedx/core/lib/xblock_builtin/xblock_discussion
|
||||
|
||||
@@ -24,8 +24,6 @@
|
||||
# via -r requirements/edx/base.txt
|
||||
-e common/lib/sandbox-packages
|
||||
# via -r requirements/edx/base.txt
|
||||
-e common/lib/symmath
|
||||
# via -r requirements/edx/base.txt
|
||||
-e openedx/core/lib/xblock_builtin/xblock_discussion
|
||||
# via -r requirements/edx/base.txt
|
||||
-e git+https://github.com/edx-solutions/xblock-google-drive.git@2d176468e33c0713c911b563f8f65f7cf232f5b6#egg=xblock-google-drive
|
||||
@@ -797,6 +795,7 @@ lxml==4.5.0
|
||||
# edxval
|
||||
# lti-consumer-xblock
|
||||
# olxcleaner
|
||||
# openedx-calc
|
||||
# ora2
|
||||
# pyquery
|
||||
# safe-lxml
|
||||
@@ -828,6 +827,7 @@ markupsafe==2.0.1
|
||||
# chem
|
||||
# jinja2
|
||||
# mako
|
||||
# openedx-calc
|
||||
# xblock
|
||||
maxminddb==2.2.0
|
||||
# via
|
||||
@@ -884,10 +884,8 @@ oauthlib==3.0.1
|
||||
# lti-consumer-xblock
|
||||
# requests-oauthlib
|
||||
# social-auth-core
|
||||
openedx-calc==2.0.1
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/base.txt
|
||||
openedx-calc==3.0.1
|
||||
# via -r requirements/edx/base.txt
|
||||
openedx-events==0.7.1
|
||||
# via -r requirements/edx/base.txt
|
||||
openedx-filters==0.4.3
|
||||
@@ -1317,7 +1315,7 @@ sympy==1.6.2
|
||||
# via
|
||||
# -c requirements/edx/../constraints.txt
|
||||
# -r requirements/edx/base.txt
|
||||
# symmath
|
||||
# openedx-calc
|
||||
tableauserverclient==0.17.0
|
||||
# via
|
||||
# -r requirements/edx/base.txt
|
||||
|
||||
@@ -17,7 +17,7 @@ function clean_file {
|
||||
# Workaround for https://github.com/jazzband/pip-tools/issues/204 -
|
||||
# change absolute paths for local editable packages back to relative ones
|
||||
FILE_CONTENT=$(<${FILE_PATH})
|
||||
FILE_URL_REGEX="-e (file:///[^"$'\n'"]*)/common/lib/symmath"
|
||||
FILE_URL_REGEX="-e (file:///[^"$'\n'"]*)/common/lib/\w+"
|
||||
if [[ "${FILE_CONTENT}" =~ ${FILE_URL_REGEX} ]]; then
|
||||
BASE_FILE_URL=${BASH_REMATCH[1]}
|
||||
sed "s|$BASE_FILE_URL/||" ${FILE_PATH} > ${TEMP_FILE}
|
||||
|
||||
@@ -38,7 +38,7 @@ exclude+='|^common/test/data/?.*$'
|
||||
# * common/lib/xmodule -> EXCLUDE from check.
|
||||
# * common/lib/xmodule/xmodule/modulestore -> INCLUDE in check.
|
||||
exclude+='|^common/lib$'
|
||||
exclude+='|^common/lib/(capa|safe_lxml|sandbox-packages|symmath|xmodule)$'
|
||||
exclude+='|^common/lib/(capa|safe_lxml|sandbox-packages|xmodule)$'
|
||||
|
||||
# Docs, scripts.
|
||||
exclude+='|^docs/.*$'
|
||||
|
||||
Reference in New Issue
Block a user