Example #1
0
    def _eat(self, element, iterator, nargs=1):
        """
        Takes ``nargs`` elements from the ``iterator``, which should be
        pointing at the first element after ``element``.

        Will complain if there are fewer or more than ``nargs`` elements
        available from the iterator.

        ``element``
            The element just before the iterator (used in error messages).
        ``iterator``
            An iterator pointing at the first element after ``element``.
        ``nargs``
            The required number of arguments that ``iterator`` should still
            contain.

        """
        # Get all operands
        ops = [self._parse_atomic(x) for x in iterator]

        # Check number of operands
        if len(ops) != nargs:
            raise MathMLError(
                'Expecting ' + str(nargs) + ' operand(s), got ' +
                str(len(ops)) + ' for ' + split(element.tag)[1] + '.', element)

        return ops
Example #2
0
    def _check_allowed_content(self,
                               element,
                               children,
                               attributes,
                               name=None,
                               math=False):
        """
        Scans ``element`` and raises an exception if any unlisted CellML
        children are found, if any unlisted null-namespace attributes are
        found, or if the element contains text.

        With ``math=False`` (default), the method also checks that no MathML
        elements are present. With ``math=True`` only a MathML ``<math>``
        element is allowed.
        """
        # Check for text inside this tag
        # For mixed-context xml this checks only up until the first child tag.
        if element.text is not None:
            if element.text.strip():
                raise CellMLParsingError(
                    'Text found in ' + self._tag(element, name) + '.', element)

        # Check child elements
        allowed = set([self._join(x) for x in children])
        if math:
            allowed.add(self._join('math', cellml.NS_MATHML))

        # Check if only contains allowed child elements
        for child in element:
            # Check for trailing text
            if child.tail is not None and child.tail.strip():
                raise CellMLParsingError(
                    'Text found in ' + self._tag(element, name) + ', after ' +
                    self._tag(child) + ' element.', child)

            # Check if allowed
            if str(child.tag) in allowed:
                continue

            raise CellMLParsingError(
                'Unexpected content type in ' + self._tag(element, name) +
                ', found element of type ' + self._tag(child) + '.', child)

        # Check attributes
        allowed = set(attributes)
        for key in element.attrib:
            # id is always allowed
            if key == 'id':
                continue

            # Must be in allowed list
            if key not in allowed:
                # Split off namespace, and add back in again (to get nicer
                # formatting).
                ns, at = split(key)
                key = self._item(ns, at)
                raise CellMLParsingError(
                    'Unexpected attribute ' + key + ' found in ' +
                    self._tag(element, name) + '.', element)
Example #3
0
    def _parse_group_relationship_ref(self, element, relationships):
        """
        Parses a relationship_ref ``element`` and adds it to the set of
        ``relationships``.

        Named containment relationships will be added as ``containment, name``.
        """
        # Check cmeta id
        self._check_cmeta_id(element)

        # Check relationship is specified
        ns = None
        rel = element.attrib.get('relationship', None)
        if rel is None:
            # Check for relationship attribute in other namespace
            for at in element.attrib:
                ns, at = split(at)
                if at == 'relationship':
                    rel = 'other'
                    break
            if rel is None:
                raise CellMLParsingError(
                    'Relationship_ref must define a relationship attribute'
                    ' (6.4.2.1).', element)

        # Check type (only if in null namespace)
        if ns is None and rel not in ['encapsulation', 'containment']:
            raise CellMLParsingError(
                'Unknown relationship type: "' + rel + '", expecting either'
                ' "encapsulation" or "containment" (6.4.2.2).', element)

        # Check name, if given
        name = element.attrib.get('name', None)
        if name is not None:
            if rel == 'encapsulation':
                raise CellMLParsingError(
                    'Encapsulation relationships may not define a name'
                    ' attribute (6.4.2.4).', element)

            if not myokit.formats.cellml.v1.is_valid_identifier(name):
                raise CellMLParsingError(
                    'Relationship_ref name must be a valid CellML identifier,'
                    ' but found "' + name + '" (6.4.2.3).', element)
            rel += ', ' + name

        # Check uniqueness of relationship (only in null namespace)
        if ns is None and rel in relationships:
            raise CellMLParsingError(
                'Relationship_refs in each group must have a unique pair of'
                ' (relationship, name) attributes (6.4.2.5).', element)

        # Store relationsip
        relationships.add(rel)

        # Check allowed content
        self._check_allowed_content(element, [], ['relationship', 'name'])
Example #4
0
    def _parse_log(self, element, iterator):
        """
        Parses the elements following a ``<log>`` element.

        Arguments:

        ``element``
            The ``<log>`` element.
        ``iterator``
            An iterator pointing at the first element after ``element``.

        """
        # Get next operands
        ops = [x for x in iterator]

        # Check for zero ops
        if len(ops) == 0:
            raise MathMLError('Expecting operand after <log> element.',
                              element)

        # Check if first op is logbase
        if split(ops[0].tag)[1] == 'logbase':

            # Get logbase
            base = ops[0]
            if len(base) != 1:
                raise MathMLError(
                    'Expecting a single operand inside <logbase> element.',
                    base)
            base = self._parse_atomic(base[0])

            # Get main operand
            if len(ops) != 2:
                raise MathMLError(
                    'Expecting a single operand after the <logbase> element'
                    ' inside a <log>.', element)
            op = self._parse_atomic(ops[1])

        # No logbase given
        else:
            base = None

            if len(ops) != 1:
                raise MathMLError(
                    'Expecting a single operand (or a <logbase> followed by a'
                    ' single operand) inside a <log> element.', element)
            op = self._parse_atomic(ops[0])

        if base is None or base.eval() == 10:
            return myokit.Log10(op)
        else:
            return myokit.Log(op, base)
Example #5
0
    def _parse_root(self, element, iterator):
        """
        Parses the elements following a ``<root>`` element.

        Arguments:

        ``element``
            The ``<root>`` element.
        ``iterator``
            An iterator pointing at the first element after ``element``.

        """
        # Get next operands
        ops = [x for x in iterator]

        # Check for zero ops
        if len(ops) == 0:
            raise MathMLError('Expecting operand after <root> element.',
                              element)

        # Check if first op is degree
        if split(ops[0].tag)[1] == 'degree':

            # Get degree
            degree = ops[0]
            if len(degree) != 1:
                raise MathMLError(
                    'Expecting a single operand inside <degree> element.',
                    degree)
            degree = self._parse_atomic(degree[0])

            # Get main operand
            if len(ops) != 2:
                raise MathMLError(
                    'Expecting a single operand after the <degree> element'
                    ' inside a <root>.', element)
            op = self._parse_atomic(ops[1])

            # Return expression
            if degree.eval() == 2:
                return myokit.Sqrt(op)
            return myokit.Power(op, myokit.Divide(self._const(1), degree))

        # No degree given
        if len(ops) != 1:
            raise MathMLError(
                'Expecting a single operand (or a <degree> followed by a'
                ' single operand) inside a <root> element.', element)
        op = self._parse_atomic(ops[0])

        return myokit.Sqrt(op)
Example #6
0
    def _check_for_cellml_in_extensions(self, element):
        """
        Checks that a non-CellML element does not contain CellML elements or
        attributes.
        """
        # Check if this element contains CellML attributes
        for key in element.attrib:
            ns, at = split(key)
            if ns == self._ns:
                raise CellMLParsingError(
                    'CellML attribute ' + self._item(ns, at) + ' found'
                    ' in extension element ' + self._tag(element) +
                    ' (2.4.3).', element)

        # Check if this element has CellML children
        for child in element:
            if split(child.tag)[0] == self._ns:
                raise CellMLParsingError(
                    'CellML element ' + self._tag(child) + ' found inside'
                    ' extension element ' + self._tag(element) + ' (2.4.3).',
                    child)

            # Recurse into children
            self._check_for_cellml_in_extensions(child)
Example #7
0
    def _tag(self, element, name=None):
        """
        Returns an element's name, but changes the syntax from ``{...}tag`` to
        ``cellml:tag`` for CellML elements.

        If the element is in a CellML namespace and an optional ``name``
        attribute is given, this is added to the returned output using xpath
        syntax, e.g. ``cellml:model[@name="MyModel"]``.

        Can also be used for attributes.
        """
        ns, el = split(element.tag)
        tag = self._item(ns, el)
        if ns == self._ns and name is not None:
            tag += '[@name="' + name + '"]'
        return tag
Example #8
0
    def _parse_atomic(self, element):
        """
        Parses a bit of MathML entirely encoded in ``element``, i.e. a number,
        variable, or apply.
        """

        # Get element type, decide what to do
        _, name = split(element.tag)

        # Brackets
        if name == 'apply':
            return self._parse_apply(element)

        # Variable reference
        elif name == 'ci':
            return self._parse_name(element)

        # Number
        elif name == 'cn':
            return self._parse_number(element)

        elif name == 'csymbol':
            return self._parse_symbol(element)

        # Constants
        elif name == 'pi':
            return self._const('3.14159265358979323846')
        elif name == 'exponentiale':
            return myokit.Exp(self._const(1))
        elif name == 'true':
            # This is correct, ``True == 1`` -> False, ``True == 2`` -> False
            return self._const(1)
        elif name == 'false':
            return self._const(0)
        elif name == 'notanumber':
            return self._const(float('nan'))
        elif name == 'infinity':
            return self._const(float('inf'))

        # Piecewise statement
        elif name == 'piecewise':
            return self._parse_piecewise(element)

        # Unexpected element
        else:
            raise MathMLError('Unsupported element: ' + str(element.tag) + '.',
                              element)
Example #9
0
    def model(self, path):
        """
        Reads a CellML file and returns a:class:`myokit.Model`.
        """
        import myokit.formats.cellml as cellml
        import myokit.formats.cellml.v1 as v1
        import myokit.formats.cellml.v2 as v2
        parsers = {
            cellml.NS_CELLML_1_0: v1.CellMLParser,
            cellml.NS_CELLML_1_1: v1.CellMLParser,
            cellml.NS_CELLML_2_0: v2.CellMLParser,
        }

        # Open XML file
        try:
            parser = etree.XMLParser(remove_comments=True)
            tree = etree.parse(path, parser=parser)
        except Exception as e:
            raise CellMLImporterError('Unable to parse XML: ' + str(e))

        # Get root node
        root = tree.getroot()

        # Detect namespace, create appropriate parser
        ns, el = split(root.tag)
        try:
            parser = parsers[ns]
        except KeyError:
            raise CellMLImporterError(
                'Unknown CellML version or not a CellML document at ' +
                str(path) + '.')
        parser = parser()

        try:
            # Parse and validate CellML model
            cellml_model = parser.parse(root)
            cellml_model.validate()

            # Create and return Myokit model
            return cellml_model.myokit_model()

        except v1.CellMLParsingError as e:
            raise CellMLImporterError(str(e))
        except v2.CellMLParsingError as e:
            raise CellMLImporterError(str(e))
Example #10
0
    def parse(self, element):
        """
        Parses a MathML expression, rooted in the given ``<apply>`` element,
        and returns a :class:`myokit.Expression`.

        Arguments:

        ``element``
            An ``xml.etree.ElementTree.Element`` (or similar) to start parsing
            from. Must be an ``<apply>`` element.

        """
        # Remove <math> element, if found
        ns, el = split(element.tag)
        if el == 'math':
            element = element[0]

        return self._parse_atomic(element)
Example #11
0
    def _parse_piecewise(self, element):
        """
        Parses a ``<piecewise>`` element.
        """

        # Piecewise contains at least one piece, optionally contains an
        # "otherwise". Syntax doesn't ensure this statement makes sense.
        ops = []
        other = None

        # Scan pieces
        for child in element:
            _, el = split(child.tag)

            if el == 'piece':
                if len(child) != 2:
                    raise MathMLError(
                        '<piece> element must have exactly 2 children.', child)
                ops.append(self._parse_atomic(child[1]))  # Condition
                ops.append(self._parse_atomic(child[0]))  # Value

            elif el == 'otherwise':
                if other is not None:
                    raise MathMLError(
                        'Found more than one <otherwise> inside a <piecewise>'
                        ' element.', child)
                if len(child) != 1:
                    raise MathMLError(
                        '<otherwise> element must have exactly 1 child.',
                        child)
                other = self._parse_atomic(child[0])

            else:
                raise MathMLError(
                    'Unexpected content in <piecewise>. Expecting <piece> or'
                    ' <otherwise>, found <' + el + '>.', child)

        # Add otherwise
        if other is None:
            ops.append(self._const(0))
        else:
            ops.append(other)

        return myokit.Piecewise(*ops)
Example #12
0
    def _next(self, iterator, tag=None):
        """
        Returns the next element from an ``iterator``.

        If ``tag`` is given, elements will be drawn from the iterator until an
        element with the given tag is found.
        """
        try:
            # Return next element
            if tag is None:
                return next(iterator)

            # Find a specific element
            el = next(iterator)
            while split(el.tag)[1] != tag:
                el = next(iterator)
            return el

        # Ran out of elements
        except StopIteration:
            return None
Example #13
0
    def _parse_math(self, element, component):
        """
        Parses a mathml:math ``element``, adding equations to the variables of
        the given ``component``.
        """
        model = component.model()

        # Get variables from component
        def variable_factory(name, element):
            try:
                var = component.variable(name)
            except KeyError:
                raise CellMLParsingError(
                    'Variable references in equations must name a variable'
                    ' from the local component.', element)

            return myokit.Name(var)

        # Create numbers with units
        attr = self._join('units')

        def number_factory(value, element):
            # Numbers not connected to a cn
            if element is None:
                return myokit.Number(value, myokit.units.dimensionless)

            # Get units attribute
            try:
                units = element.attrib[attr]
            except KeyError:
                raise CellMLParsingError(
                    'Numbers inside MathML must define a cellml:units'
                    ' attribute.', element)

            # Find units in component
            try:
                units = model.find_units(units)
            except myokit.formats.cellml.v2.CellMLError:
                raise CellMLParsingError(
                    'Unknown unit "' + str(units) + '" referenced inside a'
                    ' MathML equation.', element)

            # Create and return
            return myokit.Number(value, units.myokit_unit())

        # Create parser
        p = myokit.formats.mathml.MathMLParser(variable_factory,
                                               number_factory, self._vois)

        # Iterate over applies.
        for child in element:

            # Check each child is in MathML namespace
            ns, el = split(child.tag)
            if ns != cellml.NS_MATHML:
                raise CellMLParsingError(
                    'The contents of a mathml:math element must be in the'
                    ' mathml namespace, found "' + str(child.tag) +
                    '" inside ' + str(component) + '.', child)

            # If it isn't these it must be an apply
            if el != 'apply':
                raise CellMLParsingError(
                    'Unexpected contents in mathml:math. Expecting'
                    ' mathml:apply but found mathml:' + el + ' inside maths'
                    ' for ' + str(component) + '.', child)

            # Parse
            eq = p.parse(child)
            if not isinstance(eq, myokit.Equal):
                raise CellMLParsingError(
                    'Unexpected element in MathML, expecting a list of'
                    ' equations, got ' + self._tag(child) + '.', child)
            lhs, rhs = eq

            # Check lhs
            if not isinstance(lhs, myokit.LhsExpression):
                raise CellMLParsingError(
                    'Invalid expression found on the left-hand side of an'
                    ' equation: ' + self._dae_message, child)

            # Check variable is undefined
            var = lhs.var()
            if var.has_equation():
                raise CellMLParsingError(
                    'Overdefined variable: ' + str(var) + ': Two defining'
                    ' equations.', child)

            # Set equations
            try:
                lhs.var().set_equation(myokit.Equation(lhs, rhs))
            except myokit.formats.cellml.v2.CellMLError \
                    as e:  # pragma: no cover (currently can't happen)
                raise CellMLParsingError(str(e), child)
Example #14
0
    def _parse_math(self, element, component):
        """
        Parses a mathml:math ``element``, adding equations to the variables of
        the given ``component``.
        """

        # Get variables from component
        def variable_factory(name, element):
            try:
                var = component.variable(name)
            except KeyError:
                raise CellMLParsingError(
                    'Variable references in equation must name a variable from'
                    ' the local component (4.4.2.1).', element)

            return myokit.Name(var)

        # Create numbers with units
        attr = self._join('units')

        def number_factory(value, element):
            # Numbers not connected to a cn
            if element is None:
                return myokit.Number(value, myokit.units.dimensionless)

            # Get units attribute
            try:
                units = element.attrib[attr]
            except KeyError:
                raise CellMLParsingError(
                    'Numbers inside MathML must define a cellml:units'
                    ' attribute (4.4.3.1).', element)

            # Find units in component
            try:
                units = component.find_units(units)
                units = units.myokit_unit()
            except myokit.formats.cellml.v1.UnitsError as e:
                warnings.warn(
                    'The units "' + str(units) + '" (referenced inside a'
                    ' MathML equation) are not supported and have been'
                    ' replaced by `dimensionless`. (' + str(e) + ')')
                units = myokit.units.dimensionless
            except myokit.formats.cellml.v1.CellMLError:
                raise CellMLParsingError(
                    'Unknown unit "' + str(units) + '" referenced inside a'
                    ' MathML equation (4.4.3.2).', element)

            # Create and return
            return myokit.Number(value, units)

        # Create parser
        p = myokit.formats.mathml.MathMLParser(variable_factory,
                                               number_factory, self._free_vars)

        # Iterate over applies, allowing for applies nested inside <semantics>
        # elements
        todo = collections.deque([x for x in element])
        while todo:
            child = todo.popleft()

            # Check each child is in MathML namespace
            ns, el = split(child.tag)
            if ns != cellml.NS_MATHML:
                raise CellMLParsingError(
                    'The contents of a mathml:math element must be in the'
                    ' mathml namespace, found "' + str(child.tag) +
                    '" inside ' + str(component) + '.', child)

            # Ignore annotations
            if el in ['annotation', 'annotation-xml']:
                continue

            # Look inside of semantics elements
            if el == 'semantics':
                todo.extendleft(reversed(child))
                continue

            # If it isn't these it must be an apply
            if el != 'apply':
                raise CellMLParsingError(
                    'Unexpected contents in mathml:math. Expecting'
                    ' mathml:apply but found mathml:' + el + ' inside maths'
                    ' for ' + str(component) + '.', child)

            # Parse
            eq = p.parse(child)
            if not isinstance(eq, myokit.Equal):
                raise CellMLParsingError(
                    'Unexpected element in MathML, expecting a list of'
                    ' equations, got ' + self._tag(child) + '.', child)
            lhs, rhs = eq

            # Check lhs
            if not isinstance(lhs, myokit.LhsExpression):
                raise CellMLParsingError(
                    'Invalid expression found on the left-hand side of an'
                    ' equation: ' + self._dae_message, child)

            # Promote derivative, check non-derivatives have no initial value
            var = lhs.var()
            if isinstance(lhs, myokit.Derivative):
                var.set_is_state(True)
            elif var.initial_value() is not None:
                raise CellMLParsingError(
                    'Initial value and a defining equation found for'
                    ' non-state ' + str(var) + ': ' + self._dae_message, child)

            # Check for double rhs
            if var.rhs() is not None:
                raise CellMLParsingError(
                    'Two defining equations found for ' + str(var) + ': ' +
                    self._dae_message, child)

            # Set rhs
            try:
                lhs.var().set_rhs(rhs)
            except myokit.formats.cellml.v1.CellMLError as e:
                raise CellMLParsingError(str(e), child)
Example #15
0
    def _parse_number(self, element):
        """
        Parses a ``<cn>`` element and returns a number object created by the
        number factory.
        """
        # https://www.w3.org/TR/MathML2/chapter4.html#contm.typeattrib
        kind = element.attrib.get('type', 'real')

        # Get value
        if kind == 'real':
            # Float, specified as 123.123 (no exponent!)
            # May be in a different base than 10
            base = element.attrib.get('base', '10').strip()
            try:
                base = float(base)
            except (TypeError, ValueError):
                raise MathMLError('Invalid base specified on <ci> element.',
                                  element)
            if base != 10:
                raise MathMLError(
                    'Numbers in bases other than 10 are not supported.',
                    element)

            # Get value
            # Note: We are being tolerant here and allowing e-notation (which
            # is not consistent with the spec!)
            if element.text is None:
                raise MathMLError('Empty <cn> element', element)
            try:
                value = float(element.text.strip())
            except ValueError:
                raise MathMLError(
                    'Unable to convert contents of <cn> to a real number: "' +
                    str(element.text) + '"', element)

        elif kind == 'integer':
            # Integer in any given base
            base = element.attrib.get('base', '10').strip()
            try:
                base = int(base)
            except ValueError:
                raise MathMLError(
                    'Unable to parse base of <cn> element: "' + base + '"',
                    element)

            # Get value
            if element.text is None:
                raise MathMLError('Empty <cn> element', element)
            try:
                value = int(element.text.strip(), base)
            except ValueError:
                raise MathMLError(
                    'Unable to convert contents of <cn> to an integer: "' +
                    str(element.text) + '"', element)

        elif kind == 'double':
            # Floating point (positive, negative, exponents, etc)

            if element.text is None:
                raise MathMLError('Empty <cn> element', element)
            try:
                value = float(element.text.strip())
            except ValueError:
                raise MathMLError(
                    'Unable to convert contents of <cn> to a real number: "' +
                    str(element.text) + '"', element)

        elif kind == 'e-notation':
            # 1<sep />3 = 1e3

            # Check contents
            parts = [x for x in element]
            if len(parts) != 1 or split(parts[0].tag)[1] != 'sep':
                raise MathMLError(
                    'Number in e-notation should have the format'
                    ' number<sep />number.', element)

            # Get parts of number
            sig = element.text
            exp = parts[0].tail
            if sig is None or not sig.strip():
                raise MathMLError(
                    'Unable to parse number in e-notation: missing part before'
                    ' the separator.', element)
            if exp is None or not exp.strip():
                raise MathMLError(
                    'Unable to parse number in e-notation: missing part after'
                    ' the separator.', element)

            # Get value
            try:
                value = float(sig.strip() + 'e' + exp.strip())
            except ValueError:
                raise MathMLError(
                    'Unable to parse number in e-notation "' + sig + 'e' +
                    exp + '".', element)

        elif kind == 'rational':
            # 1<sep />3 = 1 / 3
            # Check contents
            parts = [x for x in element]
            if len(parts) != 1 or split(parts[0].tag)[1] != 'sep':
                raise MathMLError(
                    'Rational number should have the format'
                    ' number<sep />number.', element)

            # Get parts of number
            numer = element.text
            denom = parts[0].tail
            if numer is None or not numer.strip():
                raise MathMLError(
                    'Unable to parse rational number: missing part before the'
                    ' separator.', element)
            if denom is None or not denom.strip():
                raise MathMLError(
                    'Unable to parse rational number: missing part after the'
                    ' separator.', element)

            # Get value
            try:
                value = float(numer.strip()) / float(denom.strip())
            except ValueError:
                raise MathMLError(
                    'Unable to parse rational number "' + numer + ' / ' +
                    denom + '".', element)

        else:
            raise MathMLError('Unsupported <cn> type: ' + kind, element)

        # Create number and return
        return self._nfac(value, element)
Example #16
0
    def _check_allowed_content(self,
                               element,
                               children,
                               attributes,
                               name=None,
                               math=False):
        """
        Scans ``element`` and raises an exception if any unlisted CellML
        children are found, if any unlisted null-namespace attributes are
        found, or if the element contains text.

        With ``math=False`` (default), the method also checks that no MathML
        elements are present. With ``math=True`` only a MathML ``<math>``
        element is allowed.
        """
        # Check for text inside this tag
        # For mixed-context xml this checks only up until the first child tag.
        if element.text is not None:
            if element.text.strip():
                raise CellMLParsingError(
                    'Text found in ' + self._tag(element, name) + '.', element)

        # Not extension namespaces
        not_ext_ns = (self._ns, cellml.NS_CMETA, cellml.NS_RDF,
                      cellml.NS_MATHML)

        # Check child elements
        allowed = set([self._join(x) for x in children])
        allowed.add(self._join('RDF', cellml.NS_RDF))
        if math:
            allowed.add(self._join('math', cellml.NS_MATHML))

        # Check if only contains allowed child elements
        for child in element:
            # Check for trailing text
            if child.tail is not None and child.tail.strip():
                raise CellMLParsingError(
                    'Text found in ' + self._tag(element, name) + ' (after ' +
                    self._tag(child) + ' element).', child)

            # Check if allowed
            if str(child.tag) in allowed:
                continue

            # Check if in an extension element
            ns = split(child.tag)[0]
            if ns in not_ext_ns:
                raise CellMLParsingError(
                    'Unexpected content type in ' + self._tag(element, name) +
                    ', found element of type ' + self._tag(child) + '.', child)
            else:
                # Check if CellML appearing in non-CellML elements
                # But leave checking inside MathML for later
                self._check_for_cellml_in_extensions(child)

        # Check attributes
        allowed = set(attributes)
        cmeta_id = self._join('id', cellml.NS_CMETA)
        for key in element.attrib:
            # Cmeta id is always allowed
            if key == cmeta_id:
                continue

            # Extension namespaces are always allowed
            ns, at = split(key)
            if ns is not None and ns not in not_ext_ns:
                continue

            # No namespace, then must be in allowed list
            if key not in allowed:
                key = self._item(ns, at)
                raise CellMLParsingError(
                    'Unexpected attribute ' + key + ' found in ' +
                    self._tag(element, name) + '.', element)
Example #17
0
    def _parse_apply(self, apply_element):
        """
        Parses an ``<apply>`` element.
        """
        # Apply must have kids
        if len(apply_element) == 0:
            raise MathMLError('Apply must contain at least one child element.',
                              apply_element)

        # Get first child
        iterator = iter(apply_element)
        element = self._next(iterator)

        # Decide what to do based on first child
        _, name = split(element.tag)

        # Handle derivative
        if name == 'diff':
            return self._parse_derivative(element, iterator)

        # Algebra (unary/binary/n-ary operators)
        elif name == 'plus':
            return self._parse_nary(element, iterator, myokit.Plus,
                                    myokit.PrefixPlus)
        elif name == 'minus':
            return self._parse_nary(element, iterator, myokit.Minus,
                                    myokit.PrefixMinus)
        elif name == 'times':
            return self._parse_nary(element, iterator, myokit.Multiply)
        elif name == 'divide':
            return self._parse_nary(element, iterator, myokit.Divide)

        # Basic functions
        elif name == 'exp':
            return myokit.Exp(*self._eat(element, iterator))
        elif name == 'ln':
            return myokit.Log(*self._eat(element, iterator))
        elif name == 'log':
            return self._parse_log(element, iterator)
        elif name == 'root':
            return self._parse_root(element, iterator)
        elif name == 'power':
            return myokit.Power(*self._eat(element, iterator, 2))
        elif name == 'floor':
            return myokit.Floor(*self._eat(element, iterator))
        elif name == 'ceiling':
            return myokit.Ceil(*self._eat(element, iterator))
        elif name == 'abs':
            return myokit.Abs(*self._eat(element, iterator))
        elif name == 'quotient':
            return myokit.Quotient(*self._eat(element, iterator, 2))
        elif name == 'rem':
            return myokit.Remainder(*self._eat(element, iterator, 2))

        # Logic
        elif name == 'and':
            return self._parse_nary(element, iterator, myokit.And)
        elif name == 'or':
            return self._parse_nary(element, iterator, myokit.Or)
        elif name == 'xor':
            # Becomes ``(x or y) and not(x and y)``
            x, y = self._eat(element, iterator, 2)
            return myokit.And(myokit.Or(x, y), myokit.Not(myokit.And(x, y)))

        elif name == 'not':
            return myokit.Not(*self._eat(element, iterator))
        elif name == 'eq' or name == 'equivalent':
            return myokit.Equal(*self._eat(element, iterator, 2))
        elif name == 'neq':
            return myokit.NotEqual(*self._eat(element, iterator, 2))
        elif name == 'gt':
            return myokit.More(*self._eat(element, iterator, 2))
        elif name == 'lt':
            return myokit.Less(*self._eat(element, iterator, 2))
        elif name == 'geq':
            return myokit.MoreEqual(*self._eat(element, iterator, 2))
        elif name == 'leq':
            return myokit.LessEqual(*self._eat(element, iterator, 2))

        # Trigonometry
        elif name == 'sin':
            return myokit.Sin(*self._eat(element, iterator))
        elif name == 'cos':
            return myokit.Cos(*self._eat(element, iterator))
        elif name == 'tan':
            return myokit.Tan(*self._eat(element, iterator))
        elif name == 'arcsin':
            return myokit.ASin(*self._eat(element, iterator))
        elif name == 'arccos':
            return myokit.ACos(*self._eat(element, iterator))
        elif name == 'arctan':
            return myokit.ATan(*self._eat(element, iterator))

        # Redundant trigonometry (CellML includes this)
        elif name == 'csc':
            # Cosecant: csc(x) = 1 / sin(x)
            return myokit.Divide(self._const(1),
                                 myokit.Sin(*self._eat(element, iterator)))
        elif name == 'sec':
            # Secant: sec(x) = 1 / cos(x)
            return myokit.Divide(self._const(1),
                                 myokit.Cos(*self._eat(element, iterator)))
        elif name == 'cot':
            # Contangent: cot(x) = 1 / tan(x)
            return myokit.Divide(self._const(1),
                                 myokit.Tan(*self._eat(element, iterator)))
        elif name == 'arccsc':
            # ArcCosecant: acsc(x) = asin(1/x)
            return myokit.ASin(
                myokit.Divide(self._const(1), *self._eat(element, iterator)))
        elif name == 'arcsec':
            # ArcSecant: asec(x) = acos(1/x)
            return myokit.ACos(
                myokit.Divide(self._const(1), *self._eat(element, iterator)))
        elif name == 'arccot':
            # ArcCotangent: acot(x) = atan(1/x)
            return myokit.ATan(
                myokit.Divide(self._const(1), *self._eat(element, iterator)))

        # Hyperbolic trig
        elif name == 'sinh':
            # Hyperbolic sine: sinh(x) = 0.5 * (e^x - e^-x)
            x = self._eat(element, iterator)[0]
            return myokit.Multiply(
                self._const(0.5),
                myokit.Minus(myokit.Exp(x), myokit.Exp(myokit.PrefixMinus(x))))
        elif name == 'cosh':
            # Hyperbolic cosine: cosh(x) = 0.5 * (e^x + e^-x)
            x = self._eat(element, iterator)[0]
            return myokit.Multiply(
                self._const(0.5),
                myokit.Plus(myokit.Exp(x), myokit.Exp(myokit.PrefixMinus(x))))
        elif name == 'tanh':
            # Hyperbolic tangent: tanh(x) = (e^2x - 1) / (e^2x + 1)
            x = self._eat(element, iterator)[0]
            e2x = myokit.Exp(myokit.Multiply(self._const(2), x))
            return myokit.Divide(myokit.Minus(e2x, self._const(1)),
                                 myokit.Plus(e2x, self._const(1)))
        elif name == 'arcsinh':
            # Inverse hyperbolic sine: asinh(x) = log(x + sqrt(x*x + 1))
            x = self._eat(element, iterator)[0]
            return myokit.Log(
                myokit.Plus(
                    x,
                    myokit.Sqrt(
                        myokit.Plus(myokit.Multiply(x, x), self._const(1)))))
        elif name == 'arccosh':
            # Inverse hyperbolic cosine:
            #   acosh(x) = log(x + sqrt(x*x - 1))
            x = self._eat(element, iterator)[0]
            return myokit.Log(
                myokit.Plus(
                    x,
                    myokit.Sqrt(
                        myokit.Minus(myokit.Multiply(x, x), self._const(1)))))
        elif name == 'arctanh':
            # Inverse hyperbolic tangent:
            #   atanh(x) = 0.5 * log((1 + x) / (1 - x))
            x = self._eat(element, iterator)[0]
            return myokit.Multiply(
                self._const(0.5),
                myokit.Log(
                    myokit.Divide(myokit.Plus(self._const(1), x),
                                  myokit.Minus(self._const(1), x))))

        # Hyperbolic redundant trig
        elif name == 'csch':
            # Hyperbolic cosecant: csch(x) = 2 / (exp(x) - exp(-x))
            x = self._eat(element, iterator)[0]
            return myokit.Divide(
                self._const(2),
                myokit.Minus(myokit.Exp(x), myokit.Exp(myokit.PrefixMinus(x))))
        elif name == 'sech':
            # Hyperbolic secant: sech(x) = 2 / (exp(x) + exp(-x))
            x = self._eat(element, iterator)[0]
            return myokit.Divide(
                self._const(2),
                myokit.Plus(myokit.Exp(x), myokit.Exp(myokit.PrefixMinus(x))))
        elif name == 'coth':
            # Hyperbolic cotangent:
            #   coth(x) = (exp(2*x) + 1) / (exp(2*x) - 1)
            x = self._eat(element, iterator)[0]
            e2x = myokit.Exp(myokit.Multiply(self._const(2), x))
            return myokit.Divide(myokit.Plus(e2x, self._const(1)),
                                 myokit.Minus(e2x, self._const(1)))
        elif name == 'arccsch':
            # Inverse hyperbolic cosecant:
            #   arccsch(x) = log(1 / x + sqrt(1 / x^2 + 1))
            x = self._eat(element, iterator)[0]
            return myokit.Log(
                myokit.Plus(
                    myokit.Divide(self._const(1), x),
                    myokit.Sqrt(
                        myokit.Plus(
                            myokit.Divide(self._const(1),
                                          myokit.Multiply(x, x)),
                            self._const(1)))))
        elif name == 'arcsech':
            # Inverse hyperbolic secant:
            #   arcsech(x) = log(1 / x + sqrt(1 / x^2 - 1))
            x = self._eat(element, iterator)[0]
            return myokit.Log(
                myokit.Plus(
                    myokit.Divide(self._const(1), x),
                    myokit.Sqrt(
                        myokit.Minus(
                            myokit.Divide(self._const(1),
                                          myokit.Multiply(x, x)),
                            self._const(1)))))
        elif name == 'arccoth':
            # Inverse hyperbolic cotangent:
            #   arccoth(x) = 0.5 * log((x + 1) / (x - 1))
            x = self._eat(element, iterator)[0]
            return myokit.Multiply(
                self._const(0.5),
                myokit.Log(
                    myokit.Divide(myokit.Plus(x, self._const(1)),
                                  myokit.Minus(x, self._const(1)))))

        # Last option: A single atomic inside an apply
        # Do this one last to stop e.g. <apply><times /></apply> returning the
        # error 'Unsupported element' (which is what parse_atomic would call).
        elif len(apply_element) == 1:
            return self._parse_atomic(element)

        # Unexpected element
        else:
            raise MathMLError(
                'Unsupported element in apply: ' + str(element.tag) + '.',
                element)
Example #18
0
    def _parse_model(self, element):
        """
        Parses a CellML model element.
        """
        # Handle document-level validation here.

        # Check namespace
        ns, el = split(element.tag)
        if ns == cellml.NS_CELLML_1_0:
            version = '1.0'
        elif ns == cellml.NS_CELLML_1_1:
            version = '1.1'
        else:
            raise CellMLParsingError(
                'Root node must be in CellML 1.0 or 1.1 namespace.', element)

        # Store namespace
        self._ns = ns

        # Check root element is a model
        if el != 'model':
            raise CellMLParsingError(
                'Root node must be a CellML model element.', element)

        # Check name is present
        try:
            name = element.attrib['name']
        except KeyError:
            raise CellMLParsingError(
                'Model element must have a name attribute (3.4.1.1).', element)

        # Create model (validates name)
        try:
            model = myokit.formats.cellml.v1.Model(name, version)

            # Check cmeta id
            cmeta_id = self._check_cmeta_id(element)
            if cmeta_id:
                model.set_cmeta_id(cmeta_id)

        except myokit.formats.cellml.v1.CellMLError as e:
            raise CellMLParsingError(str(e), element)

        # Check for imports
        im = element.find(self._join('import'))
        if im is not None:
            if version == '1.1':
                raise CellMLParsingError('Imports are not supported.', im)
            else:
                raise CellMLParsingError(
                    'Imports are not allowed in CellML 1.0.', im)

        # Check allowed content
        self._check_allowed_content(
            element,
            ['component', 'connection', 'group', 'units'],
            ['name'],
            name,
        )

        # Create model units
        for child in self._sort_units(element):
            self._parse_units(child, model)

        # Create components
        components = element.findall(self._join('component'))
        for child in components:
            self._parse_component(child, model)

        # Create encapsulation hierarchy
        for child in element.findall(self._join('group')):
            self._parse_group(child, model)

        # Add connections
        connected = set()
        for child in element.findall(self._join('connection')):
            self._parse_connection(child, model, connected)

        # Add equations
        for child in components:
            component = model.component(child.attrib['name'])
            for math in child.findall(self._join('math', cellml.NS_MATHML)):
                self._parse_math(math, component)

        # Check number of free variables
        self._free_vars = set(
            [x.var().value_source() for x in self._free_vars])
        if len(self._free_vars) > 1:
            raise CellMLParsingError(
                'Models that take derivatives with respect to more than one'
                ' variable are not supported.')

        elif len(self._free_vars) == 1:
            # Set free variable
            model.set_free_variable(self._free_vars.pop())

        # Read any rdf annotations
        rdf_tag = self._join('RDF', cellml.NS_RDF)
        for child in element.findall(rdf_tag):
            self._parse_rdf(child, model)

        # Read any documentation
        doc_tag = self._join('documentation', cellml.NS_TMP_DOC)
        for child in element.findall(doc_tag):
            self._parse_documentation(child, model)

        # Perform final validation and return
        model.validate()

        return model
Example #19
0
    def _parse_model(self, element):
        """
        Parses a CellML model element.
        """

        # Handle document-level validation here.

        # Check namespace
        ns, el = split(element.tag)
        if ns == cellml.NS_CELLML_2_0:
            version = '2.0'
        else:
            raise CellMLParsingError(
                'Root node must be in CellML 2.0 namespace.', element)

        # Store namespace
        self._ns = ns

        # Check root element is a model
        if el != 'model':
            raise CellMLParsingError(
                'Root node must be a CellML model element.', element)

        # Check name is present
        try:
            name = element.attrib['name']
        except KeyError:
            raise CellMLParsingError(
                'Model element must have a name attribute.', element)

        # Create model (validates name)
        try:
            model = myokit.formats.cellml.v2.Model(name, version)
        except myokit.formats.cellml.v2.CellMLError as e:
            raise CellMLParsingError(str(e), element)

        # Check id
        self._check_id(element, model)

        # Check for imports
        im = element.find(self._join('import'))
        if im is not None:
            raise CellMLParsingError('Imports are not supported.', im)

        # Check allowed content
        self._check_allowed_content(
            element,
            ['component', 'connection', 'encapsulation', 'units'],
            ['name'],
            name,
        )

        # Create model units
        for child in self._sort_units(element):
            self._parse_units(child, model)

        # Create components
        components = element.findall(self._join('component'))
        for child in components:
            self._parse_component(child, model)

        # Create encapsulation hierarchy
        encapsulation = element.findall(self._join('encapsulation'))
        if len(encapsulation) > 1:
            raise CellMLParsingError(
                'A model cannot contain more than one encapsulation element.',
                encapsulation[1])
        for child in encapsulation:
            self._parse_encapsulation(child, model)

        # Add connections
        connected = set()
        for child in element.findall(self._join('connection')):
            self._parse_connection(child, model, connected)

        # Add equations
        for child in components:
            component = model.component(child.attrib['name'])
            for math in child.findall(self._join('math', cellml.NS_MATHML)):
                self._parse_math(math, component)

        # Check number of variables of integration
        try:
            for var in self._vois:
                model.set_variable_of_integration(var.var())
        except myokit.formats.cellml.v2.CellMLError as e:
            raise CellMLParsingError(
                'Models that take derivatives with respect to more than one'
                ' variable are not supported (' + str(e) + ').')

        # Read any rdf annotations
        # TODO: Allow RDF annotations from external files

        # Perform final validation and return
        model.validate()

        return model