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
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)
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'])
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)
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)
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)
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
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)
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))
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)
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)
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
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)
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)
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)
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)
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)
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
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