Module abstochkin.process

Define a process of the form Reactants -> Products.

Expand source code
""" Define a process of the form Reactants -> Products. """

#  Copyright (c) 2023, Alex Plakantonakis.
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.

import contextlib
import re
from typing import Self

from numpy import array


class Process:
    """
    Define a unidirectional process: Reactants -> Products, where the
    Reactants and Products are specified using standard chemical notation.
    That is, stoichiometric coefficients (integers) and species names are
    specified. For example: 2A + B -> C.

    Attributes
    ----------
    reactants : dict
        The reactants of a given process are specified with
        key-value pairs describing each species name and its
        stoichiometric coefficient, respectively.
    products : dict
        The products of a given process are specified with
        key-value pairs describing each species name and its
        stoichiometric coefficient, respectively.
    k : float, int, list of floats, tuple of floats
        The *microscopic* rate constant(s) for the given process. The data
        type of `k` determines the "structure" of the population as follows:
            - A homogeneous population: if `k` is a single value (float or int),
              then the population is assumed to be homogeneous with all agents
              of the reactant species having kinetics defined by this value.
            - A heterogeneous population with a distinct number of subspecies
              (each with a corresponding `k` value): if `k` is a list of floats,
              then the population is assumed to be heterogeneous with a number
              of subspecies equal to the length of the list.
            - A heterogeneous population with normally-distributed `k` values:
              If `k` is a tuple whose length is 2, then the population is
              assumed to be heterogeneous with a normally distributed `k` value.
              The two entries in the tuple represent the mean and standard
              deviation (in that order) of the desired normal distribution.
    order : int
        The order of the process (or the molecularity of an elementary process).
        It is the sum of the stoichiometric coefficients of the reactants.
    species : set of strings
        A set of all species in a process.
    reacts_ : list of strings
        A list containing all the reactants in a process.
    prods_ : list of strings
        A list containing all the products in a process.

    Methods
    -------
    from_string
        Class method for creating a Process object from a string.
    """

    def __init__(self,
                 reactants: dict,
                 products: dict,
                 k: float | int | list[float, ...] | tuple[float, float],
                 **kwargs):

        self.reactants = reactants
        self.products = products
        self.k = k

        self._validate_nums()  # make sure there are no errors in given numbers

        # For consistency with processes instantiated using the class method `from_string()`,
        # species denoted as 'None' for a 0th order process are renamed to ''.
        if 'None' in self.reactants.keys():
            self.reactants[''] = self.reactants.pop('None')
        if 'None' in self.products.keys():
            self.products[''] = self.products.pop('None')

        self.order = sum(self.reactants.values())

        self.is_heterogeneous = False if isinstance(self.k, (int, float)) else True

        if self.order == 0:
            msg = "Since a birth process does not depended on the presence of agents, " \
                  "heterogeneity does not make sense in this context. Please define " \
                  "the rate constant k as a number. "
            assert not self.is_heterogeneous, msg

        # Two ways of storing the involved species:
        # 1) A set of all species
        self.species = set((self.reactants | self.products).keys())
        with contextlib.suppress(KeyError):
            self.species.remove('')  # remove empty species name from any 0th order processes

        # 2) Separate lists
        self.reacts_ = list()  # [reactant species]
        self.prods_ = list()  # [product species]
        self._get_reacts_prods_()

        """ Because Process objects are used as keys in dictionaries used 
        in an AbStochKin simulation, it's much faster to generate the object's 
        string representation once, and then access it whenever it's needed 
        (which could be thousands of times during a simulation). """
        self._str = self.__str__().split(';')[0]

        if len(kwargs) > 0:
            self._lsp(kwargs)

    def _get_reacts_prods_(self):
        """ Make lists of the reactant and product species. Repeated
        elements of a list reflect the order or molecularity of the
        species in the given process. For example, for the process
        `2A + B -> C + D, reacts_ = ['A', 'A', 'B'], prods_ = ['C', 'D']`. """
        for r, m in self.reactants.items():
            for i in range(m):
                self.reacts_.append(r)

        for p, m in self.products.items():
            for i in range(m):
                self.prods_.append(p)

        if '' in self.reacts_:  # remove empty reactant species names
            self.reacts_.remove('')  # from 0th order processes
        if '' in self.prods_:  # remove empty product species names
            self.prods_.remove('')  # from degradation processes

    @classmethod
    def from_string(cls,
                    proc_str: str,
                    /,
                    k: float | int | list[float, ...] | tuple[float, float],
                    *,
                    sep: str = '->',
                    **kwargs) -> Self:
        """ Create a process from a string.

        Parameters
        ----------
        proc_str : str
            A string describing the process in standard chemical notation
            (e.g., 'A + B -> C')
        k : float or int or list of floats or 2-tuple of floats
            The rate constant for the given process. If `k` is a float or
            int, then the process is homogeneous. If `k` is a list, then
            the population of the reactants constsists of distinct subspecies
            or subinteractions depending on the order. If `k` is a 2-tuple,
            then the constant is normally-distributed with a mean and standard
            deviation specified in the tuple's elements.
        sep : str, default: '->'
            Specifies the characters that distinguish the reactants from the
            products. The default is '->'. The code also treats `-->` as a
            default, if it's present in `proc_str`.

        Notes
        -----
        - Species names should not contain spaces, dashes, and
          should start with a non-numeric character.
        - Zeroth order processes should be specified by an empty space or 'None'.

        Examples
        --------
        >>> Process.from_string("2A + B -> X", 0.3)
        >>> Process.from_string(" -> Y", 0.1)  # for a 0th order (birth) process.
        >>> Process.from_string("Protein_X -> None", 0.15)  # for a 1st order degradation process.
        """

        if len(kwargs) > 0:
            cls._lsp(kwargs)

        sep = '-->' if '-->' in proc_str else sep
        if sep not in proc_str:
            raise Exception("Cannot distinguish the reactants from the products.\n"
                            "Please use the *sep* keyword: e.g. sep='->'.")

        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side

        return cls(cls._to_dict(lhs_terms), cls._to_dict(rhs_terms), k)

    @staticmethod
    def _lsp(kwargs: dict):
        """
        The `Process` class accepts additional arguments (`**kwargs`).
        Since the Process class is a base class for other subclasses,
        this is done so that the Liskov Substitution Principle (LSP)
        is not violated.
        (https://en.wikipedia.org/wiki/Liskov_substitution_principle).
        This way, subclasses override the `from_string` method and have
        additional parameters while remaining consistent with this method
        from their base class. Calling the base instance with the additional
        parameters gives a warning that they will have no effect so that
        the user can intervene, if that's desired.
        """
        msg = f"Warning: Additional parameters {','.join([str(i) for i in kwargs.items()])} " \
              f"will have no effect. "
        if 'k_rev' in kwargs.keys():
            msg += f"If that's not what you intended, define the process " \
                   f"using ReversibleProcess()."
        if 'regulating_species' in kwargs.keys() or 'alpha' in kwargs.keys() or \
                'nH' in kwargs.keys() or 'K50' in kwargs.keys():
            msg += f"If that's not what you intended, define the process " \
                   f"using RegulatedProcess()."
        if 'catalyst' in kwargs.keys():
            msg += f"If that's not what you intended, define the process " \
                   f"using MichaelisMentenProcess()."
        print(msg)

    @staticmethod
    def _to_dict(terms: list) -> dict:
        """ Convert the information for a side of a process to a dictionary. """

        side_terms = dict()  # for storing the information of a side of a process
        patt = '^[\\-]*[1-9]+'  # regex pattern (accounts for leading erroneous minus sign)

        if len(terms) == 1 and terms[0].strip().lower() in ['', 'none']:
            spec = ''  # Zeroth order process
            side_terms[spec] = 0
        else:
            for term in terms:
                term = term.strip()
                try:
                    match = re.search(patt, term)
                    stoic_coef = term[slice(*match.span())]  # extract stoichiometric coef
                    spec = re.split(patt, term)[-1].strip()  # extract species name
                    if spec == '' and stoic_coef != 0:
                        raise NullSpeciesNameError()
                    stoic_coef = int(stoic_coef)
                except AttributeError:  # when there is no specified stoichiometric coefficient
                    spec = re.split(patt, term)[-1]  # extract species name
                    stoic_coef = 1

                if spec not in side_terms.keys():
                    side_terms[spec] = stoic_coef
                else:
                    side_terms[spec] += stoic_coef

        return side_terms

    def _validate_nums(self):
        """ Make sure coefficients and rate constant values are not negative. """
        for r, val in (self.reactants | self.products).items():
            assert val >= 0, f"Coefficient cannot be negative: {val} {r}."

        error_msg = f"Rate constant values have to be positive: k = {self.k}."
        if isinstance(self.k, (list, tuple)):  # heterogeneous population
            assert all(array(self.k) > 0), error_msg
        else:  # when k is a float or int, the population is homogeneous
            assert self.k > 0, error_msg

        # For normally-distributed k values, specification is a 2-tuple.
        if isinstance(self.k, tuple):  # normal distribution of k values
            assert len(self.k) == 2, "Please specify the mean and standard deviation " \
                                     "of k in a 2-tuple: (mean, std)."

    def __eq__(self, other):
        if isinstance(other, Process):
            is_equal = (self.k == other.k and
                        self.order == other.order and
                        self.reactants == other.reactants and
                        self.products == other.products and
                        self.species == other.species)
            return is_equal
        elif isinstance(other, str):
            return self._str == other or self._str.replace(' ', '') == other
        else:
            print(f"{type(self)} and {type(other)} are instances of different classes.")
            return False

    def __hash__(self):
        return hash(self._str)

    def __contains__(self, item):
        return True if item in self.species else False

    def __repr__(self):
        return f"Process Object: Process.from_string('{self._str.split(',')[0]}', k={self.k})"

    def __str__(self):
        if isinstance(self.k, (float, int)):
            het_str = "Homogeneous process."
        elif isinstance(self.k, list):
            het_str = f"Heterogeneous process with {len(self.k)} distinct subspecies."
        else:
            het_str = f"Heterogeneous process with normally-distributed k with " \
                      f"mean {self.k[0]} and standard deviation {self.k[1]}."

        lhs, rhs = self._reconstruct_string()
        return ' -> '.join([lhs, rhs]) + f', k = {self.k}; {het_str}'

    def _reconstruct_string(self):
        lhs = ' + '.join([f"{str(val) + ' ' if val not in [0, 1] else ''}{key}" for key, val in
                          self.reactants.items()])
        rhs = ' + '.join([f"{str(val) + ' ' if val not in [0, 1] else ''}{key}" for key, val in
                          self.products.items()])
        return lhs, rhs


class ReversibleProcess(Process):
    """ Define a reversible process.

    Attributes
    ----------
    k_rev : float or int or list of floats or 2-tuple of floats
        The *microscopic* rate constant for the reverse process.
    is_heterogeneous_rev : bool
        Denotes if the parameter `k_rev` exhibits heterogeneity
        (distinct subspecies/interactions or normally-distributed).

    Notes
    -----
    A ReversibleProcess object gets split into two Process objects
    (forward and reverse process) when the algorithm runs.
    """

    def __init__(self, reactants: dict, products: dict,
                 k: float | int | list[float, ...] | tuple[float, float],
                 k_rev: float | int | list[float, ...] | tuple[float, float]):
        self.k_rev = k_rev  # rate constant for reverse process
        self.is_heterogeneous_rev = False if isinstance(self.k_rev, (int, float)) else True

        super().__init__(reactants, products, k)

        self.order_rev = sum(self.products.values())

    @classmethod
    def from_string(cls,
                    proc_str: str,
                    /,
                    k: float | int | list[float, ...] | tuple[float, float],
                    *,
                    k_rev: float | int | list[float, ...] | tuple[float, float] = 0,
                    sep: str = '<->') -> Self:
        """ Create a reversible process from a string.

        Parameters
        ----------
        proc_str : str
            A string describing the process in standard chemical notation
            (e.g., 'A + B <-> C')
        k : float or int or list of floats or 2-tuple of floats
            The *microscopic* rate constant for the forward process.
        k_rev : float or int or list of floats or 2-tuple of floats
            The *microscopic* rate constant for the reverse process.
        sep : str, default: '<->'
            Specifies the characters that distinguish the reactants from the
            products. The default is '<->'. The code also treats `<-->` as a
            default, if it's present in `proc_str`.

        Notes
        -----
        - Species names should not contain spaces, dashes, and
          should start with a non-numeric character.

        Examples
        --------
        >>> ReversibleProcess.from_string("2A + B <-> X", 0.3, k_rev=0.2)
        """
        for s in ['<-->', '<=>', '<==>']:
            sep = s if s in proc_str else sep
        if sep not in proc_str:
            raise Exception("Cannot distinguish the reactants from the products.\n"
                            "Please use the *sep* keyword: e.g. sep='<->'.")

        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side

        return cls(cls._to_dict(lhs_terms), cls._to_dict(rhs_terms),
                   k, k_rev)

    def __repr__(self):
        return f"ReversibleProcess Object: ReversibleProcess.from_string(" \
               f"'{self._str.split(',')[0]}', k={self.k}, k_rev={self.k_rev})"

    def __str__(self):
        if isinstance(self.k, (float, int)):
            het_str = "Forward homogeneous process."
        elif isinstance(self.k, list):
            het_str = f"Forward heterogeneous process with {len(self.k)} " \
                      f"distinct subspecies."
        else:
            het_str = f"Forward heterogeneous process with normally-distributed " \
                      f"k with mean {self.k[0]} and standard deviation {self.k[1]}."

        if isinstance(self.k_rev, (float, int)):
            het_rev_str = "Reverse homogeneous process."
        elif isinstance(self.k_rev, list):
            het_rev_str = f"Reverse heterogeneous process with {len(self.k_rev)} " \
                          f"distinct subspecies."
        else:
            het_rev_str = f"Reverse heterogeneous process with normally-distributed " \
                          f"k with mean {self.k_rev[0]} and standard deviation {self.k_rev[1]}."

        lhs, rhs = self._reconstruct_string()
        return " <-> ".join([lhs, rhs]) + f", k = {self.k}, k_rev = {self.k_rev}; " \
                                          f"{het_str} {het_rev_str}"

    def _reconstruct_string(self):
        lhs = ' + '.join([f"{str(val) + ' ' if val not in [0, 1] else ''}{key}" for key, val in
                          self.reactants.items()])
        rhs = ' + '.join([f"{str(val) + ' ' if val not in [0, 1] else ''}{key}" for key, val in
                          self.products.items()])
        return lhs, rhs

    def __eq__(self, other):
        if isinstance(other, ReversibleProcess):
            is_equal = (self.k == other.k and
                        self.order == other.order and
                        self.k_rev == other.k_rev and
                        self.order_rev == other.order_rev and
                        self.reactants == other.reactants and
                        self.products == other.products and
                        self.species == other.species)
            return is_equal
        elif isinstance(other, str):
            return self._str == other or self._str.replace(' ', '') == other
        else:
            print(f"{type(self)} and {type(other)} are instances of different classes.")
            return False

    def __hash__(self):
        return hash(self._str)


class MichaelisMentenProcess(Process):
    """ Define a process that obeys Michaelis-Menten kinetics.

    Attributes
    ----------
    catalyst : str
        Name of the species acting as a catalyst for this process.
    Km : float or int or list of floats or 2-tuple of floats
        *Microscopic* Michaelis constant. Corresponds to the number
        of `catalyst` agents that would produce half-maximal activity.
        Heterogeneity in this parameter is determined by the type of `K50`,
        using the same rules as for parameter `k`.
    is_heterogeneous_Km : bool
        Denotes if the parameter `Km` exhibits heterogeneity
        (distinct subspecies/interactions or normally-distributed).
    """

    def __init__(self, reactants: dict, products: dict,
                 k: float | int | list[float | int, ...] | tuple[float | int, float | int],
                 catalyst: str,
                 Km: float | int | list[float | int, ...] | tuple[float | int, float | int]):
        self.catalyst = catalyst
        self.Km = Km
        self.is_heterogeneous_Km = False if isinstance(self.Km, (int, float)) else True

        super().__init__(reactants, products, k)

        self.species.add(self.catalyst)
        self._str += f", catalyst = {self.catalyst}, Km = {self.Km}"

        assert self.order != 0, "A 0th order process has no substrate for a catalyst " \
                                "to act on, therefore it cannot follow Michaelis-Menten kinetics."
        if self.order == 2:
            raise NotImplementedError

    @classmethod
    def from_string(cls,
                    proc_str: str,
                    /,
                    k: float | int | list[float | int, ...] | tuple[float | int, float | int],
                    *,
                    catalyst: str = None,
                    Km: float | int | list[float | int, ...] | tuple[
                        float | int, float | int] = None,
                    sep: str = '->') -> Self:
        """ Create a Michaelis-Menten process from a string.

        Parameters
        ----------
        proc_str : str
            A string describing the process in standard chemical notation
            (e.g., 'A + B -> C')
        k : float or int or list of floats or 2-tuple of floats
            The *microscopic* rate constant for the given process. If `k` is a
            float or int, then the process is homogeneous. If `k` is a list, then
            the population of the reactants constsists of distinct subspecies
            or subinteractions depending on the order. If `k` is a 2-tuple,
            then the constant is normally-distributed with a mean and standard
            deviation specified in the tuple's elements.
        catalyst : str
            Name of species acting as a catalyst.
        Km : float or int or list of floats or 2-tuple of floats
            *Microscopic* Michaelis constant for the process.
            Heterogeneity in this parameter is determined by the type of `Km`,
            using the same rules as for parameter `k`.
        sep : str, default: '->'
            Specifies the characters that distinguish the reactants from the
            products. The default is '->'. The code also treats `-->` as a
            default, if it's present in `proc_str`.

        Notes
        -----
        - Species names should not contain spaces, dashes, and
          should start with a non-numeric character.
        - Zeroth order processes should be specified by an empty space or 'None'.

        Examples
        --------
        >>> MichaelisMentenProcess.from_string("A -> X", k=0.3, catalyst='E', Km=10)
        >>> MichaelisMentenProcess.from_string("A -> X", k=0.3, catalyst='alpha', Km=(10, 1))
        """
        sep = '-->' if '-->' in proc_str else sep
        if sep not in proc_str:
            raise Exception("Cannot distinguish the reactants from the products.\n"
                            "Please use the *sep* keyword: e.g. sep='->'.")

        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side

        return cls(cls._to_dict(lhs_terms), cls._to_dict(rhs_terms),
                   k,
                   catalyst, Km)

    def __str__(self):
        if isinstance(self.Km, (float, int)):
            Km_het_str = "Homogeneous process with respect to Km."
        elif isinstance(self.k, list):
            Km_het_str = f"Heterogeneous process with respect to Km " \
                         f"with {len(self.Km)} distinct subspecies."
        else:
            Km_het_str = f"Heterogeneous process with normally-distributed Km with " \
                         f"mean {self.Km[0]} and standard deviation {self.Km[1]}."

        return super().__str__() + f" Catalyst: {self.catalyst}, " \
                                   f"Km = {self.Km}, {Km_het_str}"

    def __repr__(self):
        return f"MichaelisMentenProcess Object: " \
               f"MichaelisMentenProcess.from_string('{self._str.split(',')[0]}', " \
               f"k={self.k}, " \
               f"catalyst='{self.catalyst}', " \
               f"Km={self.Km})"

    def __eq__(self, other):
        if isinstance(other, MichaelisMentenProcess):
            is_equal = (self.k == other.k and
                        self.order == other.order and
                        self.reactants == other.reactants and
                        self.products == other.products and
                        self.catalyst == other.catalyst and
                        self.Km == other.Km and
                        self.species == other.species)
            return is_equal
        elif isinstance(other, str):
            return self._str == other or self._str.replace(' ', '') == other
        else:
            print(f"{type(self)} and {type(other)} are instances of different classes.")
            return False

    def __hash__(self):
        return hash(self._str)


class RegulatedProcess(Process):
    """ Define a process that is regulated.

    This class allows a Process to be defined in terms of how it is regulated.
    If there is only one regulating species, then the parameters have the same
    type as would be expected for a homogeneous/heterogeneous process.
    If there are multiple regulating species, then all parameters are a list
    of their expected type, with the length of the list being equal to the
    number of regulating species.
    
    Attributes
    ----------
    k : float or int or list of floats or 2-tuple of floats
        The *microscopic* rate constant for the given process. It is the *basal*
        rate constant in the case of activation (or the minimum `k` value)
        and the maximum rate constant in the case of repression.
    regulating_species : str or list of str
        Name of the regulating species. Multiple species can be specified as
        comma-separated in a string or a list of strings with the species names.
    alpha : float or int or list[float or int]
        Parameter denoting the degree of activation/repression.

            - 0 <= alpha < 1: repression
            - alpha = 1: no regulation
            - alpha > 1: activation
            
        alpha is a multiplier: in the case of activation, the maximum 
        rate constant value will be `alpha * k`. 
        In the case of repression, the minimum 
        rate constant value will be `alpha * k`. 
    K50 : float or int or list of floats or 2-tuple of floats or list[float or int or list of floats or 2-tuple of floats]
        *Microscopic* constant that corresponds to the number of
        `regulating_species` agents that would produce 
        half-maximal activation/repression. 
        Heterogeneity in this parameter is determined by the type of `K50`,
        using the same rules as for parameter `k`.
    nH : float or int or list[float or int]
        Hill coefficient for the given process. Indicates the degree of 
        cooperativity in the regulatory interaction. 
    is_heterogeneous_K50 : bool or list of bool
        Denotes if the parameter `K50` exhibits heterogeneity
        (distinct subspecies/interactions or normally-distributed).
    regulation_type : str or list of str
        The type of regulation for this process based on the value of alpha:
        'activation' or 'repression' or 'no regulation'.

    Notes
    -----
    Allowing a 0th order process to be regulated. However, heterogeneity
    in `k` and `K50` (or any other parameters) is not allowed for such
    a process.
    """

    def __init__(self, reactants: dict, products: dict,
                 k: float | int | list[float, ...] | tuple[float, float],
                 regulating_species: str | list[str, ...],
                 alpha: float | int | list[float | int, ...],
                 K50: float | int | list[float | int, ...] | tuple[float | int, float | int] |
                      list[float | int | list[float | int, ...] | tuple[float | int, float | int]],
                 nH: float | int | list[float | int, ...]):
        if isinstance(regulating_species, str):
            reg_sp_list = regulating_species.replace(' ', '').split(',')
            self.regulating_species = reg_sp_list[0] if len(reg_sp_list) == 1 else reg_sp_list
        else:  # if it is a list
            self.regulating_species = regulating_species

        self.alpha = alpha
        self.K50 = K50
        self.nH = nH

        if isinstance(K50, list):
            self.is_heterogeneous_K50 = [False if isinstance(val, (int, float)) else True for val
                                         in K50]
        else:
            self.is_heterogeneous_K50 = False if isinstance(self.K50, (int, float)) else True

        super().__init__(reactants, products, k)

        self._str += f", regulating_species = {self.regulating_species}, alpha = {self.alpha}, " \
                     f"K50 = {self.K50}, nH = {self.nH}"

        self._validate_reg_params()

        if isinstance(self.alpha, list):
            self.regulation_type = list()
            for a in self.alpha:
                reg_type = 'activation' if a > 1 else 'repression' if a < 1 else 'no regulation'
                self.regulation_type.append(reg_type)
        else:
            self.regulation_type = 'activation' if self.alpha > 1 else 'repression' if self.alpha < 1 else 'no regulation'

    def _validate_reg_params(self):
        """ Validate the parameters specific to the regulation. """
        if isinstance(self.regulating_species, list):  # multiple regulating species
            # First check that the right number of values for each parameter are specified
            rs_num = len(self.regulating_species)
            msg = f"Must specify {rs_num} # values when there are {rs_num} regulating species."
            assert len(self.alpha) == rs_num, msg.replace('#', 'alpha')
            assert len(self.K50) == rs_num, msg.replace('#', 'K50')
            assert len(self.nH) == rs_num, msg.replace('#', 'nH')

            for i in range(len(self.regulating_species)):
                assert self.alpha[i] >= 0, "The alpha parameter must be nonnegative."
                if self.alpha[i] == 1:
                    print("Warning: alpha=1 means the process is not regulated.")

                if isinstance(self.K50[i], (float, int)):
                    assert self.K50[i] > 0, "K50 has to be positive."
                elif isinstance(self.K50[i], list):
                    assert all([True if val > 0 else False for val in self.K50[i]]), \
                        "Subspecies K50 values have to be positive."
                else:  # isinstance(self.K50, tuple)
                    assert self.K50[i][0] > 0 and self.K50[i][1] > 0, \
                        "Mean and std of K50 have to be positive."

                if self.order == 0:
                    assert not self.is_heterogeneous_K50[i], \
                        "Heterogeneity in parameter K50 is not allowed for a 0th order process."

                assert self.nH[i] > 0, "nH has to be positive."

        else:  # just one regulating species
            assert self.alpha >= 0, "The alpha parameter must be nonnegative."
            if self.alpha == 1:
                print("Warning: alpha=1 means the process is not regulated.")

            if isinstance(self.K50, (float, int)):
                assert self.K50 > 0, "K50 has to be positive."
            elif isinstance(self.K50, list):
                assert all([True if val > 0 else False for val in self.K50]), \
                    "Subspecies K50 values have to be positive."
            else:  # isinstance(self.K50, tuple)
                assert self.K50[0] > 0 and self.K50[1] > 0, \
                    "Mean and std of K50 have to be positive."

            if self.order == 0:
                assert not self.is_heterogeneous_K50, \
                    "Heterogeneity in parameter K50 is not allowed for a 0th order process."

            assert self.nH > 0, "nH has to be positive."

    @classmethod
    def from_string(cls,
                    proc_str: str,
                    /,
                    k: float | int | list[float, ...] | tuple[float, float],
                    *,
                    regulating_species: str | list[str, ...] = None,
                    alpha: float | int | list[float | int, ...] = 1,
                    K50: float | int | list[float | int, ...] | tuple[float | int, float | int] |
                         list[float | int | list[float | int, ...] | tuple[
                             float | int, float | int]] = None,
                    nH: float | int | list[float | int, ...] = None,
                    sep: str = '->') -> Self:
        """ Create a regulated process from a string.

        Parameters
        ----------
        proc_str : str
            A string describing the process in standard chemical notation
            (e.g., 'A + B -> C')
        k : float or int or list of floats or 2-tuple of floats
            The *microscopic* rate constant for the given process. It is the *basal*
            rate constant in the case of activation (or the minimum `k` value) 
            and the maximum rate constant in the case of repression. 
            If `k` is a float or int, then the process is homogeneous. 
            If `k` is a list, then the population of the reactants 
            constsists of distinct subspecies or subinteractions 
            depending on the order. If `k` is a 2-tuple,
            then the constant is normally-distributed with a mean and standard
            deviation specified in the tuple's elements. Note that `k` cannot
            be zero for this form of regulation.
        regulating_species : str or list of str
            Name of the regulating species.
        alpha : float or int or list[float or int]
            Parameter denoting the degree of activation/repression.

                - 0 <= alpha < 1: repression
                - alpha = 1: no regulation
                - alpha > 1: activation
                
            alpha is a multiplier: in the case of activation, the maximum 
            rate constant value will be `alpha * k`. 
            In the case of repression, the minimum 
            rate constant value will be `alpha * k`. 
        K50 : float or int or list of floats or 2-tuple of floats or list of each of the previous types
            *Microscopic* constant that corresponds to the number of
            `regulating_species` agents that would produce 
            half-maximal activation/repression. 
            Heterogeneity in this parameter is determined by the type of `K50`,
            using the same rules as for parameter `k`.
        nH : float or int or list[float or int]
            Hill coefficient for the given process. Indicates the degree of 
            cooperativity in the regulatory interaction. 
        sep : str, default: '->'
            Specifies the characters that distinguish the reactants from the
            products. The default is '->'. The code also treats `-->` as a
            default, if it's present in `proc_str`.

        Notes
        -----
        - Species names should not contain spaces, dashes, and
          should start with a non-numeric character.
        - Zeroth order processes should be specified by an empty space or 'None'.

        Examples
        --------
        >>> RegulatedProcess.from_string("A -> X", k=0.2, regulating_species='X', alpha=2, K50=10, nH=1)
        >>> RegulatedProcess.from_string("A -> X", k=0.3, regulating_species='X', alpha=0.5, K50=[10, 15], nH=2)
        >>> RegulatedProcess.from_string("A + B -> X", k=0.5, regulating_species='B, X', alpha=[2, 0], K50=[(15, 5), [10, 15]], nH=[1, 2])
        """
        sep = '-->' if '-->' in proc_str else sep
        if sep not in proc_str:
            raise Exception("Cannot distinguish the reactants from the products.\n"
                            "Please use the *sep* keyword: e.g. sep='->'.")

        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side

        return cls(cls._to_dict(lhs_terms), cls._to_dict(rhs_terms),
                   k,
                   regulating_species, alpha, K50, nH)

    def __str__(self):
        if isinstance(self.regulating_species, list):
            K50_het_str = ""
            for i, sp in enumerate(self.regulating_species):
                if isinstance(self.K50[i], (float, int)):
                    K50_het_str += f"Homogeneous process with respect to species {sp} K50. "
                elif isinstance(self.K50[i], list):
                    K50_het_str += f"Heterogeneous process with respect to species {sp} K50 " \
                                   f"with {len(self.K50[i])} distinct subspecies. "
                else:
                    K50_het_str += f"Heterogeneous process with normally-distributed " \
                                   f"species {sp} K50 with mean {self.K50[i][0]} and " \
                                   f"standard deviation {self.K50[i][1]}. "
        else:
            if isinstance(self.K50, (float, int)):
                K50_het_str = "Homogeneous process with respect to K50."
            elif isinstance(self.K50, list):
                K50_het_str = f"Heterogeneous process with respect to K50 " \
                              f"with {len(self.K50)} distinct subspecies."
            else:
                K50_het_str = f"Heterogeneous process with normally-distributed K50 with " \
                              f"mean {self.K50[0]} and standard deviation {self.K50[1]}."

        return super().__str__() + f" Regulating Species: {self.regulating_species}, " \
                                   f"alpha = {self.alpha}, nH = {self.nH}, " \
                                   f"K50 = {self.K50}, {K50_het_str}"

    def __repr__(self):
        return f"RegulatedProcess Object: " \
               f"RegulatedProcess.from_string('{self._str.split(',')[0]}', " \
               f"k={self.k}, " \
               f"regulating_species='{self.regulating_species}', " \
               f"alpha={self.alpha}, " \
               f"K50={self.K50}, " \
               f"nH={self.nH})"

    def __eq__(self, other):
        if isinstance(other, RegulatedProcess):
            is_equal = (self.k == other.k and
                        self.order == other.order and
                        self.reactants == other.reactants and
                        self.products == other.products and
                        self.regulating_species == other.regulating_species and
                        self.alpha == other.alpha and
                        self.K50 == other.K50 and
                        self.nH == other.nH and
                        self.species == other.species)
            return is_equal
        elif isinstance(other, str):
            return self._str == other or self._str.replace(' ', '') == other
        else:
            print(f"{type(self)} and {type(other)} are instances of different classes.")
            return False

    def __hash__(self):
        return hash(self._str)


class RegulatedMichaelisMentenProcess(RegulatedProcess):
    """ Define a process that is regulated and obeys Michaelis-Menten kinetics.

    This class allows a Michaelis-Menten Process to be defined
    in terms of how it is regulated.
    If there is only one regulating species, then the parameters have the same
    type as would be expected for a homogeneous/heterogeneous process.
    If there are multiple regulating species, then all parameters are a list
    of their expected type, with the length of the list being equal to the
    number of regulating species.

    Attributes
    ----------
    k : float or int or list of floats or 2-tuple of floats
        The *microscopic* rate constant for the given process. It is the *basal*
        rate constant in the case of activation (or the minimum `k` value)
        and the maximum rate constant in the case of repression.
    regulating_species : str or list of str
        Name of the regulating species. Multiple species can be specified as
        comma-separated in a string or a list of strings with the species names.
    alpha : float or int or list[float or int]
        Parameter denoting the degree of activation/repression.

            - 0 <= alpha < 1: repression
            - alpha = 1: no regulation
            - alpha > 1: activation

        alpha is a multiplier: in the case of activation, the maximum
        rate constant value will be `alpha * k`.
        In the case of repression, the minimum
        rate constant value will be `alpha * k`.
    K50 : float or int or list of floats or 2-tuple of floats or list[float or int or list of floats or 2-tuple of floats]
        *Microscopic* constant that corresponds to the number of
        `regulating_species` agents that would produce
        half-maximal activation/repression.
        Heterogeneity in this parameter is determined by the type of `K50`,
        using the same rules as for parameter `k`.
    nH : float or int or list[float or int]
        Hill coefficient for the given process. Indicates the degree of
        cooperativity in the regulatory interaction.
    is_heterogeneous_K50 : bool or list of bool
        Denotes if the parameter `K50` exhibits heterogeneity
        (distinct subspecies/interactions or normally-distributed).
    regulation_type : str or list of str
        The type of regulation for this process based on the value of alpha:
        'activation' or 'repression' or 'no regulation'.
    catalyst : str
        Name of the species acting as a catalyst for this process.
    Km : float or int or list of floats or 2-tuple of floats
        *Microscopic* Michaelis constant. Corresponds to the number
        of `catalyst` agents that would produce half-maximal activity.
        Heterogeneity in this parameter is determined by the type of `K50`,
        using the same rules as for parameter `k`.
    is_heterogeneous_Km : bool
        Denotes if the parameter `Km` exhibits heterogeneity
        (distinct subspecies/interactions or normally-distributed).

    Notes
    -----
    Currently only implemented for 1st order processes. 0th order processes
    cannot obey Michaelis-Menten kinetics and 2nd order Michaelis-Menten
    processes are not implemented yet.
    """

    def __init__(self, reactants: dict, products: dict,
                 k: float | int | list[float, ...] | tuple[float, float],
                 regulating_species: str | list[str, ...],
                 alpha: float | int | list[float | int, ...],
                 K50: float | int | list[float | int, ...] | tuple[float | int, float | int] |
                      list[float | int | list[float | int, ...] | tuple[float | int, float | int]],
                 nH: float | int | list[float | int, ...],
                 catalyst: str,
                 Km: float | int | list[float | int, ...] | tuple[float | int, float | int]):
        self.catalyst = catalyst
        self.Km = Km
        self.is_heterogeneous_Km = False if isinstance(self.Km, (int, float)) else True

        super().__init__(reactants, products, k, regulating_species, alpha, K50, nH)

        self.species.add(self.catalyst)
        self._str += f", catalyst = {self.catalyst}, Km = {self.Km}"

        assert self.order != 0, "A 0th order process has no substrate for a catalyst " \
                                "to act on, therefore it cannot follow Michaelis-Menten kinetics."
        if self.order == 2:
            raise NotImplementedError

    @classmethod
    def from_string(cls,
                    proc_str: str,
                    /,
                    k: float | int | list[float, ...] | tuple[float, float],
                    *,
                    regulating_species: str | list[str, ...] = None,
                    alpha: float | int | list[float | int, ...] = 1,
                    K50: float | int | list[float | int, ...] | tuple[float | int, float | int] |
                         list[float | int | list[float | int, ...] | tuple[
                             float | int, float | int]] = None,
                    nH: float | int | list[float | int, ...] = None,
                    catalyst: str = None,
                    Km: float | int | list[float | int, ...] | tuple[
                        float | int, float | int] = None,
                    sep: str = '->') -> Self:
        """ Create a regulated Michaelis-Menten process from a string.

        Parameters
        ----------
        proc_str : str
            A string describing the process in standard chemical notation
            (e.g., 'A + B -> C')
        k : float or int or list of floats or 2-tuple of floats
            The *microscopic* rate constant for the given process. It is the *basal*
            rate constant in the case of activation (or the minimum `k` value)
            and the maximum rate constant in the case of repression.
            If `k` is a float or int, then the process is homogeneous.
            If `k` is a list, then the population of the reactants
            constsists of distinct subspecies or subinteractions
            depending on the order. If `k` is a 2-tuple,
            then the constant is normally-distributed with a mean and standard
            deviation specified in the tuple's elements. Note that `k` cannot
            be zero for this form of regulation.
        regulating_species : str or list of str
            Name of the regulating species.
        alpha : float or int or list[float or int]
            Parameter denoting the degree of activation/repression.

            - 0 <= alpha < 1: repression
            - alpha = 1: no regulation
            - alpha > 1: activation

            alpha is a multiplier: in the case of activation, the maximum
            rate constant value will be `alpha * k`.
            In the case of repression, the minimum
            rate constant value will be `alpha * k`.
        K50 : float or int or list of floats or 2-tuple of floats or list of each of the previous types
            *Microscopic* constant that corresponds to the number of
            `regulating_species` agents that would produce
            half-maximal activation/repression.
            Heterogeneity in this parameter is determined by the type of `K50`,
            using the same rules as for parameter `k`.
        nH : float or int or list[float or int]
            Hill coefficient for the given process. Indicates the degree of
            cooperativity in the regulatory interaction.
        catalyst : str
            Name of species acting as a catalyst.
        Km : float or int or list of floats or 2-tuple of floats
            *Microscopic* Michaelis constant for the process.
            Heterogeneity in this parameter is determined by the type of `Km`,
            using the same rules as for parameter `k`.
        sep : str, default: '->'
            Specifies the characters that distinguish the reactants from the
            products. The default is '->'. The code also treats `-->` as a
            default, if it's present in `proc_str`.

        Notes
        -----
        - Species names should not contain spaces, dashes, and
          should start with a non-numeric character.
        - Zeroth order processes should be specified by an empty space or 'None'.

        Examples
        --------
        >>> RegulatedMichaelisMentenProcess.from_string("A -> X", k=0.2, regulating_species='X', alpha=2, K50=10, nH=1, catalyst='E', Km=15)
        >>> RegulatedMichaelisMentenProcess.from_string("A -> X", k=0.3, regulating_species='A', alpha=0.5, K50=[10, 15], nH=2, catalyst='C', Km=5)
        """
        sep = '-->' if '-->' in proc_str else sep
        if sep not in proc_str:
            raise Exception("Cannot distinguish the reactants from the products.\n"
                            "Please use the *sep* keyword: e.g. sep='->'.")

        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side

        return cls(cls._to_dict(lhs_terms), cls._to_dict(rhs_terms),
                   k,
                   regulating_species, alpha, K50, nH,
                   catalyst, Km)

    def __str__(self):
        if isinstance(self.regulating_species, list):
            K50_het_str = ""
            for i, sp in enumerate(self.regulating_species):
                if isinstance(self.K50[i], (float, int)):
                    K50_het_str += f"Homogeneous process with respect to species {sp} K50. "
                elif isinstance(self.K50[i], list):
                    K50_het_str += f"Heterogeneous process with respect to species {sp} K50 " \
                                   f"with {len(self.K50[i])} distinct subspecies. "
                else:
                    K50_het_str += f"Heterogeneous process with normally-distributed " \
                                   f"species {sp} K50 with mean {self.K50[i][0]} and " \
                                   f"standard deviation {self.K50[i][1]}. "
        else:
            if isinstance(self.K50, (float, int)):
                K50_het_str = "Homogeneous process with respect to K50."
            elif isinstance(self.K50, list):
                K50_het_str = f"Heterogeneous process with respect to K50 " \
                              f"with {len(self.K50)} distinct subspecies."
            else:
                K50_het_str = f"Heterogeneous process with normally-distributed K50 with " \
                              f"mean {self.K50[0]} and standard deviation {self.K50[1]}."

        if isinstance(self.Km, (float, int)):
            Km_het_str = "Homogeneous process with respect to Km."
        elif isinstance(self.k, list):
            Km_het_str = f"Heterogeneous process with respect to Km " \
                         f"with {len(self.Km)} distinct subspecies."
        else:
            Km_het_str = f"Heterogeneous process with normally-distributed Km with " \
                         f"mean {self.Km[0]} and standard deviation {self.Km[1]}."

        return super().__str__() + f" Regulating Species: {self.regulating_species}, " \
                                   f"alpha = {self.alpha}, nH = {self.nH}, " \
                                   f"K50 = {self.K50}, {K50_het_str}, " \
                                   f"Catalyst: {self.catalyst}, " \
                                   f"Km = {self.Km}, {Km_het_str}"

    def __repr__(self):
        return f"RegulatedMichaelisMentenProcess Object: " \
               f"RegulatedMichaelisMentenProcess.from_string('{self._str.split(',')[0]}', " \
               f"k={self.k}, " \
               f"regulating_species='{self.regulating_species}', " \
               f"alpha={self.alpha}, " \
               f"K50={self.K50}, " \
               f"nH={self.nH}, " \
               f"catalyst={self.catalyst}, " \
               f"Km={self.Km})"

    def __eq__(self, other):
        if isinstance(other, RegulatedMichaelisMentenProcess):
            is_equal = (self.k == other.k and
                        self.order == other.order and
                        self.reactants == other.reactants and
                        self.products == other.products and
                        self.regulating_species == other.regulating_species and
                        self.alpha == other.alpha and
                        self.K50 == other.K50 and
                        self.nH == other.nH and
                        self.catalyst == other.catalyst and
                        self.Km == other.Km and
                        self.species == other.species)
            return is_equal
        elif isinstance(other, str):
            return self._str == other or self._str.replace(' ', '') == other
        else:
            print(f"{type(self)} and {type(other)} are instances of different classes.")
            return False

    def __hash__(self):
        return hash(self._str)


class NullSpeciesNameError(Exception):
    """ Error when the species name is an empty string. """

    def __str__(self):
        return "A species name cannot be an empty string."


def update_all_species(procs: tuple[Process, ...]) -> tuple[set, dict, dict]:
    """ Categorize all species in a list of processes.

    Extract all species from a list of processes. Then categorize each of them
    as a reactant or product and list the process(es) it takes part in.

    Parameters
    ----------
    procs : tuple
        A tuple of objects of type `Process` or its subclasses.

    Returns
    -------
    tuple
        all_species : set of strings
            A set of all species present in the processes.
        procs_by_reactant : dict
            A dictionary whose keys are the species that are
            reactants in one or more processes. The value for each
            key is a list of processes.
        procs_by_product : dict
            A dictionary whose keys are the species that are
            products in one or more processes. The value for each
            key is a list of processes.
    """
    procs = list(procs)
    for proc in procs:
        # For a reversible process, replace it with separate instances
        # of Process objects representing the forward and reverse reactions.
        if isinstance(proc, ReversibleProcess):
            forward_proc = Process(proc.reactants, proc.products, proc.k)
            reverse_proc = Process(proc.products, proc.reactants, proc.k_rev)
            procs.remove(proc)
            procs.extend([forward_proc, reverse_proc])

    assert len(set(procs)) == len(procs), \
        "WARNING: Duplicate processes found. Examine the list of processes to resolve this."

    all_species, rspecies, pspecies = set(), set(), set()
    procs_by_reactant, procs_by_product = dict(), dict()

    for proc in procs:
        all_species = all_species.union(proc.species)
        rspecies = rspecies.union(proc.reactants)
        pspecies = pspecies.union(proc.products)

    # Make a list containing the processes each reactant species takes part in.
    # This will be used when solving the system ODEs.
    for rspec in rspecies:
        if rspec != '':  # omit reactant species parsed from 0th order processes
            procs_by_reactant[rspec] = [proc for proc in procs if rspec in proc.reactants]
            # deleted 1st clause in above `if`: `rspec != '' and`

    # Make a list containing the processes each product species takes part in.
    # This will be used for solving the system ODEs.
    for pspec in pspecies:
        if pspec != '':  # omitting product species parsed from degradation processes
            procs_by_product[pspec] = [proc for proc in procs if pspec in proc.products]

    return all_species, procs_by_reactant, procs_by_product

Functions

def update_all_species(procs: tuple[Process, ...]) ‑> tuple[set, dict, dict]

Categorize all species in a list of processes.

Extract all species from a list of processes. Then categorize each of them as a reactant or product and list the process(es) it takes part in.

Parameters

procs : tuple
A tuple of objects of type Process or its subclasses.

Returns

tuple
all_species : set of strings A set of all species present in the processes. procs_by_reactant : dict A dictionary whose keys are the species that are reactants in one or more processes. The value for each key is a list of processes. procs_by_product : dict A dictionary whose keys are the species that are products in one or more processes. The value for each key is a list of processes.
Expand source code
def update_all_species(procs: tuple[Process, ...]) -> tuple[set, dict, dict]:
    """ Categorize all species in a list of processes.

    Extract all species from a list of processes. Then categorize each of them
    as a reactant or product and list the process(es) it takes part in.

    Parameters
    ----------
    procs : tuple
        A tuple of objects of type `Process` or its subclasses.

    Returns
    -------
    tuple
        all_species : set of strings
            A set of all species present in the processes.
        procs_by_reactant : dict
            A dictionary whose keys are the species that are
            reactants in one or more processes. The value for each
            key is a list of processes.
        procs_by_product : dict
            A dictionary whose keys are the species that are
            products in one or more processes. The value for each
            key is a list of processes.
    """
    procs = list(procs)
    for proc in procs:
        # For a reversible process, replace it with separate instances
        # of Process objects representing the forward and reverse reactions.
        if isinstance(proc, ReversibleProcess):
            forward_proc = Process(proc.reactants, proc.products, proc.k)
            reverse_proc = Process(proc.products, proc.reactants, proc.k_rev)
            procs.remove(proc)
            procs.extend([forward_proc, reverse_proc])

    assert len(set(procs)) == len(procs), \
        "WARNING: Duplicate processes found. Examine the list of processes to resolve this."

    all_species, rspecies, pspecies = set(), set(), set()
    procs_by_reactant, procs_by_product = dict(), dict()

    for proc in procs:
        all_species = all_species.union(proc.species)
        rspecies = rspecies.union(proc.reactants)
        pspecies = pspecies.union(proc.products)

    # Make a list containing the processes each reactant species takes part in.
    # This will be used when solving the system ODEs.
    for rspec in rspecies:
        if rspec != '':  # omit reactant species parsed from 0th order processes
            procs_by_reactant[rspec] = [proc for proc in procs if rspec in proc.reactants]
            # deleted 1st clause in above `if`: `rspec != '' and`

    # Make a list containing the processes each product species takes part in.
    # This will be used for solving the system ODEs.
    for pspec in pspecies:
        if pspec != '':  # omitting product species parsed from degradation processes
            procs_by_product[pspec] = [proc for proc in procs if pspec in proc.products]

    return all_species, procs_by_reactant, procs_by_product

Classes

class MichaelisMentenProcess (reactants: dict, products: dict, k: float | int | list[float | int, ...] | tuple[float | int, float | int], catalyst: str, Km: float | int | list[float | int, ...] | tuple[float | int, float | int])

Define a process that obeys Michaelis-Menten kinetics.

Attributes

catalyst : str
Name of the species acting as a catalyst for this process.
Km : float or int or list of floats or 2-tuple of floats
Microscopic Michaelis constant. Corresponds to the number of catalyst agents that would produce half-maximal activity. Heterogeneity in this parameter is determined by the type of K50, using the same rules as for parameter k.
is_heterogeneous_Km : bool
Denotes if the parameter Km exhibits heterogeneity (distinct subspecies/interactions or normally-distributed).
Expand source code
class MichaelisMentenProcess(Process):
    """ Define a process that obeys Michaelis-Menten kinetics.

    Attributes
    ----------
    catalyst : str
        Name of the species acting as a catalyst for this process.
    Km : float or int or list of floats or 2-tuple of floats
        *Microscopic* Michaelis constant. Corresponds to the number
        of `catalyst` agents that would produce half-maximal activity.
        Heterogeneity in this parameter is determined by the type of `K50`,
        using the same rules as for parameter `k`.
    is_heterogeneous_Km : bool
        Denotes if the parameter `Km` exhibits heterogeneity
        (distinct subspecies/interactions or normally-distributed).
    """

    def __init__(self, reactants: dict, products: dict,
                 k: float | int | list[float | int, ...] | tuple[float | int, float | int],
                 catalyst: str,
                 Km: float | int | list[float | int, ...] | tuple[float | int, float | int]):
        self.catalyst = catalyst
        self.Km = Km
        self.is_heterogeneous_Km = False if isinstance(self.Km, (int, float)) else True

        super().__init__(reactants, products, k)

        self.species.add(self.catalyst)
        self._str += f", catalyst = {self.catalyst}, Km = {self.Km}"

        assert self.order != 0, "A 0th order process has no substrate for a catalyst " \
                                "to act on, therefore it cannot follow Michaelis-Menten kinetics."
        if self.order == 2:
            raise NotImplementedError

    @classmethod
    def from_string(cls,
                    proc_str: str,
                    /,
                    k: float | int | list[float | int, ...] | tuple[float | int, float | int],
                    *,
                    catalyst: str = None,
                    Km: float | int | list[float | int, ...] | tuple[
                        float | int, float | int] = None,
                    sep: str = '->') -> Self:
        """ Create a Michaelis-Menten process from a string.

        Parameters
        ----------
        proc_str : str
            A string describing the process in standard chemical notation
            (e.g., 'A + B -> C')
        k : float or int or list of floats or 2-tuple of floats
            The *microscopic* rate constant for the given process. If `k` is a
            float or int, then the process is homogeneous. If `k` is a list, then
            the population of the reactants constsists of distinct subspecies
            or subinteractions depending on the order. If `k` is a 2-tuple,
            then the constant is normally-distributed with a mean and standard
            deviation specified in the tuple's elements.
        catalyst : str
            Name of species acting as a catalyst.
        Km : float or int or list of floats or 2-tuple of floats
            *Microscopic* Michaelis constant for the process.
            Heterogeneity in this parameter is determined by the type of `Km`,
            using the same rules as for parameter `k`.
        sep : str, default: '->'
            Specifies the characters that distinguish the reactants from the
            products. The default is '->'. The code also treats `-->` as a
            default, if it's present in `proc_str`.

        Notes
        -----
        - Species names should not contain spaces, dashes, and
          should start with a non-numeric character.
        - Zeroth order processes should be specified by an empty space or 'None'.

        Examples
        --------
        >>> MichaelisMentenProcess.from_string("A -> X", k=0.3, catalyst='E', Km=10)
        >>> MichaelisMentenProcess.from_string("A -> X", k=0.3, catalyst='alpha', Km=(10, 1))
        """
        sep = '-->' if '-->' in proc_str else sep
        if sep not in proc_str:
            raise Exception("Cannot distinguish the reactants from the products.\n"
                            "Please use the *sep* keyword: e.g. sep='->'.")

        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side

        return cls(cls._to_dict(lhs_terms), cls._to_dict(rhs_terms),
                   k,
                   catalyst, Km)

    def __str__(self):
        if isinstance(self.Km, (float, int)):
            Km_het_str = "Homogeneous process with respect to Km."
        elif isinstance(self.k, list):
            Km_het_str = f"Heterogeneous process with respect to Km " \
                         f"with {len(self.Km)} distinct subspecies."
        else:
            Km_het_str = f"Heterogeneous process with normally-distributed Km with " \
                         f"mean {self.Km[0]} and standard deviation {self.Km[1]}."

        return super().__str__() + f" Catalyst: {self.catalyst}, " \
                                   f"Km = {self.Km}, {Km_het_str}"

    def __repr__(self):
        return f"MichaelisMentenProcess Object: " \
               f"MichaelisMentenProcess.from_string('{self._str.split(',')[0]}', " \
               f"k={self.k}, " \
               f"catalyst='{self.catalyst}', " \
               f"Km={self.Km})"

    def __eq__(self, other):
        if isinstance(other, MichaelisMentenProcess):
            is_equal = (self.k == other.k and
                        self.order == other.order and
                        self.reactants == other.reactants and
                        self.products == other.products and
                        self.catalyst == other.catalyst and
                        self.Km == other.Km and
                        self.species == other.species)
            return is_equal
        elif isinstance(other, str):
            return self._str == other or self._str.replace(' ', '') == other
        else:
            print(f"{type(self)} and {type(other)} are instances of different classes.")
            return False

    def __hash__(self):
        return hash(self._str)

Ancestors

Static methods

def from_string(proc_str: str, /, k: float | int | list[float | int, ...] | tuple[float | int, float | int], *, catalyst: str = None, Km: float | int | list[float | int, ...] | tuple[float | int, float | int] = None, sep: str = '->') ‑> Self

Create a Michaelis-Menten process from a string.

Parameters

proc_str : str
A string describing the process in standard chemical notation (e.g., 'A + B -> C')
k : float or int or list of floats or 2-tuple of floats
The microscopic rate constant for the given process. If k is a float or int, then the process is homogeneous. If k is a list, then the population of the reactants constsists of distinct subspecies or subinteractions depending on the order. If k is a 2-tuple, then the constant is normally-distributed with a mean and standard deviation specified in the tuple's elements.
catalyst : str
Name of species acting as a catalyst.
Km : float or int or list of floats or 2-tuple of floats
Microscopic Michaelis constant for the process. Heterogeneity in this parameter is determined by the type of Km, using the same rules as for parameter k.
sep : str, default: '->'
Specifies the characters that distinguish the reactants from the products. The default is '->'. The code also treats --> as a default, if it's present in proc_str.

Notes

  • Species names should not contain spaces, dashes, and should start with a non-numeric character.
  • Zeroth order processes should be specified by an empty space or 'None'.

Examples

>>> MichaelisMentenProcess.from_string("A -> X", k=0.3, catalyst='E', Km=10)
>>> MichaelisMentenProcess.from_string("A -> X", k=0.3, catalyst='alpha', Km=(10, 1))
Expand source code
@classmethod
def from_string(cls,
                proc_str: str,
                /,
                k: float | int | list[float | int, ...] | tuple[float | int, float | int],
                *,
                catalyst: str = None,
                Km: float | int | list[float | int, ...] | tuple[
                    float | int, float | int] = None,
                sep: str = '->') -> Self:
    """ Create a Michaelis-Menten process from a string.

    Parameters
    ----------
    proc_str : str
        A string describing the process in standard chemical notation
        (e.g., 'A + B -> C')
    k : float or int or list of floats or 2-tuple of floats
        The *microscopic* rate constant for the given process. If `k` is a
        float or int, then the process is homogeneous. If `k` is a list, then
        the population of the reactants constsists of distinct subspecies
        or subinteractions depending on the order. If `k` is a 2-tuple,
        then the constant is normally-distributed with a mean and standard
        deviation specified in the tuple's elements.
    catalyst : str
        Name of species acting as a catalyst.
    Km : float or int or list of floats or 2-tuple of floats
        *Microscopic* Michaelis constant for the process.
        Heterogeneity in this parameter is determined by the type of `Km`,
        using the same rules as for parameter `k`.
    sep : str, default: '->'
        Specifies the characters that distinguish the reactants from the
        products. The default is '->'. The code also treats `-->` as a
        default, if it's present in `proc_str`.

    Notes
    -----
    - Species names should not contain spaces, dashes, and
      should start with a non-numeric character.
    - Zeroth order processes should be specified by an empty space or 'None'.

    Examples
    --------
    >>> MichaelisMentenProcess.from_string("A -> X", k=0.3, catalyst='E', Km=10)
    >>> MichaelisMentenProcess.from_string("A -> X", k=0.3, catalyst='alpha', Km=(10, 1))
    """
    sep = '-->' if '-->' in proc_str else sep
    if sep not in proc_str:
        raise Exception("Cannot distinguish the reactants from the products.\n"
                        "Please use the *sep* keyword: e.g. sep='->'.")

    lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
    lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
    rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side

    return cls(cls._to_dict(lhs_terms), cls._to_dict(rhs_terms),
               k,
               catalyst, Km)
class NullSpeciesNameError (*args, **kwargs)

Error when the species name is an empty string.

Expand source code
class NullSpeciesNameError(Exception):
    """ Error when the species name is an empty string. """

    def __str__(self):
        return "A species name cannot be an empty string."

Ancestors

  • builtins.Exception
  • builtins.BaseException
class Process (reactants: dict, products: dict, k: float | int | list[float, ...] | tuple[float, float], **kwargs)

Define a unidirectional process: Reactants -> Products, where the Reactants and Products are specified using standard chemical notation. That is, stoichiometric coefficients (integers) and species names are specified. For example: 2A + B -> C.

Attributes

reactants : dict
The reactants of a given process are specified with key-value pairs describing each species name and its stoichiometric coefficient, respectively.
products : dict
The products of a given process are specified with key-value pairs describing each species name and its stoichiometric coefficient, respectively.
k : float, int, list of floats, tuple of floats
The microscopic rate constant(s) for the given process. The data type of k determines the "structure" of the population as follows: - A homogeneous population: if k is a single value (float or int), then the population is assumed to be homogeneous with all agents of the reactant species having kinetics defined by this value. - A heterogeneous population with a distinct number of subspecies (each with a corresponding k value): if k is a list of floats, then the population is assumed to be heterogeneous with a number of subspecies equal to the length of the list. - A heterogeneous population with normally-distributed k values: If k is a tuple whose length is 2, then the population is assumed to be heterogeneous with a normally distributed k value. The two entries in the tuple represent the mean and standard deviation (in that order) of the desired normal distribution.
order : int
The order of the process (or the molecularity of an elementary process). It is the sum of the stoichiometric coefficients of the reactants.
species : set of strings
A set of all species in a process.
reacts_ : list of strings
A list containing all the reactants in a process.
prods_ : list of strings
A list containing all the products in a process.

Methods

from_string Class method for creating a Process object from a string.

Expand source code
class Process:
    """
    Define a unidirectional process: Reactants -> Products, where the
    Reactants and Products are specified using standard chemical notation.
    That is, stoichiometric coefficients (integers) and species names are
    specified. For example: 2A + B -> C.

    Attributes
    ----------
    reactants : dict
        The reactants of a given process are specified with
        key-value pairs describing each species name and its
        stoichiometric coefficient, respectively.
    products : dict
        The products of a given process are specified with
        key-value pairs describing each species name and its
        stoichiometric coefficient, respectively.
    k : float, int, list of floats, tuple of floats
        The *microscopic* rate constant(s) for the given process. The data
        type of `k` determines the "structure" of the population as follows:
            - A homogeneous population: if `k` is a single value (float or int),
              then the population is assumed to be homogeneous with all agents
              of the reactant species having kinetics defined by this value.
            - A heterogeneous population with a distinct number of subspecies
              (each with a corresponding `k` value): if `k` is a list of floats,
              then the population is assumed to be heterogeneous with a number
              of subspecies equal to the length of the list.
            - A heterogeneous population with normally-distributed `k` values:
              If `k` is a tuple whose length is 2, then the population is
              assumed to be heterogeneous with a normally distributed `k` value.
              The two entries in the tuple represent the mean and standard
              deviation (in that order) of the desired normal distribution.
    order : int
        The order of the process (or the molecularity of an elementary process).
        It is the sum of the stoichiometric coefficients of the reactants.
    species : set of strings
        A set of all species in a process.
    reacts_ : list of strings
        A list containing all the reactants in a process.
    prods_ : list of strings
        A list containing all the products in a process.

    Methods
    -------
    from_string
        Class method for creating a Process object from a string.
    """

    def __init__(self,
                 reactants: dict,
                 products: dict,
                 k: float | int | list[float, ...] | tuple[float, float],
                 **kwargs):

        self.reactants = reactants
        self.products = products
        self.k = k

        self._validate_nums()  # make sure there are no errors in given numbers

        # For consistency with processes instantiated using the class method `from_string()`,
        # species denoted as 'None' for a 0th order process are renamed to ''.
        if 'None' in self.reactants.keys():
            self.reactants[''] = self.reactants.pop('None')
        if 'None' in self.products.keys():
            self.products[''] = self.products.pop('None')

        self.order = sum(self.reactants.values())

        self.is_heterogeneous = False if isinstance(self.k, (int, float)) else True

        if self.order == 0:
            msg = "Since a birth process does not depended on the presence of agents, " \
                  "heterogeneity does not make sense in this context. Please define " \
                  "the rate constant k as a number. "
            assert not self.is_heterogeneous, msg

        # Two ways of storing the involved species:
        # 1) A set of all species
        self.species = set((self.reactants | self.products).keys())
        with contextlib.suppress(KeyError):
            self.species.remove('')  # remove empty species name from any 0th order processes

        # 2) Separate lists
        self.reacts_ = list()  # [reactant species]
        self.prods_ = list()  # [product species]
        self._get_reacts_prods_()

        """ Because Process objects are used as keys in dictionaries used 
        in an AbStochKin simulation, it's much faster to generate the object's 
        string representation once, and then access it whenever it's needed 
        (which could be thousands of times during a simulation). """
        self._str = self.__str__().split(';')[0]

        if len(kwargs) > 0:
            self._lsp(kwargs)

    def _get_reacts_prods_(self):
        """ Make lists of the reactant and product species. Repeated
        elements of a list reflect the order or molecularity of the
        species in the given process. For example, for the process
        `2A + B -> C + D, reacts_ = ['A', 'A', 'B'], prods_ = ['C', 'D']`. """
        for r, m in self.reactants.items():
            for i in range(m):
                self.reacts_.append(r)

        for p, m in self.products.items():
            for i in range(m):
                self.prods_.append(p)

        if '' in self.reacts_:  # remove empty reactant species names
            self.reacts_.remove('')  # from 0th order processes
        if '' in self.prods_:  # remove empty product species names
            self.prods_.remove('')  # from degradation processes

    @classmethod
    def from_string(cls,
                    proc_str: str,
                    /,
                    k: float | int | list[float, ...] | tuple[float, float],
                    *,
                    sep: str = '->',
                    **kwargs) -> Self:
        """ Create a process from a string.

        Parameters
        ----------
        proc_str : str
            A string describing the process in standard chemical notation
            (e.g., 'A + B -> C')
        k : float or int or list of floats or 2-tuple of floats
            The rate constant for the given process. If `k` is a float or
            int, then the process is homogeneous. If `k` is a list, then
            the population of the reactants constsists of distinct subspecies
            or subinteractions depending on the order. If `k` is a 2-tuple,
            then the constant is normally-distributed with a mean and standard
            deviation specified in the tuple's elements.
        sep : str, default: '->'
            Specifies the characters that distinguish the reactants from the
            products. The default is '->'. The code also treats `-->` as a
            default, if it's present in `proc_str`.

        Notes
        -----
        - Species names should not contain spaces, dashes, and
          should start with a non-numeric character.
        - Zeroth order processes should be specified by an empty space or 'None'.

        Examples
        --------
        >>> Process.from_string("2A + B -> X", 0.3)
        >>> Process.from_string(" -> Y", 0.1)  # for a 0th order (birth) process.
        >>> Process.from_string("Protein_X -> None", 0.15)  # for a 1st order degradation process.
        """

        if len(kwargs) > 0:
            cls._lsp(kwargs)

        sep = '-->' if '-->' in proc_str else sep
        if sep not in proc_str:
            raise Exception("Cannot distinguish the reactants from the products.\n"
                            "Please use the *sep* keyword: e.g. sep='->'.")

        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side

        return cls(cls._to_dict(lhs_terms), cls._to_dict(rhs_terms), k)

    @staticmethod
    def _lsp(kwargs: dict):
        """
        The `Process` class accepts additional arguments (`**kwargs`).
        Since the Process class is a base class for other subclasses,
        this is done so that the Liskov Substitution Principle (LSP)
        is not violated.
        (https://en.wikipedia.org/wiki/Liskov_substitution_principle).
        This way, subclasses override the `from_string` method and have
        additional parameters while remaining consistent with this method
        from their base class. Calling the base instance with the additional
        parameters gives a warning that they will have no effect so that
        the user can intervene, if that's desired.
        """
        msg = f"Warning: Additional parameters {','.join([str(i) for i in kwargs.items()])} " \
              f"will have no effect. "
        if 'k_rev' in kwargs.keys():
            msg += f"If that's not what you intended, define the process " \
                   f"using ReversibleProcess()."
        if 'regulating_species' in kwargs.keys() or 'alpha' in kwargs.keys() or \
                'nH' in kwargs.keys() or 'K50' in kwargs.keys():
            msg += f"If that's not what you intended, define the process " \
                   f"using RegulatedProcess()."
        if 'catalyst' in kwargs.keys():
            msg += f"If that's not what you intended, define the process " \
                   f"using MichaelisMentenProcess()."
        print(msg)

    @staticmethod
    def _to_dict(terms: list) -> dict:
        """ Convert the information for a side of a process to a dictionary. """

        side_terms = dict()  # for storing the information of a side of a process
        patt = '^[\\-]*[1-9]+'  # regex pattern (accounts for leading erroneous minus sign)

        if len(terms) == 1 and terms[0].strip().lower() in ['', 'none']:
            spec = ''  # Zeroth order process
            side_terms[spec] = 0
        else:
            for term in terms:
                term = term.strip()
                try:
                    match = re.search(patt, term)
                    stoic_coef = term[slice(*match.span())]  # extract stoichiometric coef
                    spec = re.split(patt, term)[-1].strip()  # extract species name
                    if spec == '' and stoic_coef != 0:
                        raise NullSpeciesNameError()
                    stoic_coef = int(stoic_coef)
                except AttributeError:  # when there is no specified stoichiometric coefficient
                    spec = re.split(patt, term)[-1]  # extract species name
                    stoic_coef = 1

                if spec not in side_terms.keys():
                    side_terms[spec] = stoic_coef
                else:
                    side_terms[spec] += stoic_coef

        return side_terms

    def _validate_nums(self):
        """ Make sure coefficients and rate constant values are not negative. """
        for r, val in (self.reactants | self.products).items():
            assert val >= 0, f"Coefficient cannot be negative: {val} {r}."

        error_msg = f"Rate constant values have to be positive: k = {self.k}."
        if isinstance(self.k, (list, tuple)):  # heterogeneous population
            assert all(array(self.k) > 0), error_msg
        else:  # when k is a float or int, the population is homogeneous
            assert self.k > 0, error_msg

        # For normally-distributed k values, specification is a 2-tuple.
        if isinstance(self.k, tuple):  # normal distribution of k values
            assert len(self.k) == 2, "Please specify the mean and standard deviation " \
                                     "of k in a 2-tuple: (mean, std)."

    def __eq__(self, other):
        if isinstance(other, Process):
            is_equal = (self.k == other.k and
                        self.order == other.order and
                        self.reactants == other.reactants and
                        self.products == other.products and
                        self.species == other.species)
            return is_equal
        elif isinstance(other, str):
            return self._str == other or self._str.replace(' ', '') == other
        else:
            print(f"{type(self)} and {type(other)} are instances of different classes.")
            return False

    def __hash__(self):
        return hash(self._str)

    def __contains__(self, item):
        return True if item in self.species else False

    def __repr__(self):
        return f"Process Object: Process.from_string('{self._str.split(',')[0]}', k={self.k})"

    def __str__(self):
        if isinstance(self.k, (float, int)):
            het_str = "Homogeneous process."
        elif isinstance(self.k, list):
            het_str = f"Heterogeneous process with {len(self.k)} distinct subspecies."
        else:
            het_str = f"Heterogeneous process with normally-distributed k with " \
                      f"mean {self.k[0]} and standard deviation {self.k[1]}."

        lhs, rhs = self._reconstruct_string()
        return ' -> '.join([lhs, rhs]) + f', k = {self.k}; {het_str}'

    def _reconstruct_string(self):
        lhs = ' + '.join([f"{str(val) + ' ' if val not in [0, 1] else ''}{key}" for key, val in
                          self.reactants.items()])
        rhs = ' + '.join([f"{str(val) + ' ' if val not in [0, 1] else ''}{key}" for key, val in
                          self.products.items()])
        return lhs, rhs

Subclasses

Static methods

def from_string(proc_str: str, /, k: float | int | list[float, ...] | tuple[float, float], *, sep: str = '->', **kwargs) ‑> Self

Create a process from a string.

Parameters

proc_str : str
A string describing the process in standard chemical notation (e.g., 'A + B -> C')
k : float or int or list of floats or 2-tuple of floats
The rate constant for the given process. If k is a float or int, then the process is homogeneous. If k is a list, then the population of the reactants constsists of distinct subspecies or subinteractions depending on the order. If k is a 2-tuple, then the constant is normally-distributed with a mean and standard deviation specified in the tuple's elements.
sep : str, default: '->'
Specifies the characters that distinguish the reactants from the products. The default is '->'. The code also treats --> as a default, if it's present in proc_str.

Notes

  • Species names should not contain spaces, dashes, and should start with a non-numeric character.
  • Zeroth order processes should be specified by an empty space or 'None'.

Examples

>>> Process.from_string("2A + B -> X", 0.3)
>>> Process.from_string(" -> Y", 0.1)  # for a 0th order (birth) process.
>>> Process.from_string("Protein_X -> None", 0.15)  # for a 1st order degradation process.
Expand source code
@classmethod
def from_string(cls,
                proc_str: str,
                /,
                k: float | int | list[float, ...] | tuple[float, float],
                *,
                sep: str = '->',
                **kwargs) -> Self:
    """ Create a process from a string.

    Parameters
    ----------
    proc_str : str
        A string describing the process in standard chemical notation
        (e.g., 'A + B -> C')
    k : float or int or list of floats or 2-tuple of floats
        The rate constant for the given process. If `k` is a float or
        int, then the process is homogeneous. If `k` is a list, then
        the population of the reactants constsists of distinct subspecies
        or subinteractions depending on the order. If `k` is a 2-tuple,
        then the constant is normally-distributed with a mean and standard
        deviation specified in the tuple's elements.
    sep : str, default: '->'
        Specifies the characters that distinguish the reactants from the
        products. The default is '->'. The code also treats `-->` as a
        default, if it's present in `proc_str`.

    Notes
    -----
    - Species names should not contain spaces, dashes, and
      should start with a non-numeric character.
    - Zeroth order processes should be specified by an empty space or 'None'.

    Examples
    --------
    >>> Process.from_string("2A + B -> X", 0.3)
    >>> Process.from_string(" -> Y", 0.1)  # for a 0th order (birth) process.
    >>> Process.from_string("Protein_X -> None", 0.15)  # for a 1st order degradation process.
    """

    if len(kwargs) > 0:
        cls._lsp(kwargs)

    sep = '-->' if '-->' in proc_str else sep
    if sep not in proc_str:
        raise Exception("Cannot distinguish the reactants from the products.\n"
                        "Please use the *sep* keyword: e.g. sep='->'.")

    lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
    lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
    rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side

    return cls(cls._to_dict(lhs_terms), cls._to_dict(rhs_terms), k)
class RegulatedMichaelisMentenProcess (reactants: dict, products: dict, k: float | int | list[float, ...] | tuple[float, float], regulating_species: str | list[str, ...], alpha: float | int | list[float | int, ...], K50: float | int | list[float | int, ...] | tuple[float | int, float | int] | list[float | int | list[float | int, ...] | tuple[float | int, float | int]], nH: float | int | list[float | int, ...], catalyst: str, Km: float | int | list[float | int, ...] | tuple[float | int, float | int])

Define a process that is regulated and obeys Michaelis-Menten kinetics.

This class allows a Michaelis-Menten Process to be defined in terms of how it is regulated. If there is only one regulating species, then the parameters have the same type as would be expected for a homogeneous/heterogeneous process. If there are multiple regulating species, then all parameters are a list of their expected type, with the length of the list being equal to the number of regulating species.

Attributes

k : float or int or list of floats or 2-tuple of floats
The microscopic rate constant for the given process. It is the basal rate constant in the case of activation (or the minimum k value) and the maximum rate constant in the case of repression.
regulating_species : str or list of str
Name of the regulating species. Multiple species can be specified as comma-separated in a string or a list of strings with the species names.
alpha : float or int or list[float or int]

Parameter denoting the degree of activation/repression.

- 0 <= alpha < 1: repression
- alpha = 1: no regulation
- alpha > 1: activation

alpha is a multiplier: in the case of activation, the maximum rate constant value will be alpha * k. In the case of repression, the minimum rate constant value will be alpha * k.

K50 : float or int or list of floats or 2-tuple of floats or list[float or int or list of floats or 2-tuple of floats]
Microscopic constant that corresponds to the number of regulating_species agents that would produce half-maximal activation/repression. Heterogeneity in this parameter is determined by the type of K50, using the same rules as for parameter k.
nH : float or int or list[float or int]
Hill coefficient for the given process. Indicates the degree of cooperativity in the regulatory interaction.
is_heterogeneous_K50 : bool or list of bool
Denotes if the parameter K50 exhibits heterogeneity (distinct subspecies/interactions or normally-distributed).
regulation_type : str or list of str
The type of regulation for this process based on the value of alpha: 'activation' or 'repression' or 'no regulation'.
catalyst : str
Name of the species acting as a catalyst for this process.
Km : float or int or list of floats or 2-tuple of floats
Microscopic Michaelis constant. Corresponds to the number of catalyst agents that would produce half-maximal activity. Heterogeneity in this parameter is determined by the type of K50, using the same rules as for parameter k.
is_heterogeneous_Km : bool
Denotes if the parameter Km exhibits heterogeneity (distinct subspecies/interactions or normally-distributed).

Notes

Currently only implemented for 1st order processes. 0th order processes cannot obey Michaelis-Menten kinetics and 2nd order Michaelis-Menten processes are not implemented yet.

Expand source code
class RegulatedMichaelisMentenProcess(RegulatedProcess):
    """ Define a process that is regulated and obeys Michaelis-Menten kinetics.

    This class allows a Michaelis-Menten Process to be defined
    in terms of how it is regulated.
    If there is only one regulating species, then the parameters have the same
    type as would be expected for a homogeneous/heterogeneous process.
    If there are multiple regulating species, then all parameters are a list
    of their expected type, with the length of the list being equal to the
    number of regulating species.

    Attributes
    ----------
    k : float or int or list of floats or 2-tuple of floats
        The *microscopic* rate constant for the given process. It is the *basal*
        rate constant in the case of activation (or the minimum `k` value)
        and the maximum rate constant in the case of repression.
    regulating_species : str or list of str
        Name of the regulating species. Multiple species can be specified as
        comma-separated in a string or a list of strings with the species names.
    alpha : float or int or list[float or int]
        Parameter denoting the degree of activation/repression.

            - 0 <= alpha < 1: repression
            - alpha = 1: no regulation
            - alpha > 1: activation

        alpha is a multiplier: in the case of activation, the maximum
        rate constant value will be `alpha * k`.
        In the case of repression, the minimum
        rate constant value will be `alpha * k`.
    K50 : float or int or list of floats or 2-tuple of floats or list[float or int or list of floats or 2-tuple of floats]
        *Microscopic* constant that corresponds to the number of
        `regulating_species` agents that would produce
        half-maximal activation/repression.
        Heterogeneity in this parameter is determined by the type of `K50`,
        using the same rules as for parameter `k`.
    nH : float or int or list[float or int]
        Hill coefficient for the given process. Indicates the degree of
        cooperativity in the regulatory interaction.
    is_heterogeneous_K50 : bool or list of bool
        Denotes if the parameter `K50` exhibits heterogeneity
        (distinct subspecies/interactions or normally-distributed).
    regulation_type : str or list of str
        The type of regulation for this process based on the value of alpha:
        'activation' or 'repression' or 'no regulation'.
    catalyst : str
        Name of the species acting as a catalyst for this process.
    Km : float or int or list of floats or 2-tuple of floats
        *Microscopic* Michaelis constant. Corresponds to the number
        of `catalyst` agents that would produce half-maximal activity.
        Heterogeneity in this parameter is determined by the type of `K50`,
        using the same rules as for parameter `k`.
    is_heterogeneous_Km : bool
        Denotes if the parameter `Km` exhibits heterogeneity
        (distinct subspecies/interactions or normally-distributed).

    Notes
    -----
    Currently only implemented for 1st order processes. 0th order processes
    cannot obey Michaelis-Menten kinetics and 2nd order Michaelis-Menten
    processes are not implemented yet.
    """

    def __init__(self, reactants: dict, products: dict,
                 k: float | int | list[float, ...] | tuple[float, float],
                 regulating_species: str | list[str, ...],
                 alpha: float | int | list[float | int, ...],
                 K50: float | int | list[float | int, ...] | tuple[float | int, float | int] |
                      list[float | int | list[float | int, ...] | tuple[float | int, float | int]],
                 nH: float | int | list[float | int, ...],
                 catalyst: str,
                 Km: float | int | list[float | int, ...] | tuple[float | int, float | int]):
        self.catalyst = catalyst
        self.Km = Km
        self.is_heterogeneous_Km = False if isinstance(self.Km, (int, float)) else True

        super().__init__(reactants, products, k, regulating_species, alpha, K50, nH)

        self.species.add(self.catalyst)
        self._str += f", catalyst = {self.catalyst}, Km = {self.Km}"

        assert self.order != 0, "A 0th order process has no substrate for a catalyst " \
                                "to act on, therefore it cannot follow Michaelis-Menten kinetics."
        if self.order == 2:
            raise NotImplementedError

    @classmethod
    def from_string(cls,
                    proc_str: str,
                    /,
                    k: float | int | list[float, ...] | tuple[float, float],
                    *,
                    regulating_species: str | list[str, ...] = None,
                    alpha: float | int | list[float | int, ...] = 1,
                    K50: float | int | list[float | int, ...] | tuple[float | int, float | int] |
                         list[float | int | list[float | int, ...] | tuple[
                             float | int, float | int]] = None,
                    nH: float | int | list[float | int, ...] = None,
                    catalyst: str = None,
                    Km: float | int | list[float | int, ...] | tuple[
                        float | int, float | int] = None,
                    sep: str = '->') -> Self:
        """ Create a regulated Michaelis-Menten process from a string.

        Parameters
        ----------
        proc_str : str
            A string describing the process in standard chemical notation
            (e.g., 'A + B -> C')
        k : float or int or list of floats or 2-tuple of floats
            The *microscopic* rate constant for the given process. It is the *basal*
            rate constant in the case of activation (or the minimum `k` value)
            and the maximum rate constant in the case of repression.
            If `k` is a float or int, then the process is homogeneous.
            If `k` is a list, then the population of the reactants
            constsists of distinct subspecies or subinteractions
            depending on the order. If `k` is a 2-tuple,
            then the constant is normally-distributed with a mean and standard
            deviation specified in the tuple's elements. Note that `k` cannot
            be zero for this form of regulation.
        regulating_species : str or list of str
            Name of the regulating species.
        alpha : float or int or list[float or int]
            Parameter denoting the degree of activation/repression.

            - 0 <= alpha < 1: repression
            - alpha = 1: no regulation
            - alpha > 1: activation

            alpha is a multiplier: in the case of activation, the maximum
            rate constant value will be `alpha * k`.
            In the case of repression, the minimum
            rate constant value will be `alpha * k`.
        K50 : float or int or list of floats or 2-tuple of floats or list of each of the previous types
            *Microscopic* constant that corresponds to the number of
            `regulating_species` agents that would produce
            half-maximal activation/repression.
            Heterogeneity in this parameter is determined by the type of `K50`,
            using the same rules as for parameter `k`.
        nH : float or int or list[float or int]
            Hill coefficient for the given process. Indicates the degree of
            cooperativity in the regulatory interaction.
        catalyst : str
            Name of species acting as a catalyst.
        Km : float or int or list of floats or 2-tuple of floats
            *Microscopic* Michaelis constant for the process.
            Heterogeneity in this parameter is determined by the type of `Km`,
            using the same rules as for parameter `k`.
        sep : str, default: '->'
            Specifies the characters that distinguish the reactants from the
            products. The default is '->'. The code also treats `-->` as a
            default, if it's present in `proc_str`.

        Notes
        -----
        - Species names should not contain spaces, dashes, and
          should start with a non-numeric character.
        - Zeroth order processes should be specified by an empty space or 'None'.

        Examples
        --------
        >>> RegulatedMichaelisMentenProcess.from_string("A -> X", k=0.2, regulating_species='X', alpha=2, K50=10, nH=1, catalyst='E', Km=15)
        >>> RegulatedMichaelisMentenProcess.from_string("A -> X", k=0.3, regulating_species='A', alpha=0.5, K50=[10, 15], nH=2, catalyst='C', Km=5)
        """
        sep = '-->' if '-->' in proc_str else sep
        if sep not in proc_str:
            raise Exception("Cannot distinguish the reactants from the products.\n"
                            "Please use the *sep* keyword: e.g. sep='->'.")

        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side

        return cls(cls._to_dict(lhs_terms), cls._to_dict(rhs_terms),
                   k,
                   regulating_species, alpha, K50, nH,
                   catalyst, Km)

    def __str__(self):
        if isinstance(self.regulating_species, list):
            K50_het_str = ""
            for i, sp in enumerate(self.regulating_species):
                if isinstance(self.K50[i], (float, int)):
                    K50_het_str += f"Homogeneous process with respect to species {sp} K50. "
                elif isinstance(self.K50[i], list):
                    K50_het_str += f"Heterogeneous process with respect to species {sp} K50 " \
                                   f"with {len(self.K50[i])} distinct subspecies. "
                else:
                    K50_het_str += f"Heterogeneous process with normally-distributed " \
                                   f"species {sp} K50 with mean {self.K50[i][0]} and " \
                                   f"standard deviation {self.K50[i][1]}. "
        else:
            if isinstance(self.K50, (float, int)):
                K50_het_str = "Homogeneous process with respect to K50."
            elif isinstance(self.K50, list):
                K50_het_str = f"Heterogeneous process with respect to K50 " \
                              f"with {len(self.K50)} distinct subspecies."
            else:
                K50_het_str = f"Heterogeneous process with normally-distributed K50 with " \
                              f"mean {self.K50[0]} and standard deviation {self.K50[1]}."

        if isinstance(self.Km, (float, int)):
            Km_het_str = "Homogeneous process with respect to Km."
        elif isinstance(self.k, list):
            Km_het_str = f"Heterogeneous process with respect to Km " \
                         f"with {len(self.Km)} distinct subspecies."
        else:
            Km_het_str = f"Heterogeneous process with normally-distributed Km with " \
                         f"mean {self.Km[0]} and standard deviation {self.Km[1]}."

        return super().__str__() + f" Regulating Species: {self.regulating_species}, " \
                                   f"alpha = {self.alpha}, nH = {self.nH}, " \
                                   f"K50 = {self.K50}, {K50_het_str}, " \
                                   f"Catalyst: {self.catalyst}, " \
                                   f"Km = {self.Km}, {Km_het_str}"

    def __repr__(self):
        return f"RegulatedMichaelisMentenProcess Object: " \
               f"RegulatedMichaelisMentenProcess.from_string('{self._str.split(',')[0]}', " \
               f"k={self.k}, " \
               f"regulating_species='{self.regulating_species}', " \
               f"alpha={self.alpha}, " \
               f"K50={self.K50}, " \
               f"nH={self.nH}, " \
               f"catalyst={self.catalyst}, " \
               f"Km={self.Km})"

    def __eq__(self, other):
        if isinstance(other, RegulatedMichaelisMentenProcess):
            is_equal = (self.k == other.k and
                        self.order == other.order and
                        self.reactants == other.reactants and
                        self.products == other.products and
                        self.regulating_species == other.regulating_species and
                        self.alpha == other.alpha and
                        self.K50 == other.K50 and
                        self.nH == other.nH and
                        self.catalyst == other.catalyst and
                        self.Km == other.Km and
                        self.species == other.species)
            return is_equal
        elif isinstance(other, str):
            return self._str == other or self._str.replace(' ', '') == other
        else:
            print(f"{type(self)} and {type(other)} are instances of different classes.")
            return False

    def __hash__(self):
        return hash(self._str)

Ancestors

Static methods

def from_string(proc_str: str, /, k: float | int | list[float, ...] | tuple[float, float], *, regulating_species: str | list[str, ...] = None, alpha: float | int | list[float | int, ...] = 1, K50: float | int | list[float | int, ...] | tuple[float | int, float | int] | list[float | int | list[float | int, ...] | tuple[float | int, float | int]] = None, nH: float | int | list[float | int, ...] = None, catalyst: str = None, Km: float | int | list[float | int, ...] | tuple[float | int, float | int] = None, sep: str = '->') ‑> Self

Create a regulated Michaelis-Menten process from a string.

Parameters

proc_str : str
A string describing the process in standard chemical notation (e.g., 'A + B -> C')
k : float or int or list of floats or 2-tuple of floats
The microscopic rate constant for the given process. It is the basal rate constant in the case of activation (or the minimum k value) and the maximum rate constant in the case of repression. If k is a float or int, then the process is homogeneous. If k is a list, then the population of the reactants constsists of distinct subspecies or subinteractions depending on the order. If k is a 2-tuple, then the constant is normally-distributed with a mean and standard deviation specified in the tuple's elements. Note that k cannot be zero for this form of regulation.
regulating_species : str or list of str
Name of the regulating species.
alpha : float or int or list[float or int]

Parameter denoting the degree of activation/repression.

  • 0 <= alpha < 1: repression
  • alpha = 1: no regulation
  • alpha > 1: activation

alpha is a multiplier: in the case of activation, the maximum rate constant value will be alpha * k. In the case of repression, the minimum rate constant value will be alpha * k.

K50 : float or int or list of floats or 2-tuple of floats or list of each of the previous types
Microscopic constant that corresponds to the number of regulating_species agents that would produce half-maximal activation/repression. Heterogeneity in this parameter is determined by the type of K50, using the same rules as for parameter k.
nH : float or int or list[float or int]
Hill coefficient for the given process. Indicates the degree of cooperativity in the regulatory interaction.
catalyst : str
Name of species acting as a catalyst.
Km : float or int or list of floats or 2-tuple of floats
Microscopic Michaelis constant for the process. Heterogeneity in this parameter is determined by the type of Km, using the same rules as for parameter k.
sep : str, default: '->'
Specifies the characters that distinguish the reactants from the products. The default is '->'. The code also treats --> as a default, if it's present in proc_str.

Notes

  • Species names should not contain spaces, dashes, and should start with a non-numeric character.
  • Zeroth order processes should be specified by an empty space or 'None'.

Examples

>>> RegulatedMichaelisMentenProcess.from_string("A -> X", k=0.2, regulating_species='X', alpha=2, K50=10, nH=1, catalyst='E', Km=15)
>>> RegulatedMichaelisMentenProcess.from_string("A -> X", k=0.3, regulating_species='A', alpha=0.5, K50=[10, 15], nH=2, catalyst='C', Km=5)
Expand source code
@classmethod
def from_string(cls,
                proc_str: str,
                /,
                k: float | int | list[float, ...] | tuple[float, float],
                *,
                regulating_species: str | list[str, ...] = None,
                alpha: float | int | list[float | int, ...] = 1,
                K50: float | int | list[float | int, ...] | tuple[float | int, float | int] |
                     list[float | int | list[float | int, ...] | tuple[
                         float | int, float | int]] = None,
                nH: float | int | list[float | int, ...] = None,
                catalyst: str = None,
                Km: float | int | list[float | int, ...] | tuple[
                    float | int, float | int] = None,
                sep: str = '->') -> Self:
    """ Create a regulated Michaelis-Menten process from a string.

    Parameters
    ----------
    proc_str : str
        A string describing the process in standard chemical notation
        (e.g., 'A + B -> C')
    k : float or int or list of floats or 2-tuple of floats
        The *microscopic* rate constant for the given process. It is the *basal*
        rate constant in the case of activation (or the minimum `k` value)
        and the maximum rate constant in the case of repression.
        If `k` is a float or int, then the process is homogeneous.
        If `k` is a list, then the population of the reactants
        constsists of distinct subspecies or subinteractions
        depending on the order. If `k` is a 2-tuple,
        then the constant is normally-distributed with a mean and standard
        deviation specified in the tuple's elements. Note that `k` cannot
        be zero for this form of regulation.
    regulating_species : str or list of str
        Name of the regulating species.
    alpha : float or int or list[float or int]
        Parameter denoting the degree of activation/repression.

        - 0 <= alpha < 1: repression
        - alpha = 1: no regulation
        - alpha > 1: activation

        alpha is a multiplier: in the case of activation, the maximum
        rate constant value will be `alpha * k`.
        In the case of repression, the minimum
        rate constant value will be `alpha * k`.
    K50 : float or int or list of floats or 2-tuple of floats or list of each of the previous types
        *Microscopic* constant that corresponds to the number of
        `regulating_species` agents that would produce
        half-maximal activation/repression.
        Heterogeneity in this parameter is determined by the type of `K50`,
        using the same rules as for parameter `k`.
    nH : float or int or list[float or int]
        Hill coefficient for the given process. Indicates the degree of
        cooperativity in the regulatory interaction.
    catalyst : str
        Name of species acting as a catalyst.
    Km : float or int or list of floats or 2-tuple of floats
        *Microscopic* Michaelis constant for the process.
        Heterogeneity in this parameter is determined by the type of `Km`,
        using the same rules as for parameter `k`.
    sep : str, default: '->'
        Specifies the characters that distinguish the reactants from the
        products. The default is '->'. The code also treats `-->` as a
        default, if it's present in `proc_str`.

    Notes
    -----
    - Species names should not contain spaces, dashes, and
      should start with a non-numeric character.
    - Zeroth order processes should be specified by an empty space or 'None'.

    Examples
    --------
    >>> RegulatedMichaelisMentenProcess.from_string("A -> X", k=0.2, regulating_species='X', alpha=2, K50=10, nH=1, catalyst='E', Km=15)
    >>> RegulatedMichaelisMentenProcess.from_string("A -> X", k=0.3, regulating_species='A', alpha=0.5, K50=[10, 15], nH=2, catalyst='C', Km=5)
    """
    sep = '-->' if '-->' in proc_str else sep
    if sep not in proc_str:
        raise Exception("Cannot distinguish the reactants from the products.\n"
                        "Please use the *sep* keyword: e.g. sep='->'.")

    lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
    lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
    rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side

    return cls(cls._to_dict(lhs_terms), cls._to_dict(rhs_terms),
               k,
               regulating_species, alpha, K50, nH,
               catalyst, Km)
class RegulatedProcess (reactants: dict, products: dict, k: float | int | list[float, ...] | tuple[float, float], regulating_species: str | list[str, ...], alpha: float | int | list[float | int, ...], K50: float | int | list[float | int, ...] | tuple[float | int, float | int] | list[float | int | list[float | int, ...] | tuple[float | int, float | int]], nH: float | int | list[float | int, ...])

Define a process that is regulated.

This class allows a Process to be defined in terms of how it is regulated. If there is only one regulating species, then the parameters have the same type as would be expected for a homogeneous/heterogeneous process. If there are multiple regulating species, then all parameters are a list of their expected type, with the length of the list being equal to the number of regulating species.

Attributes

k : float or int or list of floats or 2-tuple of floats
The microscopic rate constant for the given process. It is the basal rate constant in the case of activation (or the minimum k value) and the maximum rate constant in the case of repression.
regulating_species : str or list of str
Name of the regulating species. Multiple species can be specified as comma-separated in a string or a list of strings with the species names.
alpha : float or int or list[float or int]

Parameter denoting the degree of activation/repression.

- 0 <= alpha < 1: repression
- alpha = 1: no regulation
- alpha > 1: activation

alpha is a multiplier: in the case of activation, the maximum rate constant value will be alpha * k. In the case of repression, the minimum rate constant value will be alpha * k.

K50 : float or int or list of floats or 2-tuple of floats or list[float or int or list of floats or 2-tuple of floats]
Microscopic constant that corresponds to the number of regulating_species agents that would produce half-maximal activation/repression. Heterogeneity in this parameter is determined by the type of K50, using the same rules as for parameter k.
nH : float or int or list[float or int]
Hill coefficient for the given process. Indicates the degree of cooperativity in the regulatory interaction.
is_heterogeneous_K50 : bool or list of bool
Denotes if the parameter K50 exhibits heterogeneity (distinct subspecies/interactions or normally-distributed).
regulation_type : str or list of str
The type of regulation for this process based on the value of alpha: 'activation' or 'repression' or 'no regulation'.

Notes

Allowing a 0th order process to be regulated. However, heterogeneity in k and K50 (or any other parameters) is not allowed for such a process.

Expand source code
class RegulatedProcess(Process):
    """ Define a process that is regulated.

    This class allows a Process to be defined in terms of how it is regulated.
    If there is only one regulating species, then the parameters have the same
    type as would be expected for a homogeneous/heterogeneous process.
    If there are multiple regulating species, then all parameters are a list
    of their expected type, with the length of the list being equal to the
    number of regulating species.
    
    Attributes
    ----------
    k : float or int or list of floats or 2-tuple of floats
        The *microscopic* rate constant for the given process. It is the *basal*
        rate constant in the case of activation (or the minimum `k` value)
        and the maximum rate constant in the case of repression.
    regulating_species : str or list of str
        Name of the regulating species. Multiple species can be specified as
        comma-separated in a string or a list of strings with the species names.
    alpha : float or int or list[float or int]
        Parameter denoting the degree of activation/repression.

            - 0 <= alpha < 1: repression
            - alpha = 1: no regulation
            - alpha > 1: activation
            
        alpha is a multiplier: in the case of activation, the maximum 
        rate constant value will be `alpha * k`. 
        In the case of repression, the minimum 
        rate constant value will be `alpha * k`. 
    K50 : float or int or list of floats or 2-tuple of floats or list[float or int or list of floats or 2-tuple of floats]
        *Microscopic* constant that corresponds to the number of
        `regulating_species` agents that would produce 
        half-maximal activation/repression. 
        Heterogeneity in this parameter is determined by the type of `K50`,
        using the same rules as for parameter `k`.
    nH : float or int or list[float or int]
        Hill coefficient for the given process. Indicates the degree of 
        cooperativity in the regulatory interaction. 
    is_heterogeneous_K50 : bool or list of bool
        Denotes if the parameter `K50` exhibits heterogeneity
        (distinct subspecies/interactions or normally-distributed).
    regulation_type : str or list of str
        The type of regulation for this process based on the value of alpha:
        'activation' or 'repression' or 'no regulation'.

    Notes
    -----
    Allowing a 0th order process to be regulated. However, heterogeneity
    in `k` and `K50` (or any other parameters) is not allowed for such
    a process.
    """

    def __init__(self, reactants: dict, products: dict,
                 k: float | int | list[float, ...] | tuple[float, float],
                 regulating_species: str | list[str, ...],
                 alpha: float | int | list[float | int, ...],
                 K50: float | int | list[float | int, ...] | tuple[float | int, float | int] |
                      list[float | int | list[float | int, ...] | tuple[float | int, float | int]],
                 nH: float | int | list[float | int, ...]):
        if isinstance(regulating_species, str):
            reg_sp_list = regulating_species.replace(' ', '').split(',')
            self.regulating_species = reg_sp_list[0] if len(reg_sp_list) == 1 else reg_sp_list
        else:  # if it is a list
            self.regulating_species = regulating_species

        self.alpha = alpha
        self.K50 = K50
        self.nH = nH

        if isinstance(K50, list):
            self.is_heterogeneous_K50 = [False if isinstance(val, (int, float)) else True for val
                                         in K50]
        else:
            self.is_heterogeneous_K50 = False if isinstance(self.K50, (int, float)) else True

        super().__init__(reactants, products, k)

        self._str += f", regulating_species = {self.regulating_species}, alpha = {self.alpha}, " \
                     f"K50 = {self.K50}, nH = {self.nH}"

        self._validate_reg_params()

        if isinstance(self.alpha, list):
            self.regulation_type = list()
            for a in self.alpha:
                reg_type = 'activation' if a > 1 else 'repression' if a < 1 else 'no regulation'
                self.regulation_type.append(reg_type)
        else:
            self.regulation_type = 'activation' if self.alpha > 1 else 'repression' if self.alpha < 1 else 'no regulation'

    def _validate_reg_params(self):
        """ Validate the parameters specific to the regulation. """
        if isinstance(self.regulating_species, list):  # multiple regulating species
            # First check that the right number of values for each parameter are specified
            rs_num = len(self.regulating_species)
            msg = f"Must specify {rs_num} # values when there are {rs_num} regulating species."
            assert len(self.alpha) == rs_num, msg.replace('#', 'alpha')
            assert len(self.K50) == rs_num, msg.replace('#', 'K50')
            assert len(self.nH) == rs_num, msg.replace('#', 'nH')

            for i in range(len(self.regulating_species)):
                assert self.alpha[i] >= 0, "The alpha parameter must be nonnegative."
                if self.alpha[i] == 1:
                    print("Warning: alpha=1 means the process is not regulated.")

                if isinstance(self.K50[i], (float, int)):
                    assert self.K50[i] > 0, "K50 has to be positive."
                elif isinstance(self.K50[i], list):
                    assert all([True if val > 0 else False for val in self.K50[i]]), \
                        "Subspecies K50 values have to be positive."
                else:  # isinstance(self.K50, tuple)
                    assert self.K50[i][0] > 0 and self.K50[i][1] > 0, \
                        "Mean and std of K50 have to be positive."

                if self.order == 0:
                    assert not self.is_heterogeneous_K50[i], \
                        "Heterogeneity in parameter K50 is not allowed for a 0th order process."

                assert self.nH[i] > 0, "nH has to be positive."

        else:  # just one regulating species
            assert self.alpha >= 0, "The alpha parameter must be nonnegative."
            if self.alpha == 1:
                print("Warning: alpha=1 means the process is not regulated.")

            if isinstance(self.K50, (float, int)):
                assert self.K50 > 0, "K50 has to be positive."
            elif isinstance(self.K50, list):
                assert all([True if val > 0 else False for val in self.K50]), \
                    "Subspecies K50 values have to be positive."
            else:  # isinstance(self.K50, tuple)
                assert self.K50[0] > 0 and self.K50[1] > 0, \
                    "Mean and std of K50 have to be positive."

            if self.order == 0:
                assert not self.is_heterogeneous_K50, \
                    "Heterogeneity in parameter K50 is not allowed for a 0th order process."

            assert self.nH > 0, "nH has to be positive."

    @classmethod
    def from_string(cls,
                    proc_str: str,
                    /,
                    k: float | int | list[float, ...] | tuple[float, float],
                    *,
                    regulating_species: str | list[str, ...] = None,
                    alpha: float | int | list[float | int, ...] = 1,
                    K50: float | int | list[float | int, ...] | tuple[float | int, float | int] |
                         list[float | int | list[float | int, ...] | tuple[
                             float | int, float | int]] = None,
                    nH: float | int | list[float | int, ...] = None,
                    sep: str = '->') -> Self:
        """ Create a regulated process from a string.

        Parameters
        ----------
        proc_str : str
            A string describing the process in standard chemical notation
            (e.g., 'A + B -> C')
        k : float or int or list of floats or 2-tuple of floats
            The *microscopic* rate constant for the given process. It is the *basal*
            rate constant in the case of activation (or the minimum `k` value) 
            and the maximum rate constant in the case of repression. 
            If `k` is a float or int, then the process is homogeneous. 
            If `k` is a list, then the population of the reactants 
            constsists of distinct subspecies or subinteractions 
            depending on the order. If `k` is a 2-tuple,
            then the constant is normally-distributed with a mean and standard
            deviation specified in the tuple's elements. Note that `k` cannot
            be zero for this form of regulation.
        regulating_species : str or list of str
            Name of the regulating species.
        alpha : float or int or list[float or int]
            Parameter denoting the degree of activation/repression.

                - 0 <= alpha < 1: repression
                - alpha = 1: no regulation
                - alpha > 1: activation
                
            alpha is a multiplier: in the case of activation, the maximum 
            rate constant value will be `alpha * k`. 
            In the case of repression, the minimum 
            rate constant value will be `alpha * k`. 
        K50 : float or int or list of floats or 2-tuple of floats or list of each of the previous types
            *Microscopic* constant that corresponds to the number of
            `regulating_species` agents that would produce 
            half-maximal activation/repression. 
            Heterogeneity in this parameter is determined by the type of `K50`,
            using the same rules as for parameter `k`.
        nH : float or int or list[float or int]
            Hill coefficient for the given process. Indicates the degree of 
            cooperativity in the regulatory interaction. 
        sep : str, default: '->'
            Specifies the characters that distinguish the reactants from the
            products. The default is '->'. The code also treats `-->` as a
            default, if it's present in `proc_str`.

        Notes
        -----
        - Species names should not contain spaces, dashes, and
          should start with a non-numeric character.
        - Zeroth order processes should be specified by an empty space or 'None'.

        Examples
        --------
        >>> RegulatedProcess.from_string("A -> X", k=0.2, regulating_species='X', alpha=2, K50=10, nH=1)
        >>> RegulatedProcess.from_string("A -> X", k=0.3, regulating_species='X', alpha=0.5, K50=[10, 15], nH=2)
        >>> RegulatedProcess.from_string("A + B -> X", k=0.5, regulating_species='B, X', alpha=[2, 0], K50=[(15, 5), [10, 15]], nH=[1, 2])
        """
        sep = '-->' if '-->' in proc_str else sep
        if sep not in proc_str:
            raise Exception("Cannot distinguish the reactants from the products.\n"
                            "Please use the *sep* keyword: e.g. sep='->'.")

        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side

        return cls(cls._to_dict(lhs_terms), cls._to_dict(rhs_terms),
                   k,
                   regulating_species, alpha, K50, nH)

    def __str__(self):
        if isinstance(self.regulating_species, list):
            K50_het_str = ""
            for i, sp in enumerate(self.regulating_species):
                if isinstance(self.K50[i], (float, int)):
                    K50_het_str += f"Homogeneous process with respect to species {sp} K50. "
                elif isinstance(self.K50[i], list):
                    K50_het_str += f"Heterogeneous process with respect to species {sp} K50 " \
                                   f"with {len(self.K50[i])} distinct subspecies. "
                else:
                    K50_het_str += f"Heterogeneous process with normally-distributed " \
                                   f"species {sp} K50 with mean {self.K50[i][0]} and " \
                                   f"standard deviation {self.K50[i][1]}. "
        else:
            if isinstance(self.K50, (float, int)):
                K50_het_str = "Homogeneous process with respect to K50."
            elif isinstance(self.K50, list):
                K50_het_str = f"Heterogeneous process with respect to K50 " \
                              f"with {len(self.K50)} distinct subspecies."
            else:
                K50_het_str = f"Heterogeneous process with normally-distributed K50 with " \
                              f"mean {self.K50[0]} and standard deviation {self.K50[1]}."

        return super().__str__() + f" Regulating Species: {self.regulating_species}, " \
                                   f"alpha = {self.alpha}, nH = {self.nH}, " \
                                   f"K50 = {self.K50}, {K50_het_str}"

    def __repr__(self):
        return f"RegulatedProcess Object: " \
               f"RegulatedProcess.from_string('{self._str.split(',')[0]}', " \
               f"k={self.k}, " \
               f"regulating_species='{self.regulating_species}', " \
               f"alpha={self.alpha}, " \
               f"K50={self.K50}, " \
               f"nH={self.nH})"

    def __eq__(self, other):
        if isinstance(other, RegulatedProcess):
            is_equal = (self.k == other.k and
                        self.order == other.order and
                        self.reactants == other.reactants and
                        self.products == other.products and
                        self.regulating_species == other.regulating_species and
                        self.alpha == other.alpha and
                        self.K50 == other.K50 and
                        self.nH == other.nH and
                        self.species == other.species)
            return is_equal
        elif isinstance(other, str):
            return self._str == other or self._str.replace(' ', '') == other
        else:
            print(f"{type(self)} and {type(other)} are instances of different classes.")
            return False

    def __hash__(self):
        return hash(self._str)

Ancestors

Subclasses

Static methods

def from_string(proc_str: str, /, k: float | int | list[float, ...] | tuple[float, float], *, regulating_species: str | list[str, ...] = None, alpha: float | int | list[float | int, ...] = 1, K50: float | int | list[float | int, ...] | tuple[float | int, float | int] | list[float | int | list[float | int, ...] | tuple[float | int, float | int]] = None, nH: float | int | list[float | int, ...] = None, sep: str = '->') ‑> Self

Create a regulated process from a string.

Parameters

proc_str : str
A string describing the process in standard chemical notation (e.g., 'A + B -> C')
k : float or int or list of floats or 2-tuple of floats
The microscopic rate constant for the given process. It is the basal rate constant in the case of activation (or the minimum k value) and the maximum rate constant in the case of repression. If k is a float or int, then the process is homogeneous. If k is a list, then the population of the reactants constsists of distinct subspecies or subinteractions depending on the order. If k is a 2-tuple, then the constant is normally-distributed with a mean and standard deviation specified in the tuple's elements. Note that k cannot be zero for this form of regulation.
regulating_species : str or list of str
Name of the regulating species.
alpha : float or int or list[float or int]

Parameter denoting the degree of activation/repression.

- 0 <= alpha < 1: repression
- alpha = 1: no regulation
- alpha > 1: activation

alpha is a multiplier: in the case of activation, the maximum rate constant value will be alpha * k. In the case of repression, the minimum rate constant value will be alpha * k.

K50 : float or int or list of floats or 2-tuple of floats or list of each of the previous types
Microscopic constant that corresponds to the number of regulating_species agents that would produce half-maximal activation/repression. Heterogeneity in this parameter is determined by the type of K50, using the same rules as for parameter k.
nH : float or int or list[float or int]
Hill coefficient for the given process. Indicates the degree of cooperativity in the regulatory interaction.
sep : str, default: '->'
Specifies the characters that distinguish the reactants from the products. The default is '->'. The code also treats --> as a default, if it's present in proc_str.

Notes

  • Species names should not contain spaces, dashes, and should start with a non-numeric character.
  • Zeroth order processes should be specified by an empty space or 'None'.

Examples

>>> RegulatedProcess.from_string("A -> X", k=0.2, regulating_species='X', alpha=2, K50=10, nH=1)
>>> RegulatedProcess.from_string("A -> X", k=0.3, regulating_species='X', alpha=0.5, K50=[10, 15], nH=2)
>>> RegulatedProcess.from_string("A + B -> X", k=0.5, regulating_species='B, X', alpha=[2, 0], K50=[(15, 5), [10, 15]], nH=[1, 2])
Expand source code
@classmethod
def from_string(cls,
                proc_str: str,
                /,
                k: float | int | list[float, ...] | tuple[float, float],
                *,
                regulating_species: str | list[str, ...] = None,
                alpha: float | int | list[float | int, ...] = 1,
                K50: float | int | list[float | int, ...] | tuple[float | int, float | int] |
                     list[float | int | list[float | int, ...] | tuple[
                         float | int, float | int]] = None,
                nH: float | int | list[float | int, ...] = None,
                sep: str = '->') -> Self:
    """ Create a regulated process from a string.

    Parameters
    ----------
    proc_str : str
        A string describing the process in standard chemical notation
        (e.g., 'A + B -> C')
    k : float or int or list of floats or 2-tuple of floats
        The *microscopic* rate constant for the given process. It is the *basal*
        rate constant in the case of activation (or the minimum `k` value) 
        and the maximum rate constant in the case of repression. 
        If `k` is a float or int, then the process is homogeneous. 
        If `k` is a list, then the population of the reactants 
        constsists of distinct subspecies or subinteractions 
        depending on the order. If `k` is a 2-tuple,
        then the constant is normally-distributed with a mean and standard
        deviation specified in the tuple's elements. Note that `k` cannot
        be zero for this form of regulation.
    regulating_species : str or list of str
        Name of the regulating species.
    alpha : float or int or list[float or int]
        Parameter denoting the degree of activation/repression.

            - 0 <= alpha < 1: repression
            - alpha = 1: no regulation
            - alpha > 1: activation
            
        alpha is a multiplier: in the case of activation, the maximum 
        rate constant value will be `alpha * k`. 
        In the case of repression, the minimum 
        rate constant value will be `alpha * k`. 
    K50 : float or int or list of floats or 2-tuple of floats or list of each of the previous types
        *Microscopic* constant that corresponds to the number of
        `regulating_species` agents that would produce 
        half-maximal activation/repression. 
        Heterogeneity in this parameter is determined by the type of `K50`,
        using the same rules as for parameter `k`.
    nH : float or int or list[float or int]
        Hill coefficient for the given process. Indicates the degree of 
        cooperativity in the regulatory interaction. 
    sep : str, default: '->'
        Specifies the characters that distinguish the reactants from the
        products. The default is '->'. The code also treats `-->` as a
        default, if it's present in `proc_str`.

    Notes
    -----
    - Species names should not contain spaces, dashes, and
      should start with a non-numeric character.
    - Zeroth order processes should be specified by an empty space or 'None'.

    Examples
    --------
    >>> RegulatedProcess.from_string("A -> X", k=0.2, regulating_species='X', alpha=2, K50=10, nH=1)
    >>> RegulatedProcess.from_string("A -> X", k=0.3, regulating_species='X', alpha=0.5, K50=[10, 15], nH=2)
    >>> RegulatedProcess.from_string("A + B -> X", k=0.5, regulating_species='B, X', alpha=[2, 0], K50=[(15, 5), [10, 15]], nH=[1, 2])
    """
    sep = '-->' if '-->' in proc_str else sep
    if sep not in proc_str:
        raise Exception("Cannot distinguish the reactants from the products.\n"
                        "Please use the *sep* keyword: e.g. sep='->'.")

    lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
    lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
    rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side

    return cls(cls._to_dict(lhs_terms), cls._to_dict(rhs_terms),
               k,
               regulating_species, alpha, K50, nH)
class ReversibleProcess (reactants: dict, products: dict, k: float | int | list[float, ...] | tuple[float, float], k_rev: float | int | list[float, ...] | tuple[float, float])

Define a reversible process.

Attributes

k_rev : float or int or list of floats or 2-tuple of floats
The microscopic rate constant for the reverse process.
is_heterogeneous_rev : bool
Denotes if the parameter k_rev exhibits heterogeneity (distinct subspecies/interactions or normally-distributed).

Notes

A ReversibleProcess object gets split into two Process objects (forward and reverse process) when the algorithm runs.

Expand source code
class ReversibleProcess(Process):
    """ Define a reversible process.

    Attributes
    ----------
    k_rev : float or int or list of floats or 2-tuple of floats
        The *microscopic* rate constant for the reverse process.
    is_heterogeneous_rev : bool
        Denotes if the parameter `k_rev` exhibits heterogeneity
        (distinct subspecies/interactions or normally-distributed).

    Notes
    -----
    A ReversibleProcess object gets split into two Process objects
    (forward and reverse process) when the algorithm runs.
    """

    def __init__(self, reactants: dict, products: dict,
                 k: float | int | list[float, ...] | tuple[float, float],
                 k_rev: float | int | list[float, ...] | tuple[float, float]):
        self.k_rev = k_rev  # rate constant for reverse process
        self.is_heterogeneous_rev = False if isinstance(self.k_rev, (int, float)) else True

        super().__init__(reactants, products, k)

        self.order_rev = sum(self.products.values())

    @classmethod
    def from_string(cls,
                    proc_str: str,
                    /,
                    k: float | int | list[float, ...] | tuple[float, float],
                    *,
                    k_rev: float | int | list[float, ...] | tuple[float, float] = 0,
                    sep: str = '<->') -> Self:
        """ Create a reversible process from a string.

        Parameters
        ----------
        proc_str : str
            A string describing the process in standard chemical notation
            (e.g., 'A + B <-> C')
        k : float or int or list of floats or 2-tuple of floats
            The *microscopic* rate constant for the forward process.
        k_rev : float or int or list of floats or 2-tuple of floats
            The *microscopic* rate constant for the reverse process.
        sep : str, default: '<->'
            Specifies the characters that distinguish the reactants from the
            products. The default is '<->'. The code also treats `<-->` as a
            default, if it's present in `proc_str`.

        Notes
        -----
        - Species names should not contain spaces, dashes, and
          should start with a non-numeric character.

        Examples
        --------
        >>> ReversibleProcess.from_string("2A + B <-> X", 0.3, k_rev=0.2)
        """
        for s in ['<-->', '<=>', '<==>']:
            sep = s if s in proc_str else sep
        if sep not in proc_str:
            raise Exception("Cannot distinguish the reactants from the products.\n"
                            "Please use the *sep* keyword: e.g. sep='<->'.")

        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side

        return cls(cls._to_dict(lhs_terms), cls._to_dict(rhs_terms),
                   k, k_rev)

    def __repr__(self):
        return f"ReversibleProcess Object: ReversibleProcess.from_string(" \
               f"'{self._str.split(',')[0]}', k={self.k}, k_rev={self.k_rev})"

    def __str__(self):
        if isinstance(self.k, (float, int)):
            het_str = "Forward homogeneous process."
        elif isinstance(self.k, list):
            het_str = f"Forward heterogeneous process with {len(self.k)} " \
                      f"distinct subspecies."
        else:
            het_str = f"Forward heterogeneous process with normally-distributed " \
                      f"k with mean {self.k[0]} and standard deviation {self.k[1]}."

        if isinstance(self.k_rev, (float, int)):
            het_rev_str = "Reverse homogeneous process."
        elif isinstance(self.k_rev, list):
            het_rev_str = f"Reverse heterogeneous process with {len(self.k_rev)} " \
                          f"distinct subspecies."
        else:
            het_rev_str = f"Reverse heterogeneous process with normally-distributed " \
                          f"k with mean {self.k_rev[0]} and standard deviation {self.k_rev[1]}."

        lhs, rhs = self._reconstruct_string()
        return " <-> ".join([lhs, rhs]) + f", k = {self.k}, k_rev = {self.k_rev}; " \
                                          f"{het_str} {het_rev_str}"

    def _reconstruct_string(self):
        lhs = ' + '.join([f"{str(val) + ' ' if val not in [0, 1] else ''}{key}" for key, val in
                          self.reactants.items()])
        rhs = ' + '.join([f"{str(val) + ' ' if val not in [0, 1] else ''}{key}" for key, val in
                          self.products.items()])
        return lhs, rhs

    def __eq__(self, other):
        if isinstance(other, ReversibleProcess):
            is_equal = (self.k == other.k and
                        self.order == other.order and
                        self.k_rev == other.k_rev and
                        self.order_rev == other.order_rev and
                        self.reactants == other.reactants and
                        self.products == other.products and
                        self.species == other.species)
            return is_equal
        elif isinstance(other, str):
            return self._str == other or self._str.replace(' ', '') == other
        else:
            print(f"{type(self)} and {type(other)} are instances of different classes.")
            return False

    def __hash__(self):
        return hash(self._str)

Ancestors

Static methods

def from_string(proc_str: str, /, k: float | int | list[float, ...] | tuple[float, float], *, k_rev: float | int | list[float, ...] | tuple[float, float] = 0, sep: str = '<->') ‑> Self

Create a reversible process from a string.

Parameters

proc_str : str
A string describing the process in standard chemical notation (e.g., 'A + B <-> C')
k : float or int or list of floats or 2-tuple of floats
The microscopic rate constant for the forward process.
k_rev : float or int or list of floats or 2-tuple of floats
The microscopic rate constant for the reverse process.
sep : str, default: '<->'
Specifies the characters that distinguish the reactants from the products. The default is '<->'. The code also treats <--> as a default, if it's present in proc_str.

Notes

  • Species names should not contain spaces, dashes, and should start with a non-numeric character.

Examples

>>> ReversibleProcess.from_string("2A + B <-> X", 0.3, k_rev=0.2)
Expand source code
@classmethod
def from_string(cls,
                proc_str: str,
                /,
                k: float | int | list[float, ...] | tuple[float, float],
                *,
                k_rev: float | int | list[float, ...] | tuple[float, float] = 0,
                sep: str = '<->') -> Self:
    """ Create a reversible process from a string.

    Parameters
    ----------
    proc_str : str
        A string describing the process in standard chemical notation
        (e.g., 'A + B <-> C')
    k : float or int or list of floats or 2-tuple of floats
        The *microscopic* rate constant for the forward process.
    k_rev : float or int or list of floats or 2-tuple of floats
        The *microscopic* rate constant for the reverse process.
    sep : str, default: '<->'
        Specifies the characters that distinguish the reactants from the
        products. The default is '<->'. The code also treats `<-->` as a
        default, if it's present in `proc_str`.

    Notes
    -----
    - Species names should not contain spaces, dashes, and
      should start with a non-numeric character.

    Examples
    --------
    >>> ReversibleProcess.from_string("2A + B <-> X", 0.3, k_rev=0.2)
    """
    for s in ['<-->', '<=>', '<==>']:
        sep = s if s in proc_str else sep
    if sep not in proc_str:
        raise Exception("Cannot distinguish the reactants from the products.\n"
                        "Please use the *sep* keyword: e.g. sep='<->'.")

    lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
    lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
    rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side

    return cls(cls._to_dict(lhs_terms), cls._to_dict(rhs_terms),
               k, k_rev)