"""Convenience functions for libsbml core"""
import warnings
from numbers import Number
import sympy as sp
from sympy.printing.mathml import MathMLContentPrinter
from . import _DEFAULT_SBML_LEVEL, _DEFAULT_SBML_VERSION
from .csymbol import CSymbol
from .species_symbol import SpeciesSymbol
__all__ = ["SBMLMathMLPrinter"]
[docs]
class SBMLMathMLPrinter(MathMLContentPrinter):
"""MathML code printer.
This printer converts SymPy expressions to MathML code that can be used in
SBML models.
Note:
* assumes all constants are dimensionless
"""
def __init__(
self,
*args,
literals_dimensionless=True,
sbml_level: int = _DEFAULT_SBML_LEVEL,
sbml_version: int = _DEFAULT_SBML_VERSION,
**kwargs,
):
"""Construct.
:param literals_dimensionless:
Assume numeric literals are dimensionless.
"""
super().__init__(*args, **kwargs)
if sbml_level < 3:
warnings.warn(
"SBML level < 3 has not been thoroughly tested.",
UserWarning,
stacklevel=2,
)
self.mathml_ns = 'xmlns="http://www.w3.org/1998/Math/MathML"'
self.sbml_ns = f'xmlns:sbml="http://www.sbml.org/sbml/level{sbml_level}/version{sbml_version}/core"'
# note: so far, there is no L3V2-multi
self.multi_ns = f'xmlns:multi="http://www.sbml.org/sbml/level{sbml_level}/version1/multi/version1"'
self.literals_dimensionless = literals_dimensionless
def _print_Symbol(self, sym):
"""
Print Symbol (<ci>)
Skip MathML presentation layer, which is not part of the SBML MathML
subset.
"""
ci = self.dom.createElement(self.mathml_tag(sym))
ci.appendChild(self.dom.createTextNode(sym.name))
return ci
def _print_SpeciesSymbol(self, sym: SpeciesSymbol):
ci = self._print_Symbol(sym)
if sym.representation_type:
ci.setAttribute(
"multi:representationType", sym.representation_type
)
if sym.species_reference:
ci.setAttribute("multi:speciesReference", sym.species_reference)
return ci
[docs]
def doprint(self, expr, with_prolog=True, with_math=True) -> str:
"""Convert SymPy expression to MathML string.
:param expr: The SymPy expression to be converted.
:param with_prolog: Whether to include the XML prolog.
:param with_math: Whether to include the <math> tags.
>>> SBMLMathMLPrinter().doprint(sp.sympify("3 * a"))
'<?xml version="1.0" encoding="UTF-8"?>\\n<math xmlns="http://www.w3.org/1998/Math/MathML" xmlns:sbml="http://www.sbml.org/sbml/level3/version2/core">\\n<apply><times/><cn type="integer" sbml:units="dimensionless">3</cn><ci>a</ci></apply></math>'
>>> SBMLMathMLPrinter().doprint(sp.sympify("cbrt(3)"), with_math=False, with_prolog=False)
'<apply><root/><degree><cn>3</cn></degree><cn type="integer" sbml:units="dimensionless">3</cn></apply>'
"""
if isinstance(expr, float):
expr = sp.Float(expr)
try:
mathml = super().doprint(expr)
except Exception as e:
raise ValueError(f"MathML printing failed for {expr}") from e
if not with_math:
return mathml
prolog = (
'<?xml version="1.0" encoding="UTF-8"?>\n' if with_prolog else ""
)
sbml_ns = f" {self.sbml_ns}" if " sbml:" in mathml else ""
multi_ns = f" {self.multi_ns}" if " multi:" in mathml else ""
return (
f"{prolog}<math {self.mathml_ns}{sbml_ns}{multi_ns}>\n"
f"{mathml}</math>"
)
def _print_Number(self, e):
# only try printing as int if it fits int32
if isinstance(e, int) and _is_sbml_compatible_int(e):
res = self._print_int(e)
else:
res = super()._print_Number(e)
if self.literals_dimensionless:
res.setAttribute("sbml:units", "dimensionless")
return res
def _print_Rational(self, e):
if e.q == 1:
# don't divide by one
return self._print_int(e.p)
if not (_is_sbml_compatible_int(e.p) and _is_sbml_compatible_int(e.q)):
# avoid int32 under/overflow in libsbml and print as float
return self._print_Number(e.evalf())
res = self.dom.createElement("cn")
res.setAttribute("type", "rational")
res.appendChild(self.dom.createTextNode(f"{e.p} <sep/> {e.q}"))
if self.literals_dimensionless:
res.setAttribute("sbml:units", "dimensionless")
return res
def _print_int(self, e):
if not _is_sbml_compatible_int(e):
# avoid int32 under/overflow in libsbml and print as float
return self._print_Number(e)
res = super()._print_int(e)
res.setAttribute("type", "integer")
if self.literals_dimensionless:
res.setAttribute("sbml:units", "dimensionless")
return res
def _print_Float(self, e):
res = super()._print_Float(e)
if self.literals_dimensionless:
res.setAttribute("sbml:units", "dimensionless")
return res
def _print_One(self, e):
res = self._print_int(e)
if self.literals_dimensionless:
res.setAttribute("sbml:units", "dimensionless")
return res
def _print_Quantity(self, e):
res = self._print(e.m)
if isinstance(e.m, Number):
# TODO this unit may not exist and might have to be created
# TODO if Quantities are used in sympy expressions, they might
# units might get shuffled around and end up <apply>. In this case
# we skip settings units for now.
res.setAttribute("sbml:units", str(e.u))
return res
def _print_CSymbol(self, e: CSymbol):
dom_element = self.dom.createElement("csymbol")
dom_element.appendChild(self.dom.createTextNode(str(e)))
if e.definition_url:
dom_element.setAttribute("definitionURL", e.definition_url)
if e.encoding:
dom_element.setAttribute("encoding", e.encoding)
return dom_element
def _print_Function(self, e):
if hasattr(e, "definition_url"):
return self._print_CFunction(e)
return super()._print_Function(e)
def _print_CFunction(self, e):
dom_element = self.dom.createElement("apply")
csymbol = self.dom.createElement("csymbol")
csymbol.appendChild(self.dom.createTextNode(e.name))
if e.definition_url:
csymbol.setAttribute("definitionURL", e.definition_url)
if e.encoding:
csymbol.setAttribute("encoding", e.encoding)
dom_element.appendChild(csymbol)
for arg in e.args:
dom_element.appendChild(self._print(arg))
return dom_element
def _is_sbml_compatible_int(value: int) -> bool:
"""Check if integer is compatible with SBML (fits into signed int32)."""
return 2**31 > value >= -(2**31)