Example #1
0
def test_none_in_rxn_pat():
    Monomer('A')
    Monomer('B')
    Rule('rule1', A() + None >> None + B(), Parameter('k', 1))
    Initial(A(), Parameter('A_0', 100))
    Observable('B_', B())
    npts = 200
    kres = run_simulation(model, time=100, points=npts, seed=_KAPPA_SEED)

    # check that rule1's reaction pattern parses with ComplexPatterns
    as_complex_pattern(A()) + None >> None + as_complex_pattern(B())
Example #2
0
def _cp_embeds_into(cp1, cp2):
    """Check that any state in ComplexPattern2 is matched in ComplexPattern1.
    """
    # Check that any state in cp2 is matched in cp2
    # If the thing we're matching to is just a monomer pattern, that makes
    # things easier--we just need to find the corresponding monomer pattern
    # in cp1
    cp1 = as_complex_pattern(cp1)
    cp2 = as_complex_pattern(cp2)
    if len(cp2.monomer_patterns) == 1:
        mp2 = cp2.monomer_patterns[0]
        # Iterate over the monomer patterns in cp1 and see if there is one
        # that has the same name
        for mp1 in cp1.monomer_patterns:
            if _mp_embeds_into(mp1, mp2):
                return True
    return False
Example #3
0
def _cp_embeds_into(cp1, cp2):
    """Check that any state in ComplexPattern2 is matched in ComplexPattern1.
    """
    # Check that any state in cp2 is matched in cp1
    # If the thing we're matching to is just a monomer pattern, that makes
    # things easier--we just need to find the corresponding monomer pattern
    # in cp1
    if cp1 is None or cp2 is None:
        return False
    cp1 = as_complex_pattern(cp1)
    cp2 = as_complex_pattern(cp2)
    if len(cp2.monomer_patterns) == 1:
        mp2 = cp2.monomer_patterns[0]
        # Iterate over the monomer patterns in cp1 and see if there is one
        # that has the same name
        for mp1 in cp1.monomer_patterns:
            if _mp_embeds_into(mp1, mp2):
                return True
    return False
Example #4
0
    def __init__(self, complex_pattern, lumping_rate, counter_species=None):
        try:
            self.complex_pattern = as_complex_pattern(complex_pattern)
        except InvalidComplexPatternException:
            raise ValueError('complex_pattern must be a ComplexPattern')

        if not isinstance(lumping_rate, Parameter):
            raise ValueError('lumping_rate must be a %s' % Parameter.__class__)

        self.lumping_rate = lumping_rate
        if counter_species is None:
            self.counter_species = None
        else:
            self.counter_species = str(counter_species)
Example #5
0
def test_multistate():
    Monomer('A', ['a', 'a'], {'a': ['u', 'p']})
    Parameter('k1', 100)
    Parameter('A_0', 200)
    Rule('r1', None >> A(a=MultiState('u', 'p')), k1)
    Initial(A(a=MultiState(('u', 1), 'p')) %
            A(a=MultiState(('u', 1), 'u')), A_0)

    generate_equations(model)

    assert model.species[0].is_equivalent_to(
        A(a=MultiState(('u', 1), 'p')) % A(a=MultiState(('u', 1), 'u')))
    assert model.species[1].is_equivalent_to(
        as_complex_pattern(A(a=MultiState('u', 'p'))))
Example #6
0
def test_stochkit_earm_multi_initials():
    model = earm_1_0.model
    tspan = np.linspace(0, 1000, 10)
    sim = StochKitSimulator(model, tspan=tspan)
    unbound_L = model.monomers['L'](b=None)
    simres = sim.run(initials={unbound_L: [3000, 1500]},
                     n_runs=2, seed=_STOCHKIT_SEED, algorithm="ssa")
    df = simres.dataframe

    unbound_L_index = model.get_species_index(as_complex_pattern(unbound_L))

    # Check we have two repeats of each initial
    assert np.allclose(df.loc[(slice(None), 0), '__s%d' % unbound_L_index],
                       [3000, 3000, 1500, 1500])
Example #7
0
File: bng.py Project: alubbock/pysb
    def __init__(self, complex_pattern, lumping_rate, counter_species=None):
        try:
            self.complex_pattern = as_complex_pattern(complex_pattern)
        except InvalidComplexPatternException:
            raise ValueError('complex_pattern must be a ComplexPattern')

        if not isinstance(lumping_rate, Parameter):
            raise ValueError('lumping_rate must be a %s' % Parameter.__class__)

        self.lumping_rate = lumping_rate
        if counter_species is None:
            self.counter_species = None
        else:
            self.counter_species = str(counter_species)
def test_stochkit_earm_multi_initials():
    model = earm_1_0.model
    tspan = np.linspace(0, 1000, 10)
    sim = StochKitSimulator(model, tspan=tspan)
    unbound_L = model.monomers['L'](b=None)
    simres = sim.run(initials={unbound_L: [3000, 1500]},
                     n_runs=2, seed=_STOCHKIT_SEED, algorithm="ssa")
    df = simres.dataframe

    unbound_L_index = model.get_species_index(as_complex_pattern(unbound_L))

    # Check we have two repeats of each initial
    assert np.allclose(df.loc[(slice(None), 0), '__s%d' % unbound_L_index],
                       [3000, 3000, 1500, 1500])
Example #9
0
File: base.py Project: shf43/pysb
        def _set_initials(initials_source):
            for cp, value_obj in initials_source:
                cp = as_complex_pattern(cp)
                si = self._model.get_species_index(cp)
                if si is None:
                    raise IndexError("Species not found in model: %s" %
                                     repr(cp))
                # Loop over all simulations
                for sim in range(len(y0)):
                    # If this initial condition has already been set, skip it
                    # (i.e., an override)
                    if not np.isnan(y0[sim][si]):
                        continue

                    def _get_value(sim):
                        if isinstance(value_obj, (collections.Sequence,
                                                  np.ndarray)) and \
                           isinstance(value_obj[sim], numbers.Number):
                            value = value_obj[sim]
                        elif isinstance(value_obj, Component):
                            if value_obj in self._model.parameters:
                                pi = self._model.parameters.index(value_obj)
                                value = self.param_values[
                                    sim if n_sims_params > 1 else 0][pi]
                            elif value_obj in self._model.expressions:
                                value = value_obj.expand_expr().evalf(subs=subs[sim])
                        else:
                            raise TypeError("Unexpected initial condition "
                                            "value type: %s" % type(value_obj))
                        return value

                    # initials from the model
                    if isinstance(initials_source, np.ndarray):
                        if len(initials_source.shape) == 1:
                            if sim == 0:
                                value = _get_value(0)
                            else:
                                # if the parameters are different for each sim,
                                # the expressions could be different too
                                if value_obj in self._model.expressions:
                                    value = value_obj.expand_expr().evalf(
                                        subs=subs[sim])
                                else:
                                    value = y0[sim-1][si]
                    # initials from dict
                    else:
                         value = _get_value(sim)
                    y0[sim][si] = value
Example #10
0
def synthesize(species, ksynth):
    """Generate a reaction which synthesizes a species.

    Note that `species` must be "concrete", i.e. the state of all
    sites in all of its monomers must be specified. No site may be
    left unmentioned.

    Parameters
    ----------
    species : Monomer, MonomerPattern or ComplexPattern
        The species to synthesize. If a Monomer, sites are considered
        as unbound and in their default state. If a pattern, must be
        concrete.
    ksynth : Parameters or number
        Synthesis rate. If a Parameter is passed, it will be used directly in
        the generated Rule. If a number is passed, a Parameter will be created
        with an automatically generated name based on the names and site states
        of the components of `species` and this parameter will be included at
        the end of the returned component list.

    Returns
    -------
    components : ComponentSet
        The generated components. Contains the unidirectional synthesis Rule and
        optionally a Parameter if ksynth was given as a number.

    Examples
    --------
        Model()
        Monomer('A', ['x', 'y'], {'y': ['e', 'f']})
        synthesize(A(x=None, y='e'), 1e-4)

    """

    def synthesize_name_func(rule_expression):
        cps = rule_expression.product_pattern.complex_patterns
        return '_'.join(_complex_pattern_label(cp) for cp in cps)

    # TODO: either the >> operator should work with a monomer, or complexpattern
    # shouldn't blow up if it is called
    if isinstance(species, Monomer):
        species = species()
    species = as_complex_pattern(species)
    if not species.is_concrete():
        raise ValueError("species must be concrete")

    return _macro_rule('synthesize', None >> species, [ksynth], ['k'],
                       name_func=synthesize_name_func)
Example #11
0
def synthesize(species, ksynth):
    """Generate a reaction which synthesizes a species.

    Note that `species` must be "concrete", i.e. the state of all
    sites in all of its monomers must be specified. No site may be
    left unmentioned.

    Parameters
    ----------
    species : Monomer, MonomerPattern or ComplexPattern
        The species to synthesize. If a Monomer, sites are considered
        as unbound and in their default state. If a pattern, must be
        concrete.
    ksynth : Parameters or number
        Synthesis rate. If a Parameter is passed, it will be used directly in
        the generated Rule. If a number is passed, a Parameter will be created
        with an automatically generated name based on the names and site states
        of the components of `species` and this parameter will be included at
        the end of the returned component list.

    Returns
    -------
    components : ComponentSet
        The generated components. Contains the unidirectional synthesis Rule and
        optionally a Parameter if ksynth was given as a number.

    Examples
    --------
        Model()
        Monomer('A', ['x', 'y'], {'y': ['e', 'f']})
        synthesize(A(x=None, y='e'), 1e-4)

    """

    def synthesize_name_func(rule_expression):
        cps = rule_expression.product_pattern.complex_patterns
        return '_'.join(_complex_pattern_label(cp) for cp in cps)

    if isinstance(species, Monomer):
        species = species()
    species = as_complex_pattern(species)
    if not species.is_concrete():
        raise ValueError("species must be concrete")

    return _macro_rule('synthesize', None >> species, [ksynth], ['k'],
                       name_func=synthesize_name_func)
Example #12
0
def degrade(species, kdeg):
    """Generate a reaction which degrades a species.

    Note that `species` is not required to be "concrete".

    Parameters
    ----------
    species : Monomer, MonomerPattern or ComplexPattern
        The species to synthesize. If a Monomer, sites are considered
        as unbound and in their default state. If a pattern, must be
        concrete.
    kdeg : Parameters or number
        Degradation rate. If a Parameter is passed, it will be used directly in
        the generated Rule. If a number is passed, a Parameter will be created
        with an automatically generated name based on the names and site states
        of the components of `species` and this parameter will be included at
        the end of the returned component list.

    Returns
    -------
    components : ComponentSet
        The generated components. Contains the unidirectional degradation Rule
        and optionally a Parameter if ksynth was given as a number.

    Examples
    --------
        Model()
        Monomer('B', ['x'])
        degrade(B(), 1e-6)  # degrade all B, even bound species

    """

    def degrade_name_func(rule_expression):
        cps = rule_expression.reactant_pattern.complex_patterns
        return '_'.join(_complex_pattern_label(cp) for cp in cps)

    # TODO: the >> operator should work with a monomer, or complexpattern
    # shouldn't blow up if it is called
    if isinstance(species, Monomer):
        species = species()
    species = as_complex_pattern(species)

    return _macro_rule('degrade', species >> None, [kdeg], ['k'],
                       name_func=degrade_name_func)
Example #13
0
def degrade(species, kdeg):
    """Generate a reaction which degrades a species.

    Note that `species` is not required to be "concrete".

    Parameters
    ----------
    species : Monomer, MonomerPattern or ComplexPattern
        The species to synthesize. If a Monomer, sites are considered
        as unbound and in their default state. If a pattern, must be
        concrete.
    kdeg : Parameters or number
        Degradation rate. If a Parameter is passed, it will be used directly in
        the generated Rule. If a number is passed, a Parameter will be created
        with an automatically generated name based on the names and site states
        of the components of `species` and this parameter will be included at
        the end of the returned component list.

    Returns
    -------
    components : ComponentSet
        The generated components. Contains the unidirectional degradation Rule
        and optionally a Parameter if ksynth was given as a number.

    Examples
    --------
        Model()
        Monomer('B', ['x'])
        degrade(B(), 1e-6)  # degrade all B, even bound species

    """

    def degrade_name_func(rule_expression):
        cps = rule_expression.reactant_pattern.complex_patterns
        return '_'.join(_complex_pattern_label(cp) for cp in cps)

    if isinstance(species, Monomer):
        species = species()
    species = as_complex_pattern(species)

    return _macro_rule('degrade', species >> None, [kdeg], ['k'],
                       name_func=degrade_name_func)
Example #14
0
 def _set_initials(initials_source):
     for cp, value_obj in initials_source:
         cp = as_complex_pattern(cp)
         si = self._model.get_species_index(cp)
         if si is None:
             raise IndexError("Species not found in model: %s" %
                              repr(cp))
         # If this initial condition has already been set, skip it
         if y0[si] != 0:
             continue
         if isinstance(value_obj, (int, float)):
             value = value_obj
         elif value_obj in self._model.parameters:
             pi = self._model.parameters.index(value_obj)
             value = param_vals[pi]
         elif value_obj in self._model.expressions:
             value = value_obj.expand_expr().evalf(subs=subs)
         else:
             raise ValueError("Unexpected initial condition value type")
         y0[si] = value
Example #15
0
    def export(self, initials=None, param_values=None):
        """Generate the corresponding StochKit2 XML for a PySB model

        Parameters
        ----------
        initials : list of numbers
            List of initial species concentrations overrides
            (must be same length as model.species). If None,
            the concentrations from the model are used.
        param_values : list
            List of parameter value overrides (must be same length as
            model.parameters). If None, the parameter values from the model
            are used.

        Returns
        -------
        string
            The model in StochKit2 XML format
        """
        if self.model.compartments:
            raise CompartmentsNotSupported()

        generate_equations(self.model)
        document = etree.Element("Model")

        d = etree.Element('Description')

        d.text = 'Exported from PySB Model: %s' % self.model.name
        document.append(d)

        # Number of Reactions
        nr = etree.Element('NumberOfReactions')
        nr.text = str(len(self.model.reactions))
        document.append(nr)

        # Number of Species
        ns = etree.Element('NumberOfSpecies')
        ns.text = str(len(self.model.species))
        document.append(ns)

        if param_values is None:
            # Get parameter values from model if not supplied
            param_values = [p.value for p in self.model.parameters]
        else:
            # Validate length
            if len(param_values) != len(self.model.parameters):
                raise Exception('param_values must be a list of numeric '
                                'parameter values the same length as '
                                'model.parameters')

        # Get initial species concentrations from model if not supplied
        if initials is None:
            initials = np.zeros((len(self.model.species),))
            subs = dict((p, param_values[i]) for i, p in
                        enumerate(self.model.parameters))

            for ic in self.model.initials:
                cp = as_complex_pattern(ic.pattern)
                si = self.model.get_species_index(cp)
                if si is None:
                    raise IndexError("Species not found in model: %s" %
                                     repr(cp))
                if ic.value in self.model.parameters:
                    pi = self.model.parameters.index(ic.value)
                    value = param_values[pi]
                elif ic.value in self.model.expressions:
                    value = ic.value.expand_expr().evalf(subs=subs)
                else:
                    raise ValueError(
                        "Unexpected initial condition value type")
                initials[si] = value
        else:
            # Validate length
            if len(initials) != len(self.model.species):
                raise Exception('initials must be a list of numeric initial '
                                'concentrations the same length as '
                                'model.species')

        # Species
        spec = etree.Element('SpeciesList')
        for s_id in range(len(self.model.species)):
            spec.append(self._species_to_element('__s%d' % s_id,
                                                 initials[s_id]))
        document.append(spec)

        # Parameters
        params = etree.Element('ParametersList')
        for p_id, param in enumerate(self.model.parameters):
            p_name = param.name
            if p_name == 'vol':
                p_name = '__vol'
            p_value = param.value if param_values is None else \
                param_values[p_id]
            params.append(self._parameter_to_element(p_name, p_value))
        # Default volume parameter value
        params.append(self._parameter_to_element('vol', 1.0))

        document.append(params)

        # Expressions and observables
        expr_strings = {
            e.name: '(%s)' % sympy.ccode(
                e.expand_expr(expand_observables=True)
            )
            for e in self.model.expressions
        }

        # Reactions
        reacs = etree.Element('ReactionsList')
        pattern = re.compile("(__s\d+)\*\*(\d+)")
        for rxn_id, rxn in enumerate(self.model.reactions):
            rxn_name = 'Rxn%d' % rxn_id
            rxn_desc = 'Rules: %s' % str(rxn["rule"])

            reactants = defaultdict(int)
            products = defaultdict(int)
            # reactants
            for r in rxn["reactants"]:
                reactants["__s%d" % r] += 1
            # products
            for p in rxn["products"]:
                products["__s%d" % p] += 1
            # replace terms like __s**2 with __s*(__s-1)
            rate = str(rxn["rate"])

            matches = pattern.findall(rate)
            for m in matches:
                repl = m[0]
                for i in range(1, int(m[1])):
                    repl += "*(%s-%d)" % (m[0], i)
                rate = re.sub(pattern, repl, rate, count=1)

            # expand only expressions used in the rate eqn
            for e in {sym for sym in rxn["rate"].atoms()
                      if isinstance(sym, Expression)}:
                rate = re.sub(r'\b%s\b' % e.name,
                              expr_strings[e.name],
                              rate)

            total_reactants = sum(reactants.values())
            rxn_params = rxn["rate"].atoms(Parameter)
            rate = None
            if total_reactants <= 2 and len(rxn_params) == 1:
                # Try to parse as mass action to avoid compiling custom
                # propensity functions in StochKit (slow for big models)
                rxn_param = rxn_params.pop()
                putative_rate = sympy.Mul(*[sympy.symbols(r) ** r_stoich for
                                            r, r_stoich in
                                            reactants.items()]) * rxn_param

                rxn_floats = rxn["rate"].atoms(sympy.Float)
                rate_mul = 1.0
                if len(rxn_floats) == 1:
                    rate_mul = next(iter(rxn_floats))
                    putative_rate *= rate_mul

                if putative_rate == rxn["rate"]:
                    # Reaction is mass-action, set rate to a Parameter or float
                    if len(rxn_floats) == 0:
                        rate = rxn_param
                    elif len(rxn_floats) == 1:
                        rate = rxn_param.value * float(rate_mul)

                    if rate is not None and len(reactants) == 1 and \
                            max(reactants.values()) == 2:
                        # Need rate * 2 in addition to any rate factor
                        rate = (rate.value if isinstance(rate, Parameter)
                                else rate) * 2.0

            if rate is None:
                # Custom propensity function needed

                rxn_atoms = rxn["rate"].atoms()

                # replace terms like __s**2 with __s*(__s-1)
                rate = str(rxn["rate"])

                matches = pattern.findall(rate)
                for m in matches:
                    repl = m[0]
                    for i in range(1, int(m[1])):
                        repl += "*(%s-%d)" % (m[0], i)
                    rate = re.sub(pattern, repl, rate, count=1)

                # expand only expressions used in the rate eqn
                for e in {sym for sym in rxn_atoms
                          if isinstance(sym, Expression)}:
                    rate = re.sub(r'\b%s\b' % e.name,
                                  expr_strings[e.name],
                                  rate)

            reacs.append(self._reaction_to_element(rxn_name,
                                                   rxn_desc,
                                                   rate,
                                                   reactants,
                                                   products))
        document.append(reacs)

        if pretty_print:
            return etree.tostring(document, pretty_print=True).decode('utf8')
        else:
            # Hack to print pretty xml without pretty-print
            # (requires the lxml module).
            doc = etree.tostring(document)
            xmldoc = xml.dom.minidom.parseString(doc)
            uglyXml = xmldoc.toprettyxml(indent='  ')
            text_re = re.compile(">\n\s+([^<>\s].*?)\n\s+</", re.DOTALL)
            prettyXml = text_re.sub(">\g<1></", uglyXml)
            return prettyXml
Example #16
0
    def export(self, initials=None, param_values=None):
        """Generate the corresponding StochKit2 XML for a PySB model

        Parameters
        ----------
        initials : list of numbers
            List of initial species concentrations overrides
            (must be same length as model.species). If None,
            the concentrations from the model are used.
        param_values : list
            List of parameter value overrides (must be same length as
            model.parameters). If None, the parameter values from the model
            are used.

        Returns
        -------
        string
            The model in StochKit2 XML format
        """
        generate_equations(self.model)
        document = etree.Element("Model")

        d = etree.Element('Description')

        d.text = 'Exported from PySB Model: %s' % self.model.name
        document.append(d)

        # Number of Reactions
        nr = etree.Element('NumberOfReactions')
        nr.text = str(len(self.model.reactions))
        document.append(nr)

        # Number of Species
        ns = etree.Element('NumberOfSpecies')
        ns.text = str(len(self.model.species))
        document.append(ns)

        if param_values is None:
            # Get parameter values from model if not supplied
            param_values = [p.value for p in self.model.parameters]
        else:
            # Validate length
            if len(param_values) != len(self.model.parameters):
                raise Exception('param_values must be a list of numeric '
                                'parameter values the same length as '
                                'model.parameters')

        # Get initial species concentrations from model if not supplied
        if initials is None:
            initials = np.zeros((len(self.model.species), ))
            subs = dict((p, param_values[i])
                        for i, p in enumerate(self.model.parameters))

            for cp, value_obj in self.model.initial_conditions:
                cp = as_complex_pattern(cp)
                si = self.model.get_species_index(cp)
                if si is None:
                    raise IndexError("Species not found in model: %s" %
                                     repr(cp))
                if isinstance(value_obj, (int, float)):
                    value = value_obj
                elif value_obj in self.model.parameters:
                    pi = self.model.parameters.index(value_obj)
                    value = param_values[pi]
                elif value_obj in self.model.expressions:
                    value = value_obj.expand_expr().evalf(subs=subs)
                else:
                    raise ValueError("Unexpected initial condition value type")
                initials[si] = value
        else:
            # Validate length
            if len(initials) != len(self.model.species):
                raise Exception('initials must be a list of numeric initial '
                                'concentrations the same length as '
                                'model.species')

        # Species
        spec = etree.Element('SpeciesList')
        for s_id in range(len(self.model.species)):
            spec.append(
                self._species_to_element('__s%d' % s_id, initials[s_id]))
        document.append(spec)

        # Parameters
        params = etree.Element('ParametersList')
        for p_id, param in enumerate(self.model.parameters):
            p_name = param.name
            if p_name == 'vol':
                p_name = '__vol'
            p_value = param.value if param_values is None else \
                param_values[p_id]
            params.append(self._parameter_to_element(p_name, p_value))
        # Default volume parameter value
        params.append(self._parameter_to_element('vol', 1.0))

        document.append(params)

        # Expressions and observables
        expr_strings = {
            e.name:
            '(%s)' % sympy.ccode(e.expand_expr(expand_observables=True))
            for e in self.model.expressions
        }

        # Reactions
        reacs = etree.Element('ReactionsList')
        pattern = re.compile("(__s\d+)\*\*(\d+)")
        for rxn_id, rxn in enumerate(self.model.reactions):
            rxn_name = 'Rxn%d' % rxn_id
            rxn_desc = 'Rules: %s' % str(rxn["rule"])

            reactants = defaultdict(int)
            products = defaultdict(int)
            # reactants
            for r in rxn["reactants"]:
                reactants["__s%d" % r] += 1
            # products
            for p in rxn["products"]:
                products["__s%d" % p] += 1
            # replace terms like __s**2 with __s*(__s-1)
            rate = str(rxn["rate"])

            matches = pattern.findall(rate)
            for m in matches:
                repl = m[0]
                for i in range(1, int(m[1])):
                    repl += "*(%s-%d)" % (m[0], i)
                rate = re.sub(pattern, repl, rate, count=1)

            # expand only expressions used in the rate eqn
            for e in {
                    sym
                    for sym in rxn["rate"].atoms()
                    if isinstance(sym, Expression)
            }:
                rate = re.sub(r'\b%s\b' % e.name, expr_strings[e.name], rate)

            total_reactants = sum(reactants.values())
            rxn_params = rxn["rate"].atoms(Parameter)
            rate = None
            if total_reactants <= 2 and len(rxn_params) == 1:
                # Try to parse as mass action to avoid compiling custom
                # propensity functions in StochKit (slow for big models)
                rxn_param = rxn_params.pop()
                putative_rate = sympy.Mul(*[
                    sympy.symbols(r)**r_stoich
                    for r, r_stoich in reactants.items()
                ]) * rxn_param

                rxn_floats = rxn["rate"].atoms(sympy.Float)
                rate_mul = 1.0
                if len(rxn_floats) == 1:
                    rate_mul = next(iter(rxn_floats))
                    putative_rate *= rate_mul

                if putative_rate == rxn["rate"]:
                    # Reaction is mass-action, set rate to a Parameter or float
                    if len(rxn_floats) == 0:
                        rate = rxn_param
                    elif len(rxn_floats) == 1:
                        rate = rxn_param.value * float(rate_mul)

                    if rate is not None and len(reactants) == 1 and \
                            max(reactants.values()) == 2:
                        # Need rate * 2 in addition to any rate factor
                        rate = (rate.value
                                if isinstance(rate, Parameter) else rate) * 2.0

            if rate is None:
                # Custom propensity function needed

                rxn_atoms = rxn["rate"].atoms()

                # replace terms like __s**2 with __s*(__s-1)
                rate = str(rxn["rate"])

                matches = pattern.findall(rate)
                for m in matches:
                    repl = m[0]
                    for i in range(1, int(m[1])):
                        repl += "*(%s-%d)" % (m[0], i)
                    rate = re.sub(pattern, repl, rate, count=1)

                # expand only expressions used in the rate eqn
                for e in {
                        sym
                        for sym in rxn_atoms if isinstance(sym, Expression)
                }:
                    rate = re.sub(r'\b%s\b' % e.name, expr_strings[e.name],
                                  rate)

            reacs.append(
                self._reaction_to_element(rxn_name, rxn_desc, rate, reactants,
                                          products))
        document.append(reacs)

        if pretty_print:
            return etree.tostring(document, pretty_print=True)
        else:
            # Hack to print pretty xml without pretty-print
            # (requires the lxml module).
            doc = etree.tostring(document)
            xmldoc = xml.dom.minidom.parseString(doc)
            uglyXml = xmldoc.toprettyxml(indent='  ')
            text_re = re.compile(">\n\s+([^<>\s].*?)\n\s+</", re.DOTALL)
            prettyXml = text_re.sub(">\g<1></", uglyXml)
            return prettyXml