From 2b9235110b83211792dfbd8f77d94d9c27faecc4 Mon Sep 17 00:00:00 2001 From: Robert Vickerstaff Date: Sat, 29 Jul 2023 11:59:38 +0000 Subject: [PATCH] added simple sbml test with example xml showing desired output we should be aiming to produce --- examples/sbml/example_sbml_minimal.xml | 12 + examples/sbml/sbml32spec.py | 379 ++++++++++++++++++++++++ examples/sbml/sbml_validators.py | 88 ++++++ examples/sbml/test_minimal_example.json | 18 ++ examples/sbml/test_sbml3.py | 36 +++ 5 files changed, 533 insertions(+) create mode 100644 examples/sbml/example_sbml_minimal.xml create mode 100644 examples/sbml/sbml32spec.py create mode 100644 examples/sbml/sbml_validators.py create mode 100644 examples/sbml/test_minimal_example.json create mode 100755 examples/sbml/test_sbml3.py diff --git a/examples/sbml/example_sbml_minimal.xml b/examples/sbml/example_sbml_minimal.xml new file mode 100644 index 00000000..fe9b1524 --- /dev/null +++ b/examples/sbml/example_sbml_minimal.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/examples/sbml/sbml32spec.py b/examples/sbml/sbml32spec.py new file mode 100644 index 00000000..400cff3f --- /dev/null +++ b/examples/sbml/sbml32spec.py @@ -0,0 +1,379 @@ +#!/usr/bin/env python3 + +''' +initial attempt at creating an SBML API using modelspec +https://github.com/combine-org/compbiolibs/issues/28 + +based on sbml.level-3.version-2.core.release-2.pdf +''' + +import modelspec +from modelspec import field, instance_of, optional +from modelspec.base_types import Base +from typing import List + +from sbml_validators import * + +@modelspec.define +class Notes(Base): + ''' + XHTML field of SBase + + Args: + xmlns: str fixed "http://www.w3.org/1999/xhtml" + content: str valid XHTML + ''' + xmlns: str = field(default="http://www.w3.org/1999/xhtml",validator=[instance_of(str),xmlns_notes]) + content: str = field(default=None,validator=optional([instance_of(str),valid_xhtml])) + +@modelspec.define +class Math(Base): + ''' + Subset of MathML 2.0 used to define all formulae in SBML + ''' + xmlns: str = field(default="http://www.w3.org/1998/Math/MathML",validator=[instance_of(str),xmlns_math]) + content: str = field(default=None,validator=optional([instance_of(str),valid_mathml])) + +@modelspec.define +class SBase(Base): + """ + Abstract base class for all SBML objects + + Args: + sid: SId optional + name: string optional + metaid: XML ID optional + sboTerm: SBOTerm optional + + notes: XHTML 1.0 optional + annotation: XML content optional + """ + + sid: str = field(default=None,validator=optional([instance_of(str),valid_sid])) + name: str = field(default=None,validator=optional(instance_of(str))) + metaid: str = field(default=None,validator=optional([instance_of(str),valid_xml_id])) + sboTerm: str = field(default=None,validator=optional([instance_of(str),valid_sbo])) + + notes: Notes = field(default=None,validator=optional(instance_of(Notes))) + annotation: str = field(default=None,validator=optional([instance_of(str),valid_xml_content])) + +@modelspec.define +class Trigger(SBase): + initialValue: bool = field(default=None,validator=instance_of(bool)) + persistent: bool = field(default=None,validator=instance_of(bool)) + math: str = field(default=None,validator=optional(instance_of(str))) + +@modelspec.define +class Priority(SBase): + math: str = field(default=None,validator=optional(instance_of(str))) + +@modelspec.define +class Delay(SBase): + math: str = field(default=None,validator=optional(instance_of(str))) + +@modelspec.define +class EventAssignment(SBase): + ''' + Args: + variable: SIdRef + ''' + math: str = field(default=None,validator=optional(instance_of(str))) + variable: str = field(default=None,validator=optional(instance_of(str))) + +@modelspec.define +class Event(SBase): + useValuesFromTriggerTime: bool = field(default=None,validator=instance_of(bool)) + trigger: Trigger = field(default=None, validator=optional(instance_of(Trigger))) + priority: Priority = field(default=None, validator=optional(instance_of(Priority))) + delay: Delay = field(default=None, validator=optional(instance_of(Delay))) + listOfEventAssignments: List[EventAssignment] = field(factory=list) + +@modelspec.define +class SimpleSpeciesReference(SBase): + """ + Base class used by SpeciesReference and ModifierSpeciesReference + + Args: + species: SIdRef + """ + + species: str = field(default=None,validator=instance_of(str)) + +@modelspec.define +class ModifierSpeciesReference(SimpleSpeciesReference): + '' + +@modelspec.define +class SpeciesReference(SimpleSpeciesReference): + """ + Args: + stoichiometry: double optional + constant: boolean + """ + + stoichiometry: float = field(default=None,validator=optional(instance_of(float))) + constant: bool = field(default=None,validator=instance_of(bool)) + +@modelspec.define +class LocalParameter(SBase): + """ + Args: + units: UnitSIdRef optional + """ + + value: float = field(default=None,validator=optional(instance_of(float))) + units: str = field(default=None,validator=optional(instance_of(str))) + +@modelspec.define +class KineticLaw(SBase): + """ + """ + + math: str = field(default=None,validator=optional(instance_of(str))) + + listOfLocalParameters: List[LocalParameter] = field(factory=list) + +@modelspec.define +class Reaction(SBase): + """ + A model reaction + + Args: + reversible: boolean + compartment: SIdRef optional + """ + + reversible: bool = field(default=None,validator=instance_of(bool)) + compartment: str = field(default=None,validator=optional(instance_of(str))) + + listOfReactants: List[SpeciesReference] = field(factory=list) + listOfProducts: List[SpeciesReference] = field(factory=list) + listOfModifiers: List[ModifierSpeciesReference] = field(factory=list) + + kineticLaw: KineticLaw = field(default=None, validator=optional(instance_of(KineticLaw))) + +@modelspec.define +class Constraint(SBase): + """ + A model constraint + + Args: + math: MathML optional + message: XHTML 1.0 optional + """ + + math: str = field(default=None,validator=optional(instance_of(str))) + message: str = field(default=None,validator=optional(instance_of(str))) + +@modelspec.define +class Rule(SBase): + """ + A rule, either algebraic, assignment or rate + + Args: + math: MathML optional + """ + + math: str = field(default=None,validator=optional(instance_of(str))) + +@modelspec.define +class AlgebraicRule(Rule): + """ + An algebraic rule + """ + +@modelspec.define +class AssignmentRule(Rule): + """ + An assignment rule + + Args: + variable: SIdRef required + """ + + variable: str = field(default=None,validator=instance_of(str)) + +@modelspec.define +class RateRule(Rule): + """ + A rate rule + + Args: + variable: SIdRef required + """ + + variable: str = field(default=None,validator=instance_of(str)) + +@modelspec.define +class InitialAssignment(SBase): + """ + An initial assignment + + Args: + symbol: SIdRef required + math: MathML optional + """ + + symbol: str = field(default=None,validator=instance_of(str)) + math: str = field(default=None,validator=optional(instance_of(str))) + +@modelspec.define +class Parameter(SBase): + """ + A parameter + + Args: + value: double optional + units: UnitSIdRef optional + constant: boolean + """ + + constant: bool = field(default=None,validator=instance_of(bool)) + + value: float = field(default=None,validator=optional(instance_of(float))) + units: str = field(default=None,validator=optional(instance_of(str))) + +@modelspec.define +class Species(SBase): + """ + A species: entities of the same kind participating in reactions within a specific compartment + + Args: + compartment: SIdRef + initialAmount: double optional + initialConcentration: double optional + substanceUnits: UnitSIdRef optional + hasOnlySubstanceUnits: boolean + boundaryCondition: boolean + constant: boolean + conversionFactor: SIdRef optional + """ + + compartment: str = field(default=None,validator=instance_of(str)) + hasOnlySubstanceUnits: bool = field(default=None,validator=instance_of(bool)) + boundaryCondition: bool = field(default=None,validator=instance_of(bool)) + constant: bool = field(default=None,validator=instance_of(bool)) + + initialAmount: float = field(default=None, validator=optional(instance_of(float))) + initialConcentration: float = field(default=None, validator=optional(instance_of(float))) + substanceUnits: str = field(default=None, validator=optional(instance_of(str))) + conversionFactor: str = field(default=None, validator=optional(instance_of(str))) + +@modelspec.define +class Compartment(SBase): + """ + A compartment + + Args: + spatialDimensions: eg 3 for three dimensional space etc + size: initial size of compartment + units: units being used to define the compartment's size + constant: whether size is fixed + """ + + constant: bool = field(default=None,validator=instance_of(bool)) + + spatialDimensions: float = field(default=None,validator=optional(instance_of(float))) + size: float = field(default=None,validator=optional(instance_of(float))) + units: str = field(default=None,validator=optional(instance_of(str))) + +@modelspec.define +class Unit(SBase): + """ + A unit used to compose a unit definition. + unit = (multiplier x 10^scale x kind)^exponent + + Args: + kind: base unit (base or derived SI units only, see Table 2 of the SBML spec) + exponent: double + scale: integer + multiplier: double + """ + + kind: str = field(default=None,validator=[instance_of(str),valid_kind]) + exponent: str = field(default=1.0, validator=instance_of(float)) + scale: str = field(default=0, validator=instance_of(int)) + multiplier: str = field(default=1.0, validator=instance_of(float)) + +@modelspec.define +class UnitDefinition(SBase): + """ + A unit definition + + Args: + sid: UnitSid required (overrides SBase sid) + listOfUnits: List of units used to compose the definition + """ + + sid: str = field(default=None,validator=[instance_of(str),valid_unitsid]) + listOfUnits: List[Unit] = field(factory=list) + +@modelspec.define +class FunctionDefinition(SBase): + """ + A function definition using MathML + + Args: + sid: SId required + + math: MathML function definition optional + """ + + sid: str = field(default=None,validator=[instance_of(str),valid_sid]) + + math: Math = field(default=None, validator=optional(instance_of(Math))) + +@modelspec.define +class Model(SBase): + """ + The model + + Args: + substanceUnits: UnitSIdRef optional + timeUnits: UnitSIdRef optional + volumeUnits: UnitSIdRef optional + areaUnits: UnitSIdRef optional + lengthUnits: UnitSIdRef optional + extentUnits: UnitSIdRef optional + conversionFactor: SIdRef optional + """ + + substanceUnits: str = field(default=None, validator=optional([instance_of(str),valid_unitsid])) + timeUnits: str = field(default=None, validator=optional([instance_of(str),valid_unitsid])) + volumeUnits: str = field(default=None, validator=optional([instance_of(str),valid_unitsid])) + areaUnits: str = field(default=None, validator=optional([instance_of(str),valid_unitsid])) + lengthUnits: str = field(default=None, validator=optional([instance_of(str),valid_unitsid])) + extentUnits: str = field(default=None, validator=optional([instance_of(str),valid_unitsid])) + conversionFactor: str = field(default=None, validator=optional([instance_of(str),valid_unitsid])) + + listOfFunctionDefinitions: List[FunctionDefinition] = field(factory=list) + listOfUnitDefinitions: List[UnitDefinition] = field(factory=list) + listOfCompartments: List[Compartment] = field(factory=list) + listOfSpecies: List[Species] = field(factory=list) + listOfParameters: List[Parameter] = field(factory=list) + listOfInitialAssignments: List[InitialAssignment] = field(factory=list) + listOfRules: List[Rule] = field(factory=list) + listOfConstraints: List[Constraint] = field(factory=list) + listOfReactions: List[Reaction] = field(factory=list) + listOfEvents: List[Event] = field(factory=list) + +@modelspec.define +class SBML(SBase): + """ + The top-level SBML container implementing SBML 3.2. + See sbml.level-3.version-2.core.release-2.pdf section 4. + http://www.sbml.org/sbml/level3/version2/core + + Args: + xmlns: string, fixed to "http://www.sbml.org/sbml/level3/version2/core" + level: SBML level, fixed to 3 + version: SBML version, fixed to 2 + + model: Optional model + """ + + xmlns: str = field(default="http://www.sbml.org/sbml/level3/version2/core",validator=[instance_of(str),xmlns_sbml]) + level: str = field(default="3",validator=[instance_of(str),fixed_level]) + version: str = field(default="2",validator=[instance_of(str),fixed_version]) + + model: Model = field(default=None, validator=optional(instance_of(Model))) diff --git a/examples/sbml/sbml_validators.py b/examples/sbml/sbml_validators.py new file mode 100644 index 00000000..008c49c3 --- /dev/null +++ b/examples/sbml/sbml_validators.py @@ -0,0 +1,88 @@ +''' +functions used to validate the user assigned values of items +these functions will generally be called by passing to the validator option of attrs.field +''' + +import re +from lxml import etree +from io import StringIO + +#sbml.level-3.version-2.core.release-2.pdf Table 2 +sbml_si_units=\ +''' +ampere coulomb gray joule litre mole radian steradian weber avogadro dimensionless henry katal lumen newton +second tesla becquerel farad hertz kelvin lux ohm siemens volt candela gram item kilogram metre pascal sievert watt +'''.split() + +def valid_kind(instance, attribute, value): + if not value in sbml_si_units: + raise ValueError(f"kind {value} must be one of the standard SI units: {sbml_si_units}") + +def valid_mathml(instance, attribute, value): + 'http://www.w3.org/1998/Math/MathML' + +def xmlns_sbml(instance, attribute, value): + if value != "http://www.sbml.org/sbml/level3/version2/core": + raise ValueError("xmlns must be 'http://www.sbml.org/sbml/level3/version2/core'") + +def xmlns_notes(instance, attribute, value): + if value != "http://www.w3.org/1999/xhtml": + raise ValueError("xmlns must be 'http://www.w3.org/1999/xhtml'") + +def xmlns_math(instance, attribute, value): + if value != "http://www.w3.org/1998/Math/MathML": + raise ValueError("xmlns must be 'http://www.w3.org/1998/Math/MathML'") + +def fixed_level(instance, attribute, value): + if value != "3": + raise ValueError("this implementation only supports level 3") + +def fixed_version(instance, attribute, value): + if value != "2": + raise ValueError("this implementation only supports level 2") + +def valid_sid(instance, attribute, value): + if not re.fullmatch('[_A-Za-z][_A-Za-z0-9]*',value): + raise ValueError("an SId must match the regular expression: [_A-Za-z][_A-Za-z0-9]*") + +def valid_unitsid(instance, attribute, value): + 'same as sid except has a separate namespace' + if not re.fullmatch('[_A-Za-z][_A-Za-z0-9]*',value): + raise ValueError("a UnitSId must match the regular expression: [_A-Za-z][_A-Za-z0-9]*") + +def valid_unitsid(instance, attribute, value): + if not re.fullmatch('[_A-Za-z][_A-Za-z0-9]*',value): + raise ValueError("a UnitSId must match the regular expression: [_A-Za-z][_A-Za-z0-9]*") + +def valid_sbo(instance, attribute, value): + if not re.fullmatch('SBO:[0-9]{7}',value): + raise ValueError("an SBOTerm must match the regular expression: SBO:[0-9]{7}") + +def valid_xml_id(instance,attribute,value): + 'a valid XML 1.0 ID' + #not implemented yet: CombiningChar , Extender + #NameChar ::= letter | digit | '.' | '-' | '_' | ':' | CombiningChar | Extender + #ID ::= ( letter | '_' | ':' ) NameChar* + + if not re.fullmatch('[A-Za-z_:][A-Za-z0-9._:-]*',value): + raise ValueError("an SBOTerm must match the regular expression: SBO:[0-9]{7}") + +def valid_xhtml(instance,attribute,value): + 'use etree to validate XHTML, throw exception on error' + etree.parse(StringIO(value), etree.HTMLParser(recover=False)) + +def valid_xml_content(instance,attribute,value): + 'stub' + + if not re.search('<.*>',value): + raise ValueError(f"{value} doesn't look like XML (this validator is only a stub)") + +def valid_mathml(instance,attribute,value): + ''' + stubvalidator for MathML + note: see pdf section 4.3.2 for special rules for FunctionDefinition MathML + versus all other MathML uses in SBML + ''' + + if not re.search('',value): + raise ValueError(f"{value} doesn't look like MathML (this validator is only a stub)") diff --git a/examples/sbml/test_minimal_example.json b/examples/sbml/test_minimal_example.json new file mode 100644 index 00000000..e8419a1f --- /dev/null +++ b/examples/sbml/test_minimal_example.json @@ -0,0 +1,18 @@ +{ + "model": { + "substanceUnits": "mole", + "timeUnits": "second", + "extentUnits": "mole", + "listOfUnitDefinitions": [ + { + "sid": "per_second", + "listOfUnits": [ + { + "kind": "second", + "exponent": -1.0 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/examples/sbml/test_sbml3.py b/examples/sbml/test_sbml3.py new file mode 100755 index 00000000..d7c59338 --- /dev/null +++ b/examples/sbml/test_sbml3.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 + +''' +derived from https://github.com/combine-org/draft-modelspec-sbml-api/blob/main/test_sbml32.py +a minimalish sbml to xml example +''' + +import json +import yaml +import os + +from sbml32spec import * + +def test_example_sbml_minimal(): + 'aiming to match the xml file example_sbml_minimal.xml' + + path = "test_minimal_example" + + sbml_doc = SBML() + #open(f"{path}.docs.json", "w").write(json.dumps(sbml_doc.generate_documentation(format="dict"), indent=4)) + + model = Model(substanceUnits="mole",timeUnits="second",extentUnits="mole") + sbml_doc.model = model + + unit_def = UnitDefinition(sid="per_second") + model.listOfUnitDefinitions.append(unit_def) + + unit = Unit(kind="second",exponent=-1.0) + unit_def.listOfUnits.append(unit) + + sbml_doc.to_json_file(f"{path}.json") + sbml_doc.to_xml_file(f"{path}.xml") + + +if __name__ == "__main__": + test_example_sbml_minimal()