Repository: ModellingWebLab/cellmlmanip

Issue:
Expand the range of supported MathML
We should probably support the [CellML subset](https://www.cellml.org/specifications/cellml_1.1/#sec_math_cellml_subset) eventually (see also in [CellML 2](https://docs.google.com/document/d/1MleZJF6DYp4FO4vxslAHInrRFMqrSpARa1s_dPhwYU0/edit#heading=h.zegia3mrify8)). Not urgent though - just the elements in the models we have currently is sufficient to get started :)

Discussion:

Patch:
diff --git a/cellmlmanip/mathml2sympy/transpiler.py b/cellmlmanip/mathml2sympy/transpiler.py
index 4a20482..e0d7ad5 100644
--- a/cellmlmanip/mathml2sympy/transpiler.py
+++ b/cellmlmanip/mathml2sympy/transpiler.py
@@ -3,6 +3,7 @@ Parses Content MathML and returns equivalent SymPy expressions
 
 Content Markup specification: https://www.w3.org/TR/MathML2/chapter4.html
 """
+import logging
 from xml.dom import Node, minidom
 
 import sympy
@@ -45,21 +46,16 @@ def transpile(xml_node):
             # (see cn_handler for an example), show a message
             text = child_node.data.strip()
             if text:
-                print('Hit text node with text "' + text + '"')
+                logging.warning('Unhandled text node in <%s>: "%s"', child_node.tagName, text)
         elif child_node.nodeType == child_node.ELEMENT_NODE:
             # Call the appropriate MathML handler function for this tag
-            name = child_node.tagName
-            if name in HANDLERS:
-                # If this tag element itself has children
-                if child_node.childNodes:
-                    # We want to pass the node to the handler, and it will deal with children
-                    sympy_expressions.append(HANDLERS[name](child_node))
-                else:
-                    # This tag has no children
-                    sympy_expressions.append(HANDLERS[name]())
+            tag_name = child_node.tagName
+            if tag_name in HANDLERS:
+                sympy_expressions.append(HANDLERS[tag_name](child_node))
+                logging.debug('Transpiled node %s ⟶ %s', child_node.toxml(), sympy_expressions[-1])
             else:
                 # MathML handler function not found for this tag!
-                raise NotImplementedError('No handler for element <%s>' % child_node.tagName)
+                raise NotImplementedError('No handler for element <%s>' % tag_name)
         elif child_node.nodeType not in [Node.COMMENT_NODE, Node.PROCESSING_INSTRUCTION_NODE]:
             raise NotImplementedError('Unknown node type %d' % child_node.nodeType)
     return sympy_expressions
@@ -81,7 +77,6 @@ def ci_handler(node):
     """
     MathML:  https://www.w3.org/TR/MathML2/chapter4.html#contm.ci
     SymPy: http://docs.sympy.org/latest/modules/core.html#id17
-    TODO: 'type' attribute?
     """
     identifier = node.childNodes[0].data.strip()
     return sympy.Symbol(identifier)
@@ -91,8 +86,26 @@ def cn_handler(node):
     """
     MathML: https://www.w3.org/TR/MathML2/chapter4.html#contm.cn
     SymPy: http://docs.sympy.org/latest/modules/core.html#number
-    TODO: 'type' attribute?
     """
+
+    # If this number is using scientific notation
+    if 'type' in node.attributes:
+        if node.attributes['type'].value == 'e-notation':
+            # A real number may also be presented in scientific notation. Such numbers have two
+            # parts (a mantissa and an exponent) separated by sep. The first part is a real number,
+            # while the second part is an integer exponent indicating a power of the base.
+            # For example, 12.3<sep/>5 represents 12.3 times 10^5. The default presentation of
+            # this example is 12.3e5.
+            if len(node.childNodes) == 3 and node.childNodes[1].tagName == 'sep':
+                mantissa = node.childNodes[0].data.strip()
+                exponent = int(node.childNodes[2].data.strip())
+                return sympy.Float('%se%d' % (mantissa, exponent))
+            else:
+                raise SyntaxError('Expecting <cn type="e-notation">significand<sep/>exponent</cn>.'
+                                  'Got: ' + node.toxml())
+        raise NotImplementedError('Unimplemented type attribute for <cn>: '
+                                  + node.attributes['type'].value)
+
     number = float(node.childNodes[0].data.strip())
     return sympy.Number(number)
 
@@ -104,6 +117,9 @@ def apply_handler(node):
     https://www.w3.org/TR/MathML2/chapter4.html#contm.apply
     """
     result = transpile(node)
+
+    logging.debug('Result of <apply>:\n\t%s\t⟶\t%s', node.toxml(), result)
+
     if len(result) > 1:
         expression = result[0](*(result[1:]))
     else:
@@ -147,23 +163,7 @@ def otherwise_handler(node):
 
 # ARITHMETIC, ALGEBRA AND LOGIC ################################################################
 
-def plus_handler():
-    """
-    MathML: https://www.w3.org/TR/MathML2/chapter4.html#contm.plus
-    n-ary arithmetic operator
-    """
-    return sympy.Add
-
-
-def times_handler():
-    """
-    https://www.w3.org/TR/MathML2/chapter4.html#contm.times
-    n-ary arithmetic operator
-    """
-    return sympy.Mul
-
-
-def minus_handler():
+def minus_handler(node):
     """
     https://www.w3.org/TR/MathML2/chapter4.html#contm.minus
     unary arithmetic operator OR binary arithmetic operator
@@ -175,16 +175,16 @@ def minus_handler():
     * Negation (-a) is equivalent to sympy.Mul(sympy.S.NegativeOne, a)
     * Subtraction (a - b) is equivalent to sympy.Add(a, sympy.Mul(sympy.S.NegativeOne, b))
     """
-    def __wrapped_minus(left_operand, right_operand=None):
+    def _wrapped_minus(left_operand, right_operand=None):
         if right_operand is None:
             # unary arithmetic operator => negation
             return -left_operand
         # otherwise, binary arithmetic operator => subtraction
         return left_operand - right_operand
-    return __wrapped_minus
+    return _wrapped_minus
 
 
-def divide_handler():
+def divide_handler(node):
     """
     https://www.w3.org/TR/MathML2/chapter4.html#contm.divide
     binary arithmetic operator
@@ -192,130 +192,75 @@ def divide_handler():
 
     Equivalent to sympy.Mul(a, sympy.Pow(b, sympy.S.NegativeOne))
     """
-    def __wrapped_divide(dividend, divisor):
+    def _wrapped_divide(dividend, divisor):
         return dividend / divisor
-    return __wrapped_divide
+    return _wrapped_divide
 
 
-def rem_handler():
-    """
-    https://www.w3.org/TR/MathML2/chapter4.html#contm.rem
-    binary arithmetic operator
-    """
-    return sympy.Mod
-
-
-def floor_handler():
-    """
-    https://www.w3.org/TR/MathML2/chapter4.html#contm.floor
-    unary operator
-    """
-    return sympy.floor
-
-
-def abs_handler():
-    """
-    https://www.w3.org/TR/MathML2/chapter4.html#contm.abs
-    unary arithmetic operator
-    """
-    return sympy.Abs
-
-
-def power_handler():
+def power_handler(node):
     """
     https://www.w3.org/TR/MathML2/chapter4.html#contm.power
     binary arithmetic operator
     equivalent to sympy.Pow(a, b)
     """
-    def __wrapped_power(base, exponent):
+    def _wrapped_power(base, exponent):
         return base ** exponent
-    return __wrapped_power
+    return _wrapped_power
 
 
-def root_handler():
+def root_handler(node):
     """
     https://www.w3.org/TR/MathML2/chapter4.html#contm.root
     operator taking qualifiers
-    TODO: implement <degree>
-    """
-    def __wrapped_root(radicand, degree=None):
-        if degree is None:
-            # by default, sqrt
-            return sympy.root(radicand, 2)
-        else:
-            raise NotImplementedError
-            # return sympy.root(b, a)
-    return __wrapped_root
-
 
-# RELATIONS ####################################################################################
-
-def eq_handler():
-    """
-    https://www.w3.org/TR/MathML2/chapter4.html#contm.eq
-    n-ary operator
+    Nasty:
+    The root element is used to construct roots. The kind of root to be taken is specified by a
+    degree element, which should be given as the second child of the apply element enclosing the
+    root element. Thus, square roots correspond to the case where degree contains the value 2, cube
+    roots correspond to 3, and so on. If no degree is present, a default value of 2 is used.
     """
-    return sympy.Eq
+    def _wrapped_root(first_argument, second_argument=None):
+        # if no <degree> given, it's sqrt
+        if second_argument is None:
+            return sympy.root(first_argument, 2)
+        return sympy.root(second_argument, first_argument)
+    return _wrapped_root
 
 
-def leq_handler():
-    """
-    https://www.w3.org/TR/MathML2/chapter4.html#contm.leq
+def degree_handler(node):
     """
-    return sympy.Le
-
-
-def lt_handler():
-    """
-    https://www.w3.org/TR/MathML2/chapter4.html#contm.lt
-    """
-    return sympy.Lt
-
-
-def geq_handler():
+    https://www.w3.org/TR/MathML2/chapter4.html#contm.degree
+    Meaning of <degree> depends on context! We implement it for order of <bvar> in <diff> and
+    the kind of root in <root>
     """
-    https://www.w3.org/TR/MathML2/chapter4.html#contm.geq
-    n-ary relation
-    """
-    return sympy.Ge
-
-
-def gt_handler():
-    """
-    https://www.w3.org/TR/MathML2/chapter4.html#contm.gt
-    n-ary relation
-    """
-    return sympy.Gt
-
-
-def and_handler():
-    """
-    https://www.w3.org/TR/MathML2/chapter4.html#contm.and
-    n-ary operator
-    """
-    return sympy.And
-
-
-def or_handler():
-    """
-    https://www.w3.org/TR/MathML2/chapter4.html#contm.or
-    n-ary operator
-    """
-    return sympy.Or
+    result = transpile(node)
+    if len(result) != 1:
+        raise ValueError('Expected single value in <degree> tag.'
+                         'Got: ' + node.toxml())
+    return result[0]
 
 
 # CALCULUS AND VECTOR CALCULUS #################################################################
 
-def diff_handler():
+def diff_handler(node):
     """
     https://www.w3.org/TR/MathML2/chapter4.html#contm.diff
     operator taking qualifiers
     """
-    def __wrapped_diff(x_symbol, y_symbol, evaluate=False):
+    def _wrapped_diff(x_symbol, y_symbol, evaluate=False):
         # dx / dy
-        y_function = sympy.Function(y_symbol.name)  # given by child element <bvar>
+        y_function = sympy.Function(y_symbol.name)
+
+        # if bound variable element <bvar> contains <degree>, argument x_symbol is a list,
+        # otherwise, it is a symbol
+        if isinstance(x_symbol, list) and len(x_symbol) == 2:
+            bound_variable = x_symbol[0]
+            order = int(x_symbol[1])
+            return sympy.Derivative(y_function(bound_variable), bound_variable, order,
+                                    evaluate=evaluate)
+
         return sympy.Derivative(y_function(x_symbol), x_symbol, evaluate=evaluate)
-    return __wrapped_diff
+    return _wrapped_diff
 
 
 def bvar_handler(node):
@@ -324,112 +269,166 @@ def bvar_handler(node):
     NASTY: bvar element depends on the context it is being used
     In a derivative, it indicates the variable with respect to which a function is being
     differentiated.
+
+    The bound variable <bvar> can also specify degree. In this case, we'll have two elements
     """
     result = transpile(node)
-    if len(result) > 1:
-        raise NotImplementedError('multiple <bvar> not implemented')
-    return result[0]
+    if len(result) == 1:
+        # Bound variable without specifying degree
+        return result[0]
+    elif len(result) == 2:
+        return result
+    else:
+        raise SyntaxError("Don't know how to handle <bvar> " + node.toxml())
 
 
 # ELEMENTARY CLASSICAL FUNCTIONS ###############################################################
 
-def exp_handler():
-    """
-    https://www.w3.org/TR/MathML2/chapter4.html#contm.exp
-    unary arithmetic operator
-    """
-    return sympy.exp
-
-
-def ln_handler():
-    """
-    https://www.w3.org/TR/MathML2/chapter4.html#contm.ln
-    unary calculus operator
-    """
-    return sympy.ln
-
-
-def log_handler():
+def log_handler(node):
     """
     https://www.w3.org/TR/MathML2/chapter4.html#contm.log
     operator taking qualifiers or a unary calculus operator
-    TODO: implement <logbase>
     """
-    def __wrapped_log(term, base=None):
-        if base is None:
+    def _wrapped_log(first_element, second_element=None):
+        if second_element is None:
             # if no <logbase> element is present, the base is assumed to be 10
-            return sympy.log(term, 10)
-        else:
-            # return sympy.log(b, a)
-            raise NotImplementedError
-    return __wrapped_log
+            return sympy.log(first_element, 10)
 
+        # Has <logbase> element, which is the first_element after <log/>
+        return sympy.log(second_element, first_element)
+    return _wrapped_log
 
-def cos_handler():
+
+def logbase_handler(node):
     """
-    https://www.w3.org/TR/MathML2/chapter4.html#contm.trig
-    unary trigonometric operator
+    Qualifier for <log>
+
+    The log function accepts only the logbase schema. If present, the logbase schema denotes the
+    base with respect to which the logarithm is being taken. Otherwise, the log is assumed to be b
+    ase 10. When used with log, the logbase schema is expected to contain a single child schema;
+    otherwise an error is generated.
+
+    Should be the first element following log, i.e. the second child of the containing apply
+    element.
     """
-    return sympy.cos
+    return transpile(node)[0]
 
 
-def tanh_handler():
+def get_nary_relation_callback(sympy_relation):
     """
-    https://www.w3.org/TR/MathML2/chapter4.html#contm.trig
-    unary trigonometric operator
+    Wraps the Sympy binary relation to handle n-ary MathML relations
+
+    :param sympy_relation: handle for binary Sympy relation (Eq, Le, Lt, Ge, Gt)
+    :return: callback used by the apply_handler to handle n-ary relations
     """
-    return sympy.tanh
+    def _wrapper_relational(*expressions):
+        # If the MathML relation is chaining more than 2 expressions
+        if len(expressions) > 2:
+            # Convert to multiple Sympy binary relations bundled in an 'And' boolean
+            relations = []
+            for first, second in zip(expressions[:-1], expressions[1:]):
+                relations.append(sympy_relation(first, second))
+            return sympy.And(*relations)
+        return sympy_relation(*expressions)
+    return _wrapper_relational
 
 
-def arccos_handler():
+def simple_operator_handler(node):
     """
-    https://www.w3.org/TR/MathML2/chapter4.html#contm.trig
-    unary trigonometric operator
+    This function handles simple MathML <tagName> to sympy.Class operators, where no unique handling
+    of tag children etc. is required.
     """
-    return sympy.acos
+    tag_name = node.tagName
 
+    handler = getattr(sympy, SIMPLE_MATHML_TO_SYMPY_NAMES[tag_name])
 
-# CONSTANT AND SYMBOL ELEMENTS #################################################################
+    # Some MathML relations allow chaining but Sympy relations are binary operations
+    if tag_name in MATHML_NARY_RELATIONS:
+        return get_nary_relation_callback(handler)
 
-def pi_handler():
-    """
-    https://www.w3.org/TR/MathML2/chapter4.html#contm.pi
-    """
-    return sympy.pi
+    return handler
 
 
 # END OF MATHML HANDLERS #######################################################################
 
-# Mapping MathML tag element names (keys) to appropriate function for SymPy output (values)
-HANDLERS = {'abs': abs_handler,
-            'and': and_handler,
-            'apply': apply_handler,
-            'arccos': arccos_handler,
-            'bvar': bvar_handler,
-            'ci': ci_handler,
-            'cn': cn_handler,
-            'cos': cos_handler,
-            'diff': diff_handler,
-            'divide': divide_handler,
-            'eq': eq_handler,
-            'exp': exp_handler,
-            'floor': floor_handler,
-            'geq': geq_handler,
-            'gt': gt_handler,
-            'leq': leq_handler,
-            'ln': ln_handler,
-            'log': log_handler,
-            'lt': lt_handler,
-            'math': math_handler,
-            'minus': minus_handler,
-            'or': or_handler,
-            'otherwise': otherwise_handler,
-            'pi': pi_handler,
-            'piece': piece_handler,
-            'piecewise': piecewise_handler,
-            'plus': plus_handler,
-            'power': power_handler,
-            'rem': rem_handler,
-            'root': root_handler,
-            'tanh': tanh_handler,
-            'times': times_handler}
+# These MathML tags map directly to Sympy classes and don't require any extra handling
+SIMPLE_MATHML_TO_SYMPY_NAMES = {
+    'abs': 'Abs',
+    'and': 'And',
+    'arccos': 'acos',
+    'arccosh': 'acosh',
+    'arccot': 'acot',
+    'arccoth': 'acoth',
+    'arccsc': 'acsc',
+    'arccsch': 'acsch',
+    'arcsec': 'asec',
+    'arcsech': 'asech',
+    'arcsin': 'asin',
+    'arcsinh': 'asinh',
+    'arctan': 'atan',
+    'arctanh': 'atanh',
+    'ceiling': 'ceiling',
+    'cos': 'cos',
+    'cosh': 'cosh',
+    'cot': 'cot',
+    'coth': 'coth',
+    'csc': 'csc',
+    'csch': 'csch',
+    'eq': 'Eq',
+    'exp': 'exp',
+    'exponentiale': 'E',
+    'false': 'false',
+    'floor': 'floor',
+    'geq': 'Ge',
+    'gt': 'Gt',
+    'infinity': 'oo',
+    'leq': 'Le',
+    'ln': 'ln',
+    'lt': 'Lt',
+    'max': 'Max',
+    'min': 'Min',
+    'neq': 'Ne',
+    'not': 'Not',
+    'notanumber': 'nan',
+    'or': 'Or',
+    'pi': 'pi',
+    'plus': 'Add',
+    'rem': 'Mod',
+    'sec': 'sec',
+    'sech': 'sech',
+    'sin': 'sin',
+    'sinh': 'sinh',
+    'tan': 'tan',
+    'tanh': 'tanh',
+    'times': 'Mul',
+    'true': 'true',
+    'xor': 'Xor',
+}
+
+# MathML relation elements that are n-ary operators
+MATHML_NARY_RELATIONS = {'eq', 'leq', 'lt', 'geq', 'gt'}
+
+# Mapping MathML tag element names (keys) to appropriate handler for SymPy output (values)
+# These tags require explicit handling because they have children or context etc.
+HANDLERS = {
+    'apply': apply_handler,
+    'bvar': bvar_handler,
+    'ci': ci_handler,
+    'cn': cn_handler,
+    'degree': degree_handler,
+    'diff': diff_handler,
+    'divide': divide_handler,
+    'log': log_handler,
+    'logbase': logbase_handler,
+    'math': math_handler,
+    'minus': minus_handler,
+    'otherwise': otherwise_handler,
+    'piece': piece_handler,
+    'piecewise': piecewise_handler,
+    'power': power_handler,
+    'root': root_handler
+}
+
+# Add tags that can be handled by simple_operator_handler
+for tagName in SIMPLE_MATHML_TO_SYMPY_NAMES:
+    HANDLERS[tagName] = simple_operator_handler


Test patch:
diff --git a/tests/test_mathml2sympy.py b/tests/test_mathml2sympy.py
index 47d7b08..ded0740 100644
--- a/tests/test_mathml2sympy.py
+++ b/tests/test_mathml2sympy.py
@@ -17,7 +17,6 @@ class TestParser(object):
     def assert_equal(self, content_xml, sympy_expression):
         mathml_string = self.make_mathml(content_xml)
         transpiled_sympy = mathml2sympy.parse_string(mathml_string)
-        # print(mathml_string, "⟶", transpiled_sympy, "==", sympy_expression)
         assert transpiled_sympy == sympy_expression
 
     def test_symbol(self):
@@ -58,6 +57,10 @@ class TestParser(object):
         self.assert_equal('<apply><eq/><ci>a</ci><ci>b</ci></apply>',
                           [sympy.Eq(sympy.Symbol('a'), sympy.Symbol('b'))])
 
+    def test_neq(self):
+        self.assert_equal('<apply><neq/><ci>a</ci><ci>b</ci></apply>',
+                          [sympy.Ne(sympy.Symbol('a'), sympy.Symbol('b'))])
+
     def test_leq(self):
         self.assert_equal('<apply><leq/><ci>a</ci><ci>b</ci></apply>',
                           [sympy.Symbol('a') <= sympy.Symbol('b')])
@@ -106,6 +109,10 @@ class TestParser(object):
         self.assert_equal('<apply><root/><ci>a</ci></apply>',
                           [sympy.sqrt(sympy.Symbol('a'))])
 
+    def test_root_degree(self):
+        self.assert_equal('<apply><root/><degree><ci>n</ci></degree><ci>a</ci></apply>',
+                          [sympy.root(sympy.Symbol('a'), sympy.Symbol('n'))])
+
     def test_pi(self):
         self.assert_equal('<pi/>',
                           [sympy.pi])
@@ -118,12 +125,30 @@ class TestParser(object):
         self.assert_equal('<apply><log/><ci>x</ci></apply>',
                           [sympy.log(sympy.Symbol('x'), 10)])
 
+    def test_log_with_base(self):
+        # numeric base
+        self.assert_equal('<apply><log/><logbase><cn>3</cn></logbase><ci>x</ci></apply>',
+                          [sympy.log(sympy.Symbol('x'), sympy.Float(3.0))])
+        # symbolic base
+        self.assert_equal('<apply><log/><logbase><ci>y</ci></logbase><ci>x</ci></apply>',
+                          [sympy.log(sympy.Symbol('x'), sympy.Symbol('y'))])
+
     def test_diff(self):
         time = sympy.Symbol('time')
         V = sympy.Function('V')
         self.assert_equal('<apply><diff/><bvar><ci>time</ci></bvar><ci>V</ci></apply>',
                           [sympy.Derivative(V(time), time)])
 
+    def test_diff_with_order(self):
+        time = sympy.Symbol('time')
+        V = sympy.Function('V')
+        self.assert_equal('<apply>'
+                          '<diff/>'
+                          '<bvar><ci>time</ci><degree><cn>2</cn></degree></bvar>'
+                          '<ci>V</ci>'
+                          '</apply>',
+                          [sympy.Derivative(V(time), time, 2)])
+
     def test_piecewise(self):
         x = sympy.Symbol('x')
         self.assert_equal('<piecewise>'
@@ -132,10 +157,28 @@ class TestParser(object):
                           '</piecewise>',
                           [sympy.Piecewise((0, x < 0.0), (x, True))])
 
-    def test_multiple_equalities(self):
-        self.assert_equal('<apply><eq/><ci>x</ci><cn>1</cn></apply>'
-                          '<apply><eq/><ci>y</ci><cn>2</cn></apply>',
-                          [sympy.Eq(sympy.Symbol('x'), 1), sympy.Eq(sympy.Symbol('y'), 2)])
+        self.assert_equal('<piecewise>'
+                          '<piece><cn>10</cn><apply><gt/><ci>x</ci><cn>0</cn></apply></piece>'
+                          '<piece><cn>20</cn><apply><gt/><ci>x</ci><cn>1</cn></apply></piece>'
+                          '<piece><cn>30</cn><apply><gt/><ci>x</ci><cn>2</cn></apply></piece>'
+                          '<otherwise><cn>0</cn></otherwise>'
+                          '</piecewise>',
+                          [sympy.Piecewise((10, x > 0), (20, x > 1), (30, x > 2), (0, True))])
+
+    def test_multiple_relations(self):
+        from sympy.abc import a, b, c, x, y, z
+        eq_xml = '<apply><eq/><ci>x</ci><ci>y</ci><ci>z</ci><cn>2.0</cn></apply>'
+        lt_xml = '<apply><lt/><ci>a</ci><ci>b</ci><ci>c</ci><cn>2.0</cn></apply>'
+        ge_xml = '<apply><geq/><ci>x</ci><ci>y</ci><ci>z</ci><cn>2.0</cn></apply>'
+
+        self.assert_equal(eq_xml, [sympy.And(sympy.Eq(x, y), sympy.Eq(y, z), sympy.Eq(z, 2.0))])
+        self.assert_equal(lt_xml, [sympy.And(sympy.Lt(a, b), sympy.Lt(b, c), sympy.Lt(c, 2.0))])
+        self.assert_equal(ge_xml, [sympy.And(sympy.Ge(x, y), sympy.Ge(y, z), sympy.Ge(z, 2.0))])
+        self.assert_equal('<apply><and/>%s%s</apply>' % (eq_xml, lt_xml),
+                          [sympy.And(
+                              sympy.And(sympy.Eq(x, y), sympy.Eq(y, z), sympy.Eq(z, 2.0)),
+                              sympy.And(sympy.Lt(a, b), sympy.Lt(b, c), sympy.Lt(c, 2.0))
+                          )])
 
     def test_cellml_namespace(self):
         mathml_xml = '<math xmlns="http://www.w3.org/1998/Math/MathML" ' \
@@ -154,6 +197,37 @@ class TestParser(object):
                           '></apply>',
                           [sympy.Eq(sympy.Derivative(V(t), t), -(i_Stim+i_Na+i_K+i_L) / Cm)])
 
+    def test_scientific_notation(self):
+        self.assert_equal('<cn type="e-notation">1.234<sep/>5</cn>', [sympy.Number(1.234e5)])
+
+    def test_xor(self):
+        self.assert_equal('<apply><xor/><ci>a</ci><ci>b</ci></apply>',
+                          [sympy.Xor(sympy.Symbol('a'), sympy.Symbol('b'))])
+
+    def test_not(self):
+        self.assert_equal('<apply><not/><ci>a</ci></apply>',
+                          [sympy.Not(sympy.Symbol('a'))])
+
+    def test_ceiling(self):
+        self.assert_equal('<apply><ceiling/><ci>a</ci></apply>',
+                          [sympy.ceiling(sympy.Symbol('a'))])
+
+    def test_min(self):
+        self.assert_equal('<apply><min/><ci>a</ci><ci>b</ci></apply>',
+                          [sympy.Min(sympy.Symbol('a'), sympy.Symbol('b'))])
+
+    def test_max(self):
+        self.assert_equal('<apply><max/><ci>a</ci><ci>b</ci></apply>',
+                          [sympy.Max(sympy.Symbol('a'), sympy.Symbol('b'))])
+
+    def test_trig(self):
+        self.assert_equal('<apply><cos/><ci>x</ci></apply>',
+                          [sympy.cos(sympy.Symbol('x'))])
+        self.assert_equal('<apply><sin/><ci>x</ci></apply>',
+                          [sympy.sin(sympy.Symbol('x'))])
+        self.assert_equal('<apply><arctanh/><ci>x</ci></apply>',
+                          [sympy.atanh(sympy.Symbol('x'))])
+
     def test_noble_1962(self):
         cellml_path = os.path.join(os.path.dirname(__file__), "noble_model_1962.cellml")