KarrLab/intro_to_wc_modeling

View on GitHub
intro_to_wc_modeling/cell_modeling/simulation/multi_algorithm/model.py

Summary

Maintainability
F
5 days
Test Coverage
A
100%
'''
Reads models specified in Excel into a Python object

@author Jonathan Karr, karr@mssm.edu
@date 3/22/2016
'''

# required libraries
from cobra import Metabolite as CobraMetabolite
from cobra import Model as CobraModel
from cobra import Reaction as CobraReaction
from intro_to_wc_modeling.cell_modeling.simulation.multi_algorithm import util
from intro_to_wc_modeling.cell_modeling.simulation.multi_algorithm.util import N_AVOGADRO
from itertools import chain
from numpy import random
from openpyxl import load_workbook
import math
import numpy as np
import re
import warnings


class Model(object):
    # Represents a model (submodels, compartments, species, reactions, parameters, references)

    submodels = []
    compartments = []
    species = []
    reactions = []
    parameters = []
    references = []

    density = None
    fractionDryWeight = None

    speciesCounts = np.zeros(0)  # rows: species, columns: compartments
    mass = None  # cell mass
    dryWeight = None  # cell dry weight
    volume = None  # cell volume
    extracellularVolume = None  # media volume
    growth = None

    def __init__(self, submodels=None, compartments=None, species=None, reactions=None, parameters=None, references=None):
        if submodels is None:
            submodels = []
        if compartments is None:
            compartments = []
        if species is None:
            species = []
        if reactions is None:
            reactions = []
        if parameters is None:
            parameters = []
        if references is None:
            references = []
        self.submodels = submodels
        self.compartments = compartments
        self.species = species
        self.reactions = reactions
        self.parameters = parameters
        self.references = references

    '''
    def __init__(self):
        self.submodels = []
        self.compartments = []
        self.species = []
        self.reactions = []
        self.parameters = []
        self.references = []
    '''

    def setupSimulation(self):
        self.fractionDryWeight = self.getComponentById('fractionDryWeight', self.parameters).value

        for subModel in self.submodels:
            subModel.setupSimulation()

        self.calcInitialConditions()

    def calcInitialConditions(self):
        cellComp = self.getComponentById('c', self.compartments)
        extrComp = self.getComponentById('e', self.compartments)

        # volume
        self.volume = cellComp.initialVolume
        self.extracellularVolume = extrComp.initialVolume

        # species counts
        self.speciesCounts = np.zeros((len(self.species), len(self.compartments)))
        for species in self.species:
            for conc in species.concentrations:
                self.speciesCounts[species.index, conc.compartment.index] = conc.value * conc.compartment.initialVolume * N_AVOGADRO

        # cell mass
        self.calcMass()

        # density
        self.density = self.mass / self.volume

        # growth
        self.growth = np.nan

        # sync submodels
        for subModel in self.submodels:
            subModel.updateLocalCellState(self)

    def calcMass(self):
        for comp in self.compartments:
            if comp.id == 'c':
                iCellComp = comp.index

        mass = 0.
        for species in self.species:
            if species.molecularWeight is not None:
                mass += self.speciesCounts[species.index, iCellComp] * species.molecularWeight
        mass /= N_AVOGADRO

        self.mass = mass
        self.dryWeight = self.fractionDryWeight * mass

    def calcVolume(self):
        self.volume = self.mass / self.density

    def setComponentIndices(self):
        for index, obj in enumerate(self.submodels):
            obj.index = index
        for index, obj in enumerate(self.compartments):
            obj.index = index
        for index, obj in enumerate(self.species):
            obj.index = index
        for index, obj in enumerate(self.reactions):
            obj.index = index
        for index, obj in enumerate(self.parameters):
            obj.index = index
        for index, obj in enumerate(self.references):
            obj.index = index

    def getSpeciesCountsDict(self):
        # get species counts as dictionary

        speciesCountsDict = {}
        for species in self.species:
            for compartment in self.compartments:
                speciesCountsDict['%s[%s]' % (species.id, compartment.id)] = self.speciesCounts[species.index, compartment.index]
        return speciesCountsDict

    def setSpeciesCountsDict(self, speciesCountsDict):
        # set species counts for dictionary

        for species in self.species:
            for compartment in self.compartments:
                self.speciesCounts[species.index, compartment.index] = speciesCountsDict['%s[%s]' % (species.id, compartment.id)]

    def getComponentById(self, id, components=None):
        if not components:
            components = chain(self.submodels, self.compartments, self.species, self.reactions, self.parameters, self.references)

        for component in components:
            if component.id == id:
                return component


class Submodel(object):
    # Represents a model (submodels, compartments, species, reactions, parameters, references)

    index = None
    id = ''
    name = ''
    algorithm = ''

    reactions = []
    species = []
    parameters = []

    speciesCounts = np.zeros(0)
    volume = np.zeros(0)
    extracellularVolume = np.zeros(0)

    # fix
    def __init__(self, id='', name='', reactions=[], species=[]):
        self.id = id
        self.name = name
        self.reactions = reactions
        self.species = species

    def setupSimulation(self):
        # initialize species counts dictionary
        self.speciesCounts = {}
        for species in self.species:
            self.speciesCounts[species.id] = 0

    def updateLocalCellState(self, model):
        # sets local species counts from global species counts
        for species in self.species:
            self.speciesCounts[species.id] = model.speciesCounts[species.species.index, species.compartment.index]
        self.volume = model.volume
        self.extracellularVolume = model.extracellularVolume

    def updateGlobalCellState(self, model):
        # sets global species counts from local species counts
        for species in self.species:
            model.speciesCounts[species.species.index, species.compartment.index] = self.speciesCounts[species.id]

    def getSpeciesConcentrations(self):
        # get species concentrations
        volumes = self.getSpeciesVolumes()
        concs = {}
        for species in self.species:
            concs[species.id] = self.speciesCounts[species.id] / volumes[species.id] / N_AVOGADRO
        return concs

    def getSpeciesVolumes(self):
        # get container volumes for each species
        volumes = {}
        for species in self.species:
            if species.compartment.id == 'c':
                volumes[species.id] = self.volume
            else:
                volumes[species.id] = self.extracellularVolume
        return volumes

    @staticmethod
    def calcReactionRates(reactions, speciesConcentrations):
        # calculate reaction rates
        rates = np.full(len(reactions), np.nan)
        for iRxn, rxn in enumerate(reactions):
            if rxn.rateLaw:
                rates[iRxn] = eval(rxn.rateLaw.transcoded, {}, {
                                   'speciesConcentrations': speciesConcentrations, 'Vmax': rxn.vmax, 'Km': rxn.km})
        return rates

    @staticmethod
    def executeReaction(speciesCounts, reaction):
        # update species counts based on a reaction
        for part in reaction.participants:
            speciesCounts[part.id] += part.coefficient
        return speciesCounts

    def getComponentById(self, id, components):
        for component in components:
            if component.id == id:
                return component


class FbaSubmodel(Submodel):
    # Represents an FBA submodel

    metabolismProductionReaction = None
    exchangedSpecies = None

    cobraModel = None
    thermodynamicBounds = None
    exchangeRateBounds = None

    defaultFbaBound = 1e15

    dryWeight = np.nan
    reactionFluxes = np.zeros(0)
    growth = np.nan

    solver = 'glpk'

    def __init__(self, *args, **kwargs):
        Submodel.__init__(self, *args, **kwargs)
        self.algorithm = 'FBA'

    def setupSimulation(self):
        '''setup reaction participant, enzyme counts matrices'''
        Submodel.setupSimulation(self)

        '''Setup FBA'''
        cobraModel = CobraModel(self.id)
        self.cobraModel = cobraModel

        # setup metabolites
        cbMets = []
        for species in self.species:
            cbMets.append(CobraMetabolite(id=species.id, name=species.name))
        cobraModel.add_metabolites(cbMets)

        # setup reactions
        for rxn in self.reactions:
            cbRxn = CobraReaction(
                id=rxn.id,
                name=rxn.name,
                lower_bound=-self.defaultFbaBound if rxn.reversible else 0,
                upper_bound=self.defaultFbaBound,
            )
            cobraModel.add_reactions([cbRxn])

            cbMets = {}
            for part in rxn.participants:
                cbMets[part.id] = part.coefficient
                if rxn.id == 'MetabolismProduction' and part.id == 'H2O[c]' and self.solver == 'glpk':  # to compensate for GLPK bug
                    del cbMets[part.id]
            cbRxn.add_metabolites(cbMets)

        # add external exchange reactions
        self.exchangedSpecies = []
        for species in self.species:
            if species.compartment.id == 'e':
                cbRxn = CobraReaction(
                    id='%sEx' % species.species.id,
                    name='%s exchange' % species.species.name,
                    lower_bound=-self.defaultFbaBound,
                    upper_bound=self.defaultFbaBound,
                )
                cobraModel.add_reactions([cbRxn])
                cbRxn.add_metabolites({species.id: 1})

                self.exchangedSpecies.append(ExchangedSpecies(id=species.id, reactionIndex=cobraModel.reactions.index(cbRxn)))

        # add biomass exchange reaction
        cbRxn = CobraReaction(
            id='BiomassEx',
            name='Biomass exchange',
            lower_bound=0,
            upper_bound=self.defaultFbaBound,
        )
        cobraModel.add_reactions([cbRxn])
        cbRxn.add_metabolites({'Biomass[c]': -1})

        '''Bounds'''
        # thermodynamic
        lower_bounds = []
        upper_bounds = []
        for rxn in cobraModel.reactions:
            lower_bounds.append(rxn.lower_bound)
            upper_bounds.append(rxn.upper_bound)
        self.thermodynamicBounds = {
            'lower': np.array(lower_bounds),
            'upper': np.array(upper_bounds),
        }

        # exchange reactions
        carbonExRate = self.getComponentById('carbonExchangeRate', self.parameters).value
        nonCarbonExRate = self.getComponentById('nonCarbonExchangeRate', self.parameters).value
        self.exchangeRateBounds = {
            'lower': np.full(len(cobraModel.reactions), -np.nan),
            'upper': np.full(len(cobraModel.reactions),  np.nan),
        }
        for exSpecies in self.exchangedSpecies:
            if self.getComponentById(exSpecies.id, self.species).species.containsCarbon():
                self.exchangeRateBounds['lower'][exSpecies.reactionIndex] = -carbonExRate
                self.exchangeRateBounds['upper'][exSpecies.reactionIndex] = carbonExRate
            else:
                self.exchangeRateBounds['lower'][exSpecies.reactionIndex] = -nonCarbonExRate
                self.exchangeRateBounds['upper'][exSpecies.reactionIndex] = nonCarbonExRate

        '''Setup reactions'''
        self.metabolismProductionReaction = {
            'index': cobraModel.reactions.index(cobraModel.reactions.get_by_id('MetabolismProduction')),
            'reaction': self.getComponentById('MetabolismProduction', self.reactions),
        }

        cobraModel.objective = 'MetabolismProduction'
        cobraModel.solver = self.solver

    def updateLocalCellState(self, model):
        Submodel.updateLocalCellState(self, model)
        self.dryWeight = model.dryWeight

    def updateGlobalCellState(self, model):
        Submodel.updateGlobalCellState(self, model)
        model.growth = self.growth

    def calcReactionFluxes(self, timeStep=1):
        '''calculate growth rate'''
        solution = self.cobraModel.optimize()
        assert(solution.status == 'optimal')

        self.reactionFluxes = solution.fluxes
        self.growth = self.reactionFluxes[self.metabolismProductionReaction['index']]  # fraction cell/s

    def updateMetabolites(self, timeStep=1):
        # biomass production
        for part in self.metabolismProductionReaction['reaction'].participants:
            self.speciesCounts[part.id] -= self.growth * part.coefficient * timeStep

        # external nutrients
        for exSpecies in self.exchangedSpecies:
            self.speciesCounts[exSpecies.id] += self.reactionFluxes[exSpecies.reactionIndex] * timeStep

    def calcReactionBounds(self,  timeStep=1):
        # thermodynamics
        lowerBounds = self.thermodynamicBounds['lower'].copy()
        upperBounds = self.thermodynamicBounds['upper'].copy()

        # rate laws
        upperBounds[0:len(self.reactions)] = util.nanminimum(
            upperBounds[0:len(self.reactions)],
            self.calcReactionRates(self.reactions, self.getSpeciesConcentrations()) * self.volume * N_AVOGADRO,
        )

        # external nutrients availability
        for exSpecies in self.exchangedSpecies:
            upperBounds[exSpecies.reactionIndex] = max(0, np.minimum(
                upperBounds[exSpecies.reactionIndex], self.speciesCounts[exSpecies.id]) / timeStep)

        # exchange bounds
        lowerBounds = util.nanmaximum(lowerBounds, self.dryWeight / 3600 * N_AVOGADRO * 1e-3 * self.exchangeRateBounds['lower'])
        upperBounds = util.nanminimum(upperBounds, self.dryWeight / 3600 * N_AVOGADRO * 1e-3 * self.exchangeRateBounds['upper'])

        # return
        for i_rxn, rxn in enumerate(self.cobraModel.reactions):
            rxn.lower_bound = lowerBounds[i_rxn]
            rxn.upper_bound = upperBounds[i_rxn]


class SsaSubmodel(Submodel):
    # Represents an SSA submodel

    def __init__(self, *args, **kwargs):
        Submodel.__init__(self, *args, **kwargs)
        self.algorithm = 'SSA'

    def setupSimulation(self):
        Submodel.setupSimulation(self)


class Compartment(object):
    # Represents a compartment

    index = None
    id = ''
    name = ''
    initialVolume = None
    comments = ''

    def __init__(self, id='', name='', initialVolume=None, comments=''):
        self.id = id
        self.name = name
        self.initialVolume = initialVolume
        self.comments = comments


class Species(object):
    # Represents a species

    index = None
    id = ''
    name = ''
    structure = ''
    empiricalFormula = ''
    molecularWeight = None
    charge = None
    type = ''
    concentrations = []
    crossRefs = []
    comments = ''

    # fix
    def __init__(self, id='', name='', structure='', empiricalFormula='', molecularWeight=None,
                 charge=None, type='', concentrations=[], crossRefs=[], comments=''):

        self.id = id
        self.name = name
        self.structure = structure
        self.empiricalFormula = empiricalFormula
        self.molecularWeight = molecularWeight
        self.charge = charge
        self.type = type
        self.concentrations = concentrations
        self.crossRefs = crossRefs

    def containsCarbon(self):
        if self.empiricalFormula:
            return self.empiricalFormula.upper().find('C') != -1
        return False


class Reaction(object):
    # Represents a reaction

    index = None
    id = ''
    name = ''
    submodel = ''
    reversible = None
    participants = []
    enzyme = ''
    rateLaw = None
    vmax = None
    km = None
    crossRefs = []
    comments = ''

    # fix
    def __init__(self, id='', name='', submodel='', reversible=None, participants=[],
                 enzyme='', rateLaw='', vmax=None, km=None, crossRefs=[], comments=''):

        if vmax:
            vmax = float(vmax)
        if km:
            km = float(km)

        self.id = id
        self.name = name
        self.submodel = submodel
        self.reversible = reversible
        self.participants = participants
        self.enzyme = enzyme
        self.rateLaw = rateLaw
        self.vmax = vmax
        self.km = km
        self.crossRefs = crossRefs
        self.comments = comments


class Parameter(object):
    # Represents a model parameter

    index = None
    id = ''
    name = ''
    submodel = None
    value = None
    units = ''
    comments = ''

    def __init__(self, id='', name='', submodel='', value=None, units='', comments=''):
        self.id = id
        self.name = name
        self.submodel = submodel
        self.value = value
        self.units = units
        self.comments = comments


class Reference(object):
    # Represents a reference

    index = None
    id = ''
    name = ''
    crossRefs = []
    comments = ''

    # fix
    def __init__(self, id='', name='', crossRefs=[], comments=''):
        self.id = id
        self.name = name
        self.crossRefs = crossRefs
        self.comments = comments


class Concentration(object):
    # Represents a concentration in a compartment

    compartment = ''
    value = None

    def __init__(self, compartment='', value=None):
        self.compartment = compartment
        self.value = value


class SpeciesCompartment(object):
    # Represents a participant in a submodel

    index = None
    species = ''
    compartment = ''

    id = ''
    name = ''

    def __init__(self, index=None, species='', compartment=''):
        self.index = index
        self.species = species
        self.compartment = compartment

    def calcIdName(self):
        self.id = '%s[%s]' % (self.species.id, self.compartment.id)
        self.name = '%s (%s)' % (self.species.name, self.compartment.name)


class ExchangedSpecies(object):
    # Represents an external

    id = ''
    reactionIndex = None

    def __init__(self, id='', reactionIndex=None):
        self.id = id
        self.reactionIndex = reactionIndex


class ReactionParticipant(object):
    # Represents a participant in a reaction

    species = ''
    compartment = ''
    coefficient = None

    id = ''
    name = ''

    def __init__(self, species='', compartment='', coefficient=None):
        self.species = species
        self.compartment = compartment
        self.coefficient = coefficient

    def calcIdName(self):
        self.id = '%s[%s]' % (self.species.id, self.compartment.id)
        self.name = '%s (%s)' % (self.species.name, self.compartment.name)


class RateLaw(object):
    # Represents a rate law

    native = ''
    transcoded = ''

    def __init__(self, native=''):
        self.native = native or ''

    def getModifiers(self, species, compartments):
        # get modifiers of rate law
        modifiers = []
        for spec in species:
            for comp in compartments:
                id = '%s[%s]' % (spec.id, comp.id)
                if self.native.find(id) != -1:
                    modifiers.append(id)
        return modifiers

    def transcode(self, species, compartments):
        # transcoded for python
        self.transcoded = self.native

        for spec in species:
            for comp in compartments:
                id = '%s[%s]' % (spec.id, comp.id)
                self.transcoded = self.transcoded.replace(id, "speciesConcentrations['%s']" % id)


class Identifier(object):
    # Represents an entry in an external database

    namespace = ''
    id = ''

    def __init__(self, namespace='', id=''):
        self.namespace = namespace
        self.id = id


def getModelFromExcel(filename):
    # Reads model from Excel file into a Python object

    with warnings.catch_warnings():
        warnings.filterwarnings("ignore", "Discarded range with reserved name", UserWarning)
        wb = load_workbook(filename=filename)

    # initialize model object
    model = Model()

    '''Read details from Excel'''
    # submodels
    ws = wb['Submodels']
    for iRow in range(2, ws.max_row + 1):
        id = str(ws.cell(row=iRow, column=1).value)
        name = ws.cell(row=iRow, column=2).value
        algorithm = ws.cell(row=iRow, column=3).value
        if algorithm == 'FBA':
            subModel = FbaSubmodel(id=id, name=name)
        elif algorithm == 'SSA':
            subModel = SsaSubmodel(id=id, name=name)
        model.submodels.append(subModel)

    # compartments
    ws = wb['Compartments']
    for iRow in range(2, ws.max_row + 1):
        model.compartments.append(Compartment(
            id=str(ws.cell(row=iRow, column=1).value),
            name=ws.cell(row=iRow, column=2).value,
            initialVolume=float(ws.cell(row=iRow, column=3).value),
            comments=ws.cell(row=iRow, column=4).value,
        ))

    # species
    ws = wb['Species']
    for iRow in range(2, ws.max_row + 1):
        mwStr = ws.cell(row=iRow, column=5).value
        if mwStr:
            mw = float(mwStr)
        else:
            mw = None

        chargeStr = ws.cell(row=iRow, column=6).value
        if chargeStr:
            charge = float(chargeStr)
        else:
            charge = None

        model.species.append(Species(
            id=str(ws.cell(row=iRow, column=1).value),
            name=ws.cell(row=iRow, column=2).value,
            structure=ws.cell(row=iRow, column=3).value,
            empiricalFormula=ws.cell(row=iRow, column=4).value,
            molecularWeight=mw,
            charge=charge,
            type=ws.cell(row=iRow, column=7).value,
            concentrations=[
                Concentration(compartment='c', value=float(ws.cell(row=iRow, column=8).value or 0)),
                Concentration(compartment='e', value=float(ws.cell(row=iRow, column=9).value or 0)),
            ],
            crossRefs=[
                Identifier(
                    namespace=ws.cell(row=iRow, column=10).value,
                    id=ws.cell(row=iRow, column=11).value,
                ),
            ],
            comments=ws.cell(row=iRow, column=12).value,
        ))

    # reactions
    ws = wb['Reactions']

    for iRow in range(2, ws.max_row + 1):
        stoichiometry = parseStoichiometry(ws.cell(row=iRow, column=4).value)

        rateLawStr = ws.cell(row=iRow, column=6).value
        if rateLawStr:
            rateLaw = RateLaw(rateLawStr)
        else:
            rateLaw = None

        model.reactions.append(Reaction(
            id=str(ws.cell(row=iRow, column=1).value),
            name=ws.cell(row=iRow, column=2).value,
            submodel=ws.cell(row=iRow, column=3).value,
            reversible=stoichiometry['reversible'],
            participants=stoichiometry['participants'],
            enzyme=ws.cell(row=iRow, column=5).value,
            rateLaw=rateLaw,
            vmax=ws.cell(row=iRow, column=7).value,
            km=ws.cell(row=iRow, column=8).value,
            crossRefs=[
                Identifier(
                    namespace=ws.cell(row=iRow, column=9).value,
                    id=ws.cell(row=iRow, column=10).value,
                ),
            ],
            comments=ws.cell(row=iRow, column=11).value,
        ))

    # parameters
    ws = wb['Parameters']
    for iRow in range(2, ws.max_row + 1):
        model.parameters.append(Parameter(
            id=str(ws.cell(row=iRow, column=1).value),
            name=ws.cell(row=iRow, column=2).value,
            submodel=ws.cell(row=iRow, column=3).value,
            value=float(ws.cell(row=iRow, column=4).value),
            units=ws.cell(row=iRow, column=5).value,
            comments=ws.cell(row=iRow, column=6).value,
        ))

    # references
    ws = wb['References']
    for iRow in range(2, ws.max_row + 1):
        model.references.append(Reference(
            id=str(ws.cell(row=iRow, column=1).value),
            name=ws.cell(row=iRow, column=2).value,
            crossRefs=[
                Identifier(
                    namespace=ws.cell(row=iRow, column=3).value,
                    id=ws.cell(row=iRow, column=4).value,
                ),
            ],
            comments=ws.cell(row=iRow, column=5).value,
        ))

    '''set component indices'''
    model.setComponentIndices()

    '''deserialize references'''
    # species concentration
    for species in model.species:
        for conc in species.concentrations:
            id = conc.compartment
            obj = model.getComponentById(id, model.compartments)
            conc.compartment = obj

    # reaction submodel, participant species, participant compartments, enzymes
    for reaction in model.reactions:
        id = reaction.submodel
        obj = model.getComponentById(id, model.submodels)
        reaction.submodel = obj

        for part in reaction.participants:

            id = part.species
            obj = model.getComponentById(id, model.species)
            part.species = obj

            id = part.compartment
            obj = model.getComponentById(id, model.compartments)
            part.compartment = obj

            part.calcIdName()

        id = reaction.enzyme
        obj = model.getComponentById(id, model.species)
        reaction.enzyme = obj

    # parameter submodels
    for param in model.parameters:
        id = param.submodel
        if id:
            obj = model.getComponentById(id, model.submodels)
            param.submodel = obj

    ''' Assemble back references'''
    for subModel in model.submodels:
        subModel.reactions = []
        subModel.species = []
        subModel.parameters = []
    for rxn in model.reactions:
        rxn.submodel.reactions.append(rxn)
        for part in rxn.participants:
            rxn.submodel.species.append('%s[%s]' % (part.species.id, part.compartment.id))
        if rxn.enzyme:
            rxn.submodel.species.append('%s[%s]' % (rxn.enzyme.id, 'c'))
        if rxn.rateLaw:
            rxn.submodel.species += rxn.rateLaw.getModifiers(model.species, model.compartments)

    for param in model.parameters:
        if param.submodel:
            param.submodel.parameters.append(param)

    for subModel in model.submodels:
        speciesStrArr = list(set(subModel.species))
        speciesStrArr.sort()
        subModel.species = []
        for index, speciesStr in enumerate(speciesStrArr):
            speciesId, compId = speciesStr.split('[')
            compId = compId[0:-1]
            speciesComp = SpeciesCompartment(
                index=index,
                species=model.getComponentById(speciesId, model.species),
                compartment=model.getComponentById(compId, model.compartments),
            )
            speciesComp.calcIdName()
            subModel.species.append(speciesComp)

    '''Transcode rate laws'''
    for rxn in model.reactions:
        if rxn.rateLaw:
            rxn.rateLaw.transcode(model.species, model.compartments)

    '''Prepare submodels for computation'''
    model.setupSimulation()

    '''Return'''
    return model


def parseStoichiometry(rxnStr):
    # Parse a string representing the stoichiometry of a reaction into a Python object

    # Split stoichiometry in to global compartment, left-hand side, right-hand side, reversibility indictor
    rxnMatch = re.match(r'(?P<compartment>\[([a-z])\]: )?(?P<lhs>((\(\d*\.?\d*([e][-+]?[0-9]+)?\) )?[a-z0-9\-_]+(\[[a-z]\])? \+ )*(\(\d*\.?\d*([e][-+]?[0-9]+)?\) )?[a-z0-9\-_]+(\[[a-z]\])?) (?P<direction>[<]?)==> (?P<rhs>((\(\d*\.?\d*([e][-+]?[0-9]+)?\) )?[a-z0-9\-_]+(\[[a-z]\])? \+ )*(\(\d*\.?\d*([e][-+]?[0-9]+)?\) )?[a-z0-9\-_]+(\[[a-z]\])?)', rxnStr, flags=re.I)
    if rxnMatch is None:
        raise ValueError('Invalid stoichiometry: %s' % rxnStr)

    # Determine reversiblity
    rxnDict = rxnMatch.groupdict()
    reversible = rxnDict['direction'] == '<'

    # Determine if global compartment for reaction was specified
    if rxnDict['compartment'] is None:
        globalComp = None
    else:
        globalComp = re.match(r'\[(?P<compartment>[a-z])\]', rxnDict['compartment'], flags=re.I).groupdict()['compartment']

    # initialize array of reaction participants
    participants = []

    # Parse left-hand side
    for rxnPartStr in rxnDict['lhs'].split(' + '):
        rxnPartDict = re.match(
            r'(\((?P<coefficient>\d*\.?\d*([e][-+]?[0-9]+)?)\) )?(?P<species>[a-z0-9\-_]+)(\[(?P<compartment>[a-z])\])?', rxnPartStr, flags=re.I).groupdict()

        species = rxnPartDict['species']
        compartment = rxnPartDict['compartment'] or globalComp
        coefficient = float(rxnPartDict['coefficient'] or 1)

        participants.append(ReactionParticipant(
            species=species,
            compartment=compartment,
            coefficient=-coefficient,
        ))

    # Parse right-hand side
    for rxnPartStr in rxnDict['rhs'].split(' + '):
        rxnPartDict = re.match(
            r'(\((?P<coefficient>\d*\.?\d*([e][-+]?[0-9]+)?)\) )?(?P<species>[a-z0-9\-_]+)(\[(?P<compartment>[a-z])\])?', rxnPartStr, flags=re.I).groupdict()

        species = rxnPartDict['species']
        compartment = rxnPartDict['compartment'] or globalComp
        coefficient = float(rxnPartDict['coefficient'] or 1)

        participants.append(ReactionParticipant(
            species=species,
            compartment=compartment,
            coefficient=coefficient,
        ))

    return {
        'reversible': reversible,
        'participants': participants,
    }