abstochkin.process API documentation

# abstochkin.process

Define a process of the form Reactants -> Products.

View Source

       1""" Define a process of the form Reactants -> Products. """
       2
       3#  Copyright (c) 2024-2025, Alex Plakantonakis.
       4#
       5#  This program is free software: you can redistribute it and/or modify
       6#  it under the terms of the GNU General Public License as published by
       7#  the Free Software Foundation, either version 3 of the License, or
       8#  (at your option) any later version.
       9#
      10#  This program is distributed in the hope that it will be useful,
      11#  but WITHOUT ANY WARRANTY; without even the implied warranty of
      12#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      13#  GNU General Public License for more details.
      14#
      15#  You should have received a copy of the GNU General Public License
      16#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
      17
      18import contextlib
      19import re
      20from typing import Self
      21
      22from numpy import array
      23
      24from abstochkin.utils import macro_to_micro
      25
      26
      27class Process:
      28    """
      29    Define a unidirectional process: Reactants -> Products, where the
      30    Reactants and Products are specified using standard chemical notation.
      31    That is, stoichiometric coefficients (integers) and species names are
      32    specified. For example: 2A + B -> C.
      33
      34    Attributes
      35    ----------
      36    reactants : dict
      37        The reactants of a given process are specified with
      38        key-value pairs describing each species name and its
      39        stoichiometric coefficient, respectively.
      40    products : dict
      41        The products of a given process are specified with
      42        key-value pairs describing each species name and its
      43        stoichiometric coefficient, respectively.
      44    k : float, int, list of floats, tuple of floats
      45        The *microscopic* rate constant(s) for the given process. The data
      46        type of `k` determines the "structure" of the population as follows:
      47            - A homogeneous population: if `k` is a single value (float or int),
      48              then the population is assumed to be homogeneous with all agents
      49              of the reactant species having kinetics defined by this value.
      50            - A heterogeneous population with a distinct number of subspecies
      51              (each with a corresponding `k` value): if `k` is a list of floats,
      52              then the population is assumed to be heterogeneous with a number
      53              of subspecies equal to the length of the list.
      54            - A heterogeneous population with normally-distributed `k` values:
      55              If `k` is a tuple whose length is 2, then the population is
      56              assumed to be heterogeneous with a normally distributed `k` value.
      57              The two entries in the tuple represent the mean and standard
      58              deviation (in that order) of the desired normal distribution.
      59    volume : float, default : None, optional
      60        The volume *in liters* of the compartment in which the processes
      61        are taking place.
      62    order : int
      63        The order of the process (or the molecularity of an elementary process).
      64        It is the sum of the stoichiometric coefficients of the reactants.
      65    species : set of strings
      66        A set of all species in a process.
      67    reacts_ : list of strings
      68        A list containing all the reactants in a process.
      69    prods_ : list of strings
      70        A list containing all the products in a process.
      71
      72    Methods
      73    -------
      74    from_string
      75        Class method for creating a Process object from a string.
      76    """
      77
      78    def __init__(self,
      79                 /,
      80                 reactants: dict[str, int],
      81                 products: dict[str, int],
      82                 k: float | int | list[float, ...] | tuple[float, float],
      83                 *,
      84                 volume: float | None = None,
      85                 **kwargs):
      86
      87        self.reactants = reactants
      88        self.products = products
      89        self.k = k
      90        self.volume = volume
      91
      92        self._validate_nums()  # make sure there are no errors in given numbers
      93
      94        # For consistency with processes instantiated using the class method `from_string()`,
      95        # species denoted as 'None' for a 0th order process are renamed to ''.
      96        if 'None' in self.reactants.keys():
      97            self.reactants[''] = self.reactants.pop('None')
      98        if 'None' in self.products.keys():
      99            self.products[''] = self.products.pop('None')
     100
     101        self.order = sum(self.reactants.values())
     102
     103        self.is_heterogeneous = False if isinstance(self.k, (int, float)) else True
     104
     105        if self.order == 0:
     106            msg = "Since a birth process does not depended on the presence of agents, " \
     107                  "heterogeneity does not make sense in this context. Please define " \
     108                  "the rate constant k as a number. "
     109            assert not self.is_heterogeneous, msg
     110
     111        # Convert macroscopic to microscopic rate constant
     112        if self.volume is not None:
     113            self.k = macro_to_micro(self.k, self.volume, self.order)
     114
     115        # Two ways of storing the involved species:
     116        # 1) A set of all species
     117        self.species = set((self.reactants | self.products).keys())
     118        with contextlib.suppress(KeyError):
     119            self.species.remove('')  # remove empty species name from any 0th order processes
     120
     121        # 2) Separate lists
     122        self.reacts_ = list()  # [reactant species]
     123        self.prods_ = list()  # [product species]
     124        self._get_reacts_prods_()
     125
     126        """ Because Process objects are used as keys in dictionaries used
     127        in an AbStochKin simulation, it's much faster to generate the object's
     128        string representation once, and then access it whenever it's needed
     129        (which could be thousands of times during a simulation). """
     130        self._str = self.__str__().split(';')[0]
     131
     132        if len(kwargs) > 0:
     133            self._lsp(kwargs)
     134
     135    def _get_reacts_prods_(self):
     136        """ Make lists of the reactant and product species. Repeated
     137        elements of a list reflect the order or molecularity of the
     138        species in the given process. For example, for the process
     139        `2A + B -> C + D, reacts_ = ['A', 'A', 'B'], prods_ = ['C', 'D']`. """
     140        for r, m in self.reactants.items():
     141            for i in range(m):
     142                self.reacts_.append(r)
     143
     144        for p, m in self.products.items():
     145            for i in range(m):
     146                self.prods_.append(p)
     147
     148        if '' in self.reacts_:  # remove empty reactant species names
     149            self.reacts_.remove('')  # from 0th order processes
     150        if '' in self.prods_:  # remove empty product species names
     151            self.prods_.remove('')  # from degradation processes
     152
     153    @classmethod
     154    def from_string(cls,
     155                    proc_str: str,
     156                    /,
     157                    k: float | int | list[float, ...] | tuple[float, float],
     158                    *,
     159                    volume: float | None = None,
     160                    sep: str = '->',
     161                    **kwargs) -> Self:
     162        """ Create a process from a string.
     163
     164        Parameters
     165        ----------
     166        proc_str : str
     167            A string describing the process in standard chemical notation
     168            (e.g., 'A + B -> C')
     169        k : float or int or list of floats or 2-tuple of floats
     170            The rate constant for the given process. If `k` is a float or
     171            int, then the process is homogeneous. If `k` is a list, then
     172            the population of the reactants constsists of distinct subspecies
     173            or subinteractions depending on the order. If `k` is a 2-tuple,
     174            then the constant is normally-distributed with a mean and standard
     175            deviation specified in the tuple's elements.
     176        volume : float, default : None, optional
     177            The volume *in liters* of the compartment in which the processes
     178            are taking place.
     179        sep : str, default: '->'
     180            Specifies the characters that distinguish the reactants from the
     181            products. The default is '->'. The code also treats `-->` as a
     182            default, if it's present in `proc_str`.
     183
     184        Notes
     185        -----
     186        - Species names should not contain spaces, dashes, and
     187          should start with a non-numeric character.
     188        - Zeroth order processes should be specified by an empty space or 'None'.
     189
     190        Examples
     191        --------
     192        >>> Process.from_string("2A + B -> X", 0.3)
     193        >>> Process.from_string(" -> Y", 0.1)  # for a 0th order (birth) process.
     194        >>> Process.from_string("Protein_X -> None", 0.15)  # for a 1st order degradation process.
     195        """
     196
     197        if len(kwargs) > 0:
     198            cls._lsp(kwargs)
     199
     200        sep = '-->' if '-->' in proc_str else sep
     201        if sep not in proc_str:
     202            raise Exception("Cannot distinguish the reactants from the products.\n"
     203                            "Please use the *sep* keyword: e.g. sep='->'.")
     204
     205        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
     206        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
     207        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side
     208
     209        return cls(reactants=cls._to_dict(lhs_terms),
     210                   products=cls._to_dict(rhs_terms),
     211                   k=k,
     212                   volume=volume)
     213
     214    @staticmethod
     215    def _lsp(kwargs: dict):
     216        """
     217        The `Process` class accepts additional arguments (`**kwargs`).
     218        Since the Process class is a base class for other subclasses,
     219        this is done so that the Liskov Substitution Principle (LSP)
     220        is not violated.
     221        (https://en.wikipedia.org/wiki/Liskov_substitution_principle).
     222        This way, subclasses override the `from_string` method and have
     223        additional parameters while remaining consistent with this method
     224        from their base class. Calling the base instance with the additional
     225        parameters gives a warning that they will have no effect so that
     226        the user can intervene, if that's desired.
     227        """
     228        msg = f"Warning: Additional parameters {','.join([str(i) for i in kwargs.items()])} " \
     229              f"will have no effect. "
     230        if 'k_rev' in kwargs.keys():
     231            msg += f"If that's not what you intended, define the process " \
     232                   f"using ReversibleProcess()."
     233        if 'regulating_species' in kwargs.keys() or 'alpha' in kwargs.keys() or \
     234                'nH' in kwargs.keys() or 'K50' in kwargs.keys():
     235            msg += f"If that's not what you intended, define the process " \
     236                   f"using RegulatedProcess()."
     237        if 'catalyst' in kwargs.keys():
     238            msg += f"If that's not what you intended, define the process " \
     239                   f"using MichaelisMentenProcess()."
     240        print(msg)
     241
     242    @staticmethod
     243    def _to_dict(terms: list) -> dict:
     244        """ Convert the information for a side (left, right) of a process
     245        to a dictionary. """
     246        side_terms = dict()  # for storing the information of a side of a process
     247        patt = '^[\\-]*[1-9]+'  # regex pattern (accounts for leading erroneous minus sign)
     248
     249        if len(terms) == 1 and terms[0].strip().lower() in ['', 'none']:
     250            spec = ''  # Zeroth order process
     251            side_terms[spec] = 0
     252        else:
     253            for term in terms:
     254                term = term.strip()
     255                try:
     256                    match = re.search(patt, term)
     257                    stoic_coef = term[slice(*match.span())]  # extract stoichiometric coef
     258                    spec = re.split(patt, term)[-1].strip()  # extract species name
     259                    if spec == '' and stoic_coef != 0:
     260                        raise NullSpeciesNameError()
     261                    stoic_coef = int(stoic_coef)
     262                except AttributeError:  # when there is no specified stoichiometric coefficient
     263                    spec = re.split(patt, term)[-1]  # extract species name
     264                    stoic_coef = 1
     265
     266                if spec not in side_terms.keys():
     267                    side_terms[spec] = stoic_coef
     268                else:
     269                    side_terms[spec] += stoic_coef
     270
     271        return side_terms
     272
     273    def _validate_nums(self):
     274        """ Make sure coefficients, rate constant, and volume values are not negative. """
     275        # Check coefficients
     276        for r, val in (self.reactants | self.products).items():
     277            assert val >= 0, f"Coefficient cannot be negative: {val} {r}."
     278
     279        # Check rate constants
     280        error_msg = f"Rate constant values have to be positive: k = {self.k}."
     281        if isinstance(self.k, (list, tuple)):  # heterogeneous population
     282            assert all(array(self.k) > 0), error_msg
     283        else:  # when k is a float or int, the population is homogeneous
     284            assert self.k > 0, error_msg
     285
     286        # For normally-distributed k values, specification is a 2-tuple.
     287        if isinstance(self.k, tuple):  # normal distribution of k values
     288            assert len(self.k) == 2, "Please specify the mean and standard deviation " \
     289                                     "of k in a 2-tuple: (mean, std)."
     290
     291        # Check volume
     292        if self.volume is not None:
     293            assert self.volume > 0, f"Volume cannot be negative: {self.volume}."
     294
     295    def __eq__(self, other):
     296        if isinstance(other, Process):
     297            is_equal = (self.k == other.k and
     298                        self.order == other.order and
     299                        self.reactants == other.reactants and
     300                        self.products == other.products and
     301                        self.species == other.species and
     302                        self.volume == other.volume)
     303            return is_equal
     304        elif isinstance(other, str):
     305            return self._str == other or self._str.replace(' ', '') == other
     306        else:
     307            print(f"{type(self)} and {type(other)} are instances of different classes.")
     308            return False
     309
     310    def __hash__(self):
     311        return hash(self._str)
     312
     313    def __contains__(self, item):
     314        return True if item in self.species else False
     315
     316    def __repr__(self):
     317        repr_k = macro_to_micro(self.k, self.volume, self.order, inverse=True) if self.volume is not None else self.k
     318        return f"Process Object: Process.from_string('{self._str.split(',')[0]}', " \
     319               f"k={repr_k}, " \
     320               f"volume={self.volume})"
     321
     322    def __str__(self):
     323        if isinstance(self.k, (float, int)):
     324            het_str = "Homogeneous process."
     325        elif isinstance(self.k, list):
     326            het_str = f"Heterogeneous process with {len(self.k)} distinct subspecies."
     327        else:
     328            het_str = f"Heterogeneous process with normally-distributed k with " \
     329                      f"mean {self.k[0]} and standard deviation {self.k[1]}."
     330
     331        lhs, rhs = self._reconstruct_string()
     332
     333        vol_str = f", volume = {self.volume} L" if self.volume is not None else ""
     334        return ' -> '.join([lhs, rhs]) + f', k = {self.k}{vol_str}; {het_str}'
     335
     336    def _reconstruct_string(self):
     337        lhs = ' + '.join([f"{str(val) + ' ' if val not in [0, 1] else ''}{key}" for key, val in
     338                          self.reactants.items()])
     339        rhs = ' + '.join([f"{str(val) + ' ' if val not in [0, 1] else ''}{key}" for key, val in
     340                          self.products.items()])
     341        return lhs, rhs
     342
     343
     344class ReversibleProcess(Process):
     345    """ Define a reversible process.
     346
     347    The class-specific attributes are listed below.
     348
     349    Attributes
     350    ----------
     351    k_rev : float or int or list of floats or 2-tuple of floats
     352        The *microscopic* rate constant for the reverse process.
     353    is_heterogeneous_rev : bool
     354        Denotes if the parameter `k_rev` exhibits heterogeneity
     355        (distinct subspecies/interactions or normally-distributed).
     356
     357    Notes
     358    -----
     359    A `ReversibleProcess` object gets split into two `Process` objects
     360    (forward and reverse process) when the algorithm runs.
     361    """
     362
     363    def __init__(self,
     364                 /,
     365                 reactants: dict[str, int],
     366                 products: dict[str, int],
     367                 k: float | int | list[float, ...] | tuple[float, float],
     368                 k_rev: float | int | list[float, ...] | tuple[float, float],
     369                 *,
     370                 volume: float | None = None):
     371
     372        self.k_rev = k_rev  # rate constant for reverse process
     373
     374        super().__init__(reactants=reactants,
     375                         products=products,
     376                         k=k,
     377                         volume=volume)
     378
     379        self.is_heterogeneous_rev = False if isinstance(self.k_rev, (int, float)) else True
     380        self.order_rev = sum(self.products.values())
     381
     382        if self.volume is not None:  # Convert macroscopic to microscopic rate constants
     383            self.k_rev = macro_to_micro(k_rev, self.volume, self.order_rev)
     384
     385    @classmethod
     386    def from_string(cls,
     387                    proc_str: str,
     388                    /,
     389                    k: float | int | list[float, ...] | tuple[float, float],
     390                    *,
     391                    k_rev: float | int | list[float, ...] | tuple[float, float] = 0,
     392                    volume: float | None = None,
     393                    sep: str = '<->') -> Self:
     394        """ Create a reversible process from a string.
     395
     396        Parameters
     397        ----------
     398        proc_str : str
     399            A string describing the process in standard chemical notation
     400            (e.g., 'A + B <-> C')
     401        k : float or int or list of floats or 2-tuple of floats
     402            The *microscopic* rate constant for the forward process.
     403        k_rev : float or int or list of floats or 2-tuple of floats
     404            The *microscopic* rate constant for the reverse process.
     405        volume : float, default : None, optional
     406            The volume *in liters* of the compartment in which the processes
     407            are taking place.
     408        sep : str, default: '<->'
     409            Specifies the characters that distinguish the reactants from the
     410            products. The default is '<->'. The code also treats `<-->` as a
     411            default, if it's present in `proc_str`.
     412
     413        Notes
     414        -----
     415        - Species names should not contain spaces, dashes, and
     416          should start with a non-numeric character.
     417
     418        Examples
     419        --------
     420        >>> ReversibleProcess.from_string("2A + B <-> X", 0.3, k_rev=0.2)
     421        """
     422        for s in ['<-->', '<=>', '<==>']:
     423            sep = s if s in proc_str else sep
     424        if sep not in proc_str:
     425            raise Exception("Cannot distinguish the reactants from the products.\n"
     426                            "Please use the *sep* keyword: e.g. sep='<->'.")
     427
     428        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
     429        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
     430        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side
     431
     432        return cls(reactants=cls._to_dict(lhs_terms),
     433                   products=cls._to_dict(rhs_terms),
     434                   k=k,
     435                   k_rev=k_rev,
     436                   volume=volume)
     437
     438    def __repr__(self):
     439        repr_k = macro_to_micro(self.k, self.volume, self.order, inverse=True) if self.volume is not None else self.k
     440        repr_k_rev = macro_to_micro(self.k_rev, self.volume, self.order_rev,
     441                                    inverse=True) if self.volume is not None else self.k_rev
     442        return f"ReversibleProcess Object: ReversibleProcess.from_string(" \
     443               f"'{self._str.split(',')[0]}', " \
     444               f"k={repr_k}, " \
     445               f"k_rev={repr_k_rev}, " \
     446               f"volume={self.volume})"
     447
     448    def __str__(self):
     449        if isinstance(self.k, (float, int)):
     450            het_str = "Forward homogeneous process."
     451        elif isinstance(self.k, list):
     452            het_str = f"Forward heterogeneous process with {len(self.k)} " \
     453                      f"distinct subspecies."
     454        else:
     455            het_str = f"Forward heterogeneous process with normally-distributed " \
     456                      f"k with mean {self.k[0]} and standard deviation {self.k[1]}."
     457
     458        if isinstance(self.k_rev, (float, int)):
     459            het_rev_str = "Reverse homogeneous process."
     460        elif isinstance(self.k_rev, list):
     461            het_rev_str = f"Reverse heterogeneous process with {len(self.k_rev)} " \
     462                          f"distinct subspecies."
     463        else:
     464            het_rev_str = f"Reverse heterogeneous process with normally-distributed " \
     465                          f"k with mean {self.k_rev[0]} and standard deviation {self.k_rev[1]}."
     466
     467        lhs, rhs = self._reconstruct_string()
     468        vol_str = f", volume = {self.volume} L" if self.volume is not None else ""
     469        return " <-> ".join([lhs, rhs]) + f", k = {self.k}, k_rev = {self.k_rev}{vol_str}; " \
     470                                          f"{het_str} {het_rev_str}"
     471
     472    def _reconstruct_string(self):
     473        lhs = ' + '.join([f"{str(val) + ' ' if val not in [0, 1] else ''}{key}" for key, val in
     474                          self.reactants.items()])
     475        rhs = ' + '.join([f"{str(val) + ' ' if val not in [0, 1] else ''}{key}" for key, val in
     476                          self.products.items()])
     477        return lhs, rhs
     478
     479    def __eq__(self, other):
     480        if isinstance(other, ReversibleProcess):
     481            is_equal = (self.k == other.k and
     482                        self.order == other.order and
     483                        self.k_rev == other.k_rev and
     484                        self.order_rev == other.order_rev and
     485                        self.reactants == other.reactants and
     486                        self.products == other.products and
     487                        self.species == other.species and
     488                        self.volume == other.volume)
     489            return is_equal
     490        elif isinstance(other, str):
     491            return self._str == other or self._str.replace(' ', '') == other
     492        else:
     493            print(f"{type(self)} and {type(other)} are instances of different classes.")
     494            return False
     495
     496    def __hash__(self):
     497        return hash(self._str)
     498
     499
     500class MichaelisMentenProcess(Process):
     501    """ Define a process that obeys Michaelis-Menten kinetics.
     502
     503    The class-specific attributes are listed below.
     504
     505    Attributes
     506    ----------
     507    catalyst : str
     508        Name of the species acting as a catalyst for this process.
     509    Km : float or int or list of floats or 2-tuple of floats
     510        *Microscopic* Michaelis constant. Corresponds to the number
     511        of `catalyst` agents that would produce half-maximal activity.
     512        Heterogeneity in this parameter is determined by the type of `Km`,
     513        using the same rules as for parameter `k`.
     514    is_heterogeneous_Km : bool
     515        Denotes if the parameter `Km` exhibits heterogeneity
     516        (distinct subspecies/interactions or normally-distributed).
     517    """
     518
     519    def __init__(self,
     520                 /,
     521                 reactants: dict[str, int],
     522                 products: dict[str, int],
     523                 k: float | int | list[float | int, ...] | tuple[float | int, float | int],
     524                 *,
     525                 catalyst: str,
     526                 Km: float | int | list[float | int, ...] | tuple[float | int, float | int],
     527                 volume: float | None = None):
     528
     529        self.catalyst = catalyst
     530        self.Km = Km
     531
     532        super().__init__(reactants=reactants,
     533                         products=products,
     534                         k=k,
     535                         volume=volume)
     536
     537        self.is_heterogeneous_Km = False if isinstance(self.Km, (int, float)) else True
     538        self.species.add(self.catalyst)
     539        self._str += f", catalyst = {self.catalyst}, Km = {self.Km}"
     540
     541        assert self.order != 0, "A 0th order process has no substrate for a catalyst " \
     542                                "to act on, therefore it cannot follow Michaelis-Menten kinetics."
     543        if self.order == 2:
     544            raise NotImplementedError
     545
     546        if self.volume is not None:  # Convert macroscopic to microscopic Km value
     547            self.Km = macro_to_micro(Km, self.volume)
     548
     549    @classmethod
     550    def from_string(cls,
     551                    proc_str: str,
     552                    /,
     553                    k: float | int | list[float | int, ...] | tuple[float | int, float | int],
     554                    *,
     555                    catalyst: str = None,
     556                    Km: float | int | list[float | int, ...] | tuple[
     557                        float | int, float | int] = None,
     558                    volume: float | None = None,
     559                    sep: str = '->') -> Self:
     560        """ Create a Michaelis-Menten process from a string.
     561
     562        Parameters
     563        ----------
     564        proc_str : str
     565            A string describing the process in standard chemical notation
     566            (e.g., 'A + B -> C')
     567        k : float or int or list of floats or 2-tuple of floats
     568            The *microscopic* rate constant for the given process. If `k` is a
     569            float or int, then the process is homogeneous. If `k` is a list, then
     570            the population of the reactants constsists of distinct subspecies
     571            or subinteractions depending on the order. If `k` is a 2-tuple,
     572            then the constant is normally-distributed with a mean and standard
     573            deviation specified in the tuple's elements.
     574        catalyst : str
     575            Name of species acting as a catalyst.
     576        Km : float or int or list of floats or 2-tuple of floats
     577            *Microscopic* Michaelis constant for the process.
     578            Heterogeneity in this parameter is determined by the type of `Km`,
     579            using the same rules as for parameter `k`.
     580        volume : float, default : None, optional
     581            The volume *in liters* of the compartment in which the processes
     582            are taking place.
     583        sep : str, default: '->'
     584            Specifies the characters that distinguish the reactants from the
     585            products. The default is '->'. The code also treats `-->` as a
     586            default, if it's present in `proc_str`.
     587
     588        Notes
     589        -----
     590        - Species names should not contain spaces, dashes, and
     591          should start with a non-numeric character.
     592        - Zeroth order processes should be specified by an empty space or 'None'.
     593
     594        Examples
     595        --------
     596        >>> MichaelisMentenProcess.from_string("A -> X", k=0.3, catalyst='E', Km=10)
     597        >>> MichaelisMentenProcess.from_string("A -> X", k=0.3, catalyst='alpha', Km=(10, 1))
     598        """
     599        sep = '-->' if '-->' in proc_str else sep
     600        if sep not in proc_str:
     601            raise Exception("Cannot distinguish the reactants from the products.\n"
     602                            "Please use the *sep* keyword: e.g. sep='->'.")
     603
     604        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
     605        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
     606        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side
     607
     608        return cls(reactants=cls._to_dict(lhs_terms),
     609                   products=cls._to_dict(rhs_terms),
     610                   k=k,
     611                   catalyst=catalyst,
     612                   Km=Km,
     613                   volume=volume)
     614
     615    def __repr__(self):
     616        repr_k = macro_to_micro(self.k, self.volume, self.order, inverse=True) if self.volume is not None else self.k
     617        repr_Km = macro_to_micro(self.Km, self.volume, inverse=True) if self.volume is not None else self.Km
     618        return f"MichaelisMentenProcess Object: " \
     619               f"MichaelisMentenProcess.from_string('{self._str.split(',')[0]}', " \
     620               f"k={repr_k}, " \
     621               f"catalyst='{self.catalyst}', " \
     622               f"Km={repr_Km}, " \
     623               f"volume={self.volume})"
     624
     625    def __str__(self):
     626        if isinstance(self.Km, (float, int)):
     627            Km_het_str = "Homogeneous process with respect to Km."
     628        elif isinstance(self.k, list):
     629            Km_het_str = f"Heterogeneous process with respect to Km " \
     630                         f"with {len(self.Km)} distinct subspecies."
     631        else:
     632            Km_het_str = f"Heterogeneous process with normally-distributed Km with " \
     633                         f"mean {self.Km[0]} and standard deviation {self.Km[1]}."
     634
     635        return super().__str__() + f" Catalyst: {self.catalyst}, " \
     636                                   f"Km = {self.Km}, {Km_het_str}"
     637
     638    def __eq__(self, other):
     639        if isinstance(other, MichaelisMentenProcess):
     640            is_equal = (self.k == other.k and
     641                        self.order == other.order and
     642                        self.reactants == other.reactants and
     643                        self.products == other.products and
     644                        self.catalyst == other.catalyst and
     645                        self.Km == other.Km and
     646                        self.species == other.species and
     647                        self.volume == other.volume)
     648            return is_equal
     649        elif isinstance(other, str):
     650            return self._str == other or self._str.replace(' ', '') == other
     651        else:
     652            print(f"{type(self)} and {type(other)} are instances of different classes.")
     653            return False
     654
     655    def __hash__(self):
     656        return hash(self._str)
     657
     658
     659class RegulatedProcess(Process):
     660    """ Define a process that is regulated.
     661
     662    This class allows a Process to be defined in terms of how it is regulated.
     663    If there is only one regulating species, then the parameters have the same
     664    type as would be expected for a homogeneous/heterogeneous process.
     665    If there are multiple regulating species, then all parameters are a list
     666    of their expected type, with the length of the list being equal to the
     667    number of regulating species.
     668
     669    The class-specific attributes (except for `k`, which requires some
     670    additional notes) are listed below.
     671
     672    Attributes
     673    ----------
     674    k : float or int or list of floats or 2-tuple of floats
     675        The *microscopic* rate constant for the given process. It is the *basal*
     676        rate constant in the case of activation (or the minimum `k` value)
     677        and the maximum rate constant in the case of repression.
     678    regulating_species : str or list of str
     679        Name of the regulating species. Multiple species can be specified as
     680        comma-separated in a string or a list of strings with the species names.
     681    alpha : float or int or list[float or int]
     682        Parameter denoting the degree of activation/repression.
     683
     684            - 0 <= alpha < 1: repression
     685            - alpha = 1: no regulation
     686            - alpha > 1: activation
     687
     688        alpha is a multiplier: in the case of activation, the maximum
     689        rate constant value will be `alpha * k`.
     690        In the case of repression, the minimum
     691        rate constant value will be `alpha * k`.
     692    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]
     693        *Microscopic* constant that corresponds to the number of
     694        `regulating_species` agents that would produce
     695        half-maximal activation/repression.
     696        Heterogeneity in this parameter is determined by the type of `K50`,
     697        using the same rules as for parameter `k`.
     698    nH : float or int or list[float or int]
     699        Hill coefficient for the given process. Indicates the degree of
     700        cooperativity in the regulatory interaction.
     701    is_heterogeneous_K50 : bool or list of bool
     702        Denotes if the parameter `K50` exhibits heterogeneity
     703        (distinct subspecies/interactions or normally-distributed).
     704    regulation_type : str or list of str
     705        The type of regulation for this process based on the value of alpha:
     706        'activation' or 'repression' or 'no regulation'.
     707
     708    Notes
     709    -----
     710    Allowing a 0th order process to be regulated. However, heterogeneity
     711    in `k` and `K50` (or any other parameters) is not allowed for such
     712    a process.
     713    """
     714
     715    def __init__(self,
     716                 /,
     717                 reactants: dict[str, int],
     718                 products: dict[str, int],
     719                 k: float | int | list[float, ...] | tuple[float, float],
     720                 *,
     721                 regulating_species: str | list[str, ...],
     722                 alpha: float | int | list[float | int, ...],
     723                 K50: float | int | list[float | int, ...] | tuple[float | int, float | int] |
     724                      list[float | int | list[float | int, ...] | tuple[float | int, float | int]],
     725                 nH: float | int | list[float | int, ...],
     726                 volume: float | None = None):
     727
     728        if isinstance(regulating_species, str):
     729            reg_sp_list = regulating_species.replace(' ', '').split(',')
     730            self.regulating_species = reg_sp_list[0] if len(reg_sp_list) == 1 else reg_sp_list
     731        else:  # if it is a list
     732            self.regulating_species = regulating_species
     733
     734        self.alpha = alpha
     735        self.K50 = K50
     736        self.nH = nH
     737
     738        if isinstance(K50, list):
     739            self.is_heterogeneous_K50 = [False if isinstance(val, (int, float)) else True for val
     740                                         in K50]
     741        else:
     742            self.is_heterogeneous_K50 = False if isinstance(self.K50, (int, float)) else True
     743
     744        super().__init__(reactants=reactants,
     745                         products=products,
     746                         k=k,
     747                         volume=volume)
     748
     749        if self.volume is not None:  # Convert macroscopic to microscopic K50 value
     750            self.K50 = macro_to_micro(K50, self.volume)
     751
     752        self._str += f", regulating_species = {self.regulating_species}, alpha = {self.alpha}, " \
     753                     f"K50 = {self.K50}, nH = {self.nH}"
     754
     755        self._validate_reg_params()
     756
     757        if isinstance(self.alpha, list):
     758            self.regulation_type = list()
     759            for a in self.alpha:
     760                reg_type = 'activation' if a > 1 else 'repression' if a < 1 else 'no regulation'
     761                self.regulation_type.append(reg_type)
     762        else:
     763            self.regulation_type = 'activation' if self.alpha > 1 else 'repression' if self.alpha < 1 else 'no regulation'
     764
     765    def _validate_reg_params(self):
     766        """ Validate the parameters specific to the regulation. """
     767        if isinstance(self.regulating_species, list):  # multiple regulating species
     768            # First check that the right number of values for each parameter are specified
     769            rs_num = len(self.regulating_species)
     770            msg = f"Must specify {rs_num} # values when there are {rs_num} regulating species."
     771            assert len(self.alpha) == rs_num, msg.replace('#', 'alpha')
     772            assert len(self.K50) == rs_num, msg.replace('#', 'K50')
     773            assert len(self.nH) == rs_num, msg.replace('#', 'nH')
     774
     775            for i in range(len(self.regulating_species)):
     776                assert self.alpha[i] >= 0, "The alpha parameter must be nonnegative."
     777                if self.alpha[i] == 1:
     778                    print("Warning: alpha=1 means the process is not regulated.")
     779
     780                if isinstance(self.K50[i], (float, int)):
     781                    assert self.K50[i] > 0, "K50 has to be positive."
     782                elif isinstance(self.K50[i], list):
     783                    assert all([True if val > 0 else False for val in self.K50[i]]), \
     784                        "Subspecies K50 values have to be positive."
     785                else:  # isinstance(self.K50, tuple)
     786                    assert self.K50[i][0] > 0 and self.K50[i][1] > 0, \
     787                        "Mean and std of K50 have to be positive."
     788
     789                if self.order == 0:
     790                    assert not self.is_heterogeneous_K50[i], \
     791                        "Heterogeneity in parameter K50 is not allowed for a 0th order process."
     792
     793                assert self.nH[i] > 0, "nH has to be positive."
     794
     795        else:  # just one regulating species
     796            assert self.alpha >= 0, "The alpha parameter must be nonnegative."
     797            if self.alpha == 1:
     798                print("Warning: alpha=1 means the process is not regulated.")
     799
     800            if isinstance(self.K50, (float, int)):
     801                assert self.K50 > 0, "K50 has to be positive."
     802            elif isinstance(self.K50, list):
     803                assert all([True if val > 0 else False for val in self.K50]), \
     804                    "Subspecies K50 values have to be positive."
     805            else:  # isinstance(self.K50, tuple)
     806                assert self.K50[0] > 0 and self.K50[1] > 0, \
     807                    "Mean and std of K50 have to be positive."
     808
     809            if self.order == 0:
     810                assert not self.is_heterogeneous_K50, \
     811                    "Heterogeneity in parameter K50 is not allowed for a 0th order process."
     812
     813            assert self.nH > 0, "nH has to be positive."
     814
     815    @classmethod
     816    def from_string(cls,
     817                    proc_str: str,
     818                    /,
     819                    k: float | int | list[float, ...] | tuple[float, float],
     820                    *,
     821                    regulating_species: str | list[str, ...] = None,
     822                    alpha: float | int | list[float | int, ...] = 1,
     823                    K50: float | int | list[float | int, ...] | tuple[float | int, float | int] |
     824                         list[float | int | list[float | int, ...] | tuple[
     825                             float | int, float | int]] = None,
     826                    nH: float | int | list[float | int, ...] = None,
     827                    volume: float | None = None,
     828                    sep: str = '->') -> Self:
     829        """ Create a regulated process from a string.
     830
     831        Parameters
     832        ----------
     833        proc_str : str
     834            A string describing the process in standard chemical notation
     835            (e.g., 'A + B -> C')
     836        k : float or int or list of floats or 2-tuple of floats
     837            The *microscopic* rate constant for the given process. It is the *basal*
     838            rate constant in the case of activation (or the minimum `k` value)
     839            and the maximum rate constant in the case of repression.
     840            If `k` is a float or int, then the process is homogeneous.
     841            If `k` is a list, then the population of the reactants
     842            constsists of distinct subspecies or subinteractions
     843            depending on the order. If `k` is a 2-tuple,
     844            then the constant is normally-distributed with a mean and standard
     845            deviation specified in the tuple's elements. Note that `k` cannot
     846            be zero for this form of regulation.
     847        regulating_species : str or list of str
     848            Name of the regulating species.
     849        alpha : float or int or list[float or int]
     850            Parameter denoting the degree of activation/repression.
     851
     852                - 0 <= alpha < 1: repression
     853                - alpha = 1: no regulation
     854                - alpha > 1: activation
     855
     856            alpha is a multiplier: in the case of activation, the maximum
     857            rate constant value will be `alpha * k`.
     858            In the case of repression, the minimum
     859            rate constant value will be `alpha * k`.
     860        K50 : float or int or list of floats or 2-tuple of floats or list of each of the previous types
     861            *Microscopic* constant that corresponds to the number of
     862            `regulating_species` agents that would produce
     863            half-maximal activation/repression.
     864            Heterogeneity in this parameter is determined by the type of `K50`,
     865            using the same rules as for parameter `k`.
     866        nH : float or int or list[float or int]
     867            Hill coefficient for the given process. Indicates the degree of
     868            cooperativity in the regulatory interaction.
     869        volume : float, default : None, optional
     870            The volume *in liters* of the compartment in which the processes
     871            are taking place.
     872        sep : str, default: '->'
     873            Specifies the characters that distinguish the reactants from the
     874            products. The default is '->'. The code also treats `-->` as a
     875            default, if it's present in `proc_str`.
     876
     877        Notes
     878        -----
     879        - Species names should not contain spaces, dashes, and
     880          should start with a non-numeric character.
     881        - Zeroth order processes should be specified by an empty space or 'None'.
     882
     883        Examples
     884        --------
     885        >>> RegulatedProcess.from_string("A -> X", k=0.2, regulating_species='X', alpha=2, K50=10, nH=1)
     886        >>> RegulatedProcess.from_string("A -> X", k=0.3, regulating_species='X', alpha=0.5, K50=[10, 15], nH=2)
     887        >>> RegulatedProcess.from_string("A + B -> X", k=0.5, regulating_species='B, X', alpha=[2, 0], K50=[(15, 5), [10, 15]], nH=[1, 2])
     888        """
     889        sep = '-->' if '-->' in proc_str else sep
     890        if sep not in proc_str:
     891            raise Exception("Cannot distinguish the reactants from the products.\n"
     892                            "Please use the *sep* keyword: e.g. sep='->'.")
     893
     894        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
     895        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
     896        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side
     897
     898        return cls(reactants=cls._to_dict(lhs_terms),
     899                   products=cls._to_dict(rhs_terms),
     900                   k=k,
     901                   regulating_species=regulating_species,
     902                   alpha=alpha,
     903                   K50=K50,
     904                   nH=nH,
     905                   volume=volume)
     906
     907    def __repr__(self):
     908        repr_k = macro_to_micro(self.k, self.volume, self.order, inverse=True) if self.volume is not None else self.k
     909        repr_K50 = macro_to_micro(self.K50, self.volume, inverse=True) if self.volume is not None else self.K50
     910        return f"RegulatedProcess Object: " \
     911               f"RegulatedProcess.from_string('{self._str.split(',')[0]}', " \
     912               f"k={repr_k}, " \
     913               f"regulating_species='{self.regulating_species}', " \
     914               f"alpha={self.alpha}, " \
     915               f"K50={repr_K50}, " \
     916               f"nH={self.nH}," \
     917               f"volume={self.volume})"
     918
     919    def __str__(self):
     920        if isinstance(self.regulating_species, list):
     921            K50_het_str = ""
     922            for i, sp in enumerate(self.regulating_species):
     923                if isinstance(self.K50[i], (float, int)):
     924                    K50_het_str += f"Homogeneous process with respect to species {sp} K50. "
     925                elif isinstance(self.K50[i], list):
     926                    K50_het_str += f"Heterogeneous process with respect to species {sp} K50 " \
     927                                   f"with {len(self.K50[i])} distinct subspecies. "
     928                else:
     929                    K50_het_str += f"Heterogeneous process with normally-distributed " \
     930                                   f"species {sp} K50 with mean {self.K50[i][0]} and " \
     931                                   f"standard deviation {self.K50[i][1]}. "
     932        else:
     933            if isinstance(self.K50, (float, int)):
     934                K50_het_str = "Homogeneous process with respect to K50."
     935            elif isinstance(self.K50, list):
     936                K50_het_str = f"Heterogeneous process with respect to K50 " \
     937                              f"with {len(self.K50)} distinct subspecies."
     938            else:
     939                K50_het_str = f"Heterogeneous process with normally-distributed K50 with " \
     940                              f"mean {self.K50[0]} and standard deviation {self.K50[1]}."
     941
     942        return super().__str__() + f" Regulating Species: {self.regulating_species}, " \
     943                                   f"alpha = {self.alpha}, nH = {self.nH}, " \
     944                                   f"K50 = {self.K50}, {K50_het_str}"
     945
     946    def __eq__(self, other):
     947        if isinstance(other, RegulatedProcess):
     948            is_equal = (self.k == other.k and
     949                        self.order == other.order and
     950                        self.reactants == other.reactants and
     951                        self.products == other.products and
     952                        self.regulating_species == other.regulating_species and
     953                        self.alpha == other.alpha and
     954                        self.K50 == other.K50 and
     955                        self.nH == other.nH and
     956                        self.species == other.species and
     957                        self.volume == other.volume)
     958            return is_equal
     959        elif isinstance(other, str):
     960            return self._str == other or self._str.replace(' ', '') == other
     961        else:
     962            print(f"{type(self)} and {type(other)} are instances of different classes.")
     963            return False
     964
     965    def __hash__(self):
     966        return hash(self._str)
     967
     968
     969class RegulatedMichaelisMentenProcess(RegulatedProcess):
     970    """ Define a process that is regulated and obeys Michaelis-Menten kinetics.
     971
     972    This class allows a Michaelis-Menten Process to be defined
     973    in terms of how it is regulated.
     974    If there is only one regulating species, then the parameters have the same
     975    type as would be expected for a homogeneous/heterogeneous process.
     976    If there are multiple regulating species, then all parameters are a list
     977    of their expected type, with the length of the list being equal to the
     978    number of regulating species.
     979
     980    The class-specific attributes (except for `k`, which requires some
     981    additional notes) are listed below.
     982
     983    Attributes
     984    ----------
     985    k : float or int or list of floats or 2-tuple of floats
     986        The *microscopic* rate constant for the given process. It is the *basal*
     987        rate constant in the case of activation (or the minimum `k` value)
     988        and the maximum rate constant in the case of repression.
     989    regulating_species : str or list of str
     990        Name of the regulating species. Multiple species can be specified as
     991        comma-separated in a string or a list of strings with the species names.
     992    alpha : float or int or list[float or int]
     993        Parameter denoting the degree of activation/repression.
     994
     995            - 0 <= alpha < 1: repression
     996            - alpha = 1: no regulation
     997            - alpha > 1: activation
     998
     999        alpha is a multiplier: in the case of activation, the maximum
    1000        rate constant value will be `alpha * k`.
    1001        In the case of repression, the minimum
    1002        rate constant value will be `alpha * k`.
    1003    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]
    1004        *Microscopic* constant that corresponds to the number of
    1005        `regulating_species` agents that would produce
    1006        half-maximal activation/repression.
    1007        Heterogeneity in this parameter is determined by the type of `K50`,
    1008        using the same rules as for parameter `k`.
    1009    nH : float or int or list[float or int]
    1010        Hill coefficient for the given process. Indicates the degree of
    1011        cooperativity in the regulatory interaction.
    1012    is_heterogeneous_K50 : bool or list of bool
    1013        Denotes if the parameter `K50` exhibits heterogeneity
    1014        (distinct subspecies/interactions or normally-distributed).
    1015    regulation_type : str or list of str
    1016        The type of regulation for this process based on the value of alpha:
    1017        'activation' or 'repression' or 'no regulation'.
    1018    catalyst : str
    1019        Name of the species acting as a catalyst for this process.
    1020    Km : float or int or list of floats or 2-tuple of floats
    1021        *Microscopic* Michaelis constant. Corresponds to the number
    1022        of `catalyst` agents that would produce half-maximal activity.
    1023        Heterogeneity in this parameter is determined by the type of `K50`,
    1024        using the same rules as for parameter `k`.
    1025    is_heterogeneous_Km : bool
    1026        Denotes if the parameter `Km` exhibits heterogeneity
    1027        (distinct subspecies/interactions or normally-distributed).
    1028
    1029    Notes
    1030    -----
    1031    Currently only implemented for 1st order processes. 0th order processes
    1032    cannot obey Michaelis-Menten kinetics and 2nd order Michaelis-Menten
    1033    processes are not implemented yet.
    1034    """
    1035
    1036    def __init__(self,
    1037                 /,
    1038                 reactants: dict[str, int],
    1039                 products: dict[str, int],
    1040                 k: float | int | list[float, ...] | tuple[float, float],
    1041                 *,
    1042                 regulating_species: str | list[str, ...],
    1043                 alpha: float | int | list[float | int, ...],
    1044                 K50: float | int | list[float | int, ...] | tuple[float | int, float | int] |
    1045                      list[float | int | list[float | int, ...] | tuple[float | int, float | int]],
    1046                 nH: float | int | list[float | int, ...],
    1047                 catalyst: str,
    1048                 Km: float | int | list[float | int, ...] | tuple[float | int, float | int],
    1049                 volume: float | None = None):
    1050
    1051        self.catalyst = catalyst
    1052        self.Km = Km
    1053        self.is_heterogeneous_Km = False if isinstance(self.Km, (int, float)) else True
    1054
    1055        super().__init__(reactants=reactants,
    1056                         products=products,
    1057                         k=k,
    1058                         regulating_species=regulating_species,
    1059                         alpha=alpha,
    1060                         K50=K50,
    1061                         nH=nH,
    1062                         volume=volume)
    1063
    1064        super()._validate_reg_params()
    1065
    1066        assert self.order != 0, "A 0th order process has no substrate for a catalyst " \
    1067                                "to act on, therefore it cannot follow Michaelis-Menten kinetics."
    1068        if self.order == 2:
    1069            raise NotImplementedError
    1070
    1071        if self.volume is not None:  # Convert macroscopic to microscopic Km value
    1072            self.Km = macro_to_micro(Km, self.volume)
    1073
    1074        self.species.add(self.catalyst)
    1075        self._str += f", catalyst = {self.catalyst}, Km = {self.Km}"
    1076
    1077    @classmethod
    1078    def from_string(cls,
    1079                    proc_str: str,
    1080                    /,
    1081                    k: float | int | list[float, ...] | tuple[float, float],
    1082                    *,
    1083                    regulating_species: str | list[str, ...] = None,
    1084                    alpha: float | int | list[float | int, ...] = 1,
    1085                    K50: float | int | list[float | int, ...] | tuple[float | int, float | int] |
    1086                         list[float | int | list[float | int, ...] | tuple[
    1087                             float | int, float | int]] = None,
    1088                    nH: float | int | list[float | int, ...] = None,
    1089                    catalyst: str = None,
    1090                    Km: float | int | list[float | int, ...] | tuple[
    1091                        float | int, float | int] = None,
    1092                    volume: float | None = None,
    1093                    sep: str = '->') -> Self:
    1094        """ Create a regulated Michaelis-Menten process from a string.
    1095
    1096        Parameters
    1097        ----------
    1098        proc_str : str
    1099            A string describing the process in standard chemical notation
    1100            (e.g., 'A + B -> C')
    1101        k : float or int or list of floats or 2-tuple of floats
    1102            The *microscopic* rate constant for the given process. It is the *basal*
    1103            rate constant in the case of activation (or the minimum `k` value)
    1104            and the maximum rate constant in the case of repression.
    1105            If `k` is a float or int, then the process is homogeneous.
    1106            If `k` is a list, then the population of the reactants
    1107            constsists of distinct subspecies or subinteractions
    1108            depending on the order. If `k` is a 2-tuple,
    1109            then the constant is normally-distributed with a mean and standard
    1110            deviation specified in the tuple's elements. Note that `k` cannot
    1111            be zero for this form of regulation.
    1112        regulating_species : str or list of str
    1113            Name of the regulating species.
    1114        alpha : float or int or list[float or int]
    1115            Parameter denoting the degree of activation/repression.
    1116
    1117            - 0 <= alpha < 1: repression
    1118            - alpha = 1: no regulation
    1119            - alpha > 1: activation
    1120
    1121            alpha is a multiplier: in the case of activation, the maximum
    1122            rate constant value will be `alpha * k`.
    1123            In the case of repression, the minimum
    1124            rate constant value will be `alpha * k`.
    1125        K50 : float or int or list of floats or 2-tuple of floats or list of each of the previous types
    1126            *Microscopic* constant that corresponds to the number of
    1127            `regulating_species` agents that would produce
    1128            half-maximal activation/repression.
    1129            Heterogeneity in this parameter is determined by the type of `K50`,
    1130            using the same rules as for parameter `k`.
    1131        nH : float or int or list[float or int]
    1132            Hill coefficient for the given process. Indicates the degree of
    1133            cooperativity in the regulatory interaction.
    1134        catalyst : str
    1135            Name of species acting as a catalyst.
    1136        Km : float or int or list of floats or 2-tuple of floats
    1137            *Microscopic* Michaelis constant for the process.
    1138            Heterogeneity in this parameter is determined by the type of `Km`,
    1139            using the same rules as for parameter `k`.
    1140        volume : float, default : None, optional
    1141            The volume *in liters* of the compartment in which the processes
    1142            are taking place.
    1143        sep : str, default: '->'
    1144            Specifies the characters that distinguish the reactants from the
    1145            products. The default is '->'. The code also treats `-->` as a
    1146            default, if it's present in `proc_str`.
    1147
    1148        Notes
    1149        -----
    1150        - Species names should not contain spaces, dashes, and
    1151          should start with a non-numeric character.
    1152        - Zeroth order processes should be specified by an empty space or 'None'.
    1153
    1154        Examples
    1155        --------
    1156        >>> RegulatedMichaelisMentenProcess.from_string("A -> X", k=0.2, regulating_species='X', alpha=2, K50=10, nH=1, catalyst='E', Km=15)
    1157        >>> RegulatedMichaelisMentenProcess.from_string("A -> X", k=0.3, regulating_species='A', alpha=0.5, K50=[10, 15], nH=2, catalyst='C', Km=5)
    1158        """
    1159        sep = '-->' if '-->' in proc_str else sep
    1160        if sep not in proc_str:
    1161            raise Exception("Cannot distinguish the reactants from the products.\n"
    1162                            "Please use the *sep* keyword: e.g. sep='->'.")
    1163
    1164        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
    1165        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
    1166        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side
    1167
    1168        return cls(reactants=cls._to_dict(lhs_terms),
    1169                   products=cls._to_dict(rhs_terms),
    1170                   k=k,
    1171                   regulating_species=regulating_species,
    1172                   alpha=alpha,
    1173                   K50=K50,
    1174                   nH=nH,
    1175                   catalyst=catalyst,
    1176                   Km=Km,
    1177                   volume=volume)
    1178
    1179    def __repr__(self):
    1180        repr_k = macro_to_micro(self.k, self.volume, self.order, inverse=True) if self.volume is not None else self.k
    1181        repr_K50 = macro_to_micro(self.K50, self.volume, inverse=True) if self.volume is not None else self.K50
    1182        repr_Km = macro_to_micro(self.Km, self.volume, inverse=True) if self.volume is not None else self.Km
    1183        return f"RegulatedMichaelisMentenProcess Object: " \
    1184               f"RegulatedMichaelisMentenProcess.from_string('{self._str.split(',')[0]}', " \
    1185               f"k={repr_k}, " \
    1186               f"regulating_species='{self.regulating_species}', " \
    1187               f"alpha={self.alpha}, " \
    1188               f"K50={repr_K50}, " \
    1189               f"nH={self.nH}, " \
    1190               f"catalyst={self.catalyst}, " \
    1191               f"Km={repr_Km}," \
    1192               f"volume={self.volume})"
    1193
    1194    def __str__(self):
    1195        if isinstance(self.regulating_species, list):
    1196            K50_het_str = ""
    1197            for i, sp in enumerate(self.regulating_species):
    1198                if isinstance(self.K50[i], (float, int)):
    1199                    K50_het_str += f"Homogeneous process with respect to species {sp} K50. "
    1200                elif isinstance(self.K50[i], list):
    1201                    K50_het_str += f"Heterogeneous process with respect to species {sp} K50 " \
    1202                                   f"with {len(self.K50[i])} distinct subspecies. "
    1203                else:
    1204                    K50_het_str += f"Heterogeneous process with normally-distributed " \
    1205                                   f"species {sp} K50 with mean {self.K50[i][0]} and " \
    1206                                   f"standard deviation {self.K50[i][1]}. "
    1207        else:
    1208            if isinstance(self.K50, (float, int)):
    1209                K50_het_str = "Homogeneous process with respect to K50."
    1210            elif isinstance(self.K50, list):
    1211                K50_het_str = f"Heterogeneous process with respect to K50 " \
    1212                              f"with {len(self.K50)} distinct subspecies."
    1213            else:
    1214                K50_het_str = f"Heterogeneous process with normally-distributed K50 with " \
    1215                              f"mean {self.K50[0]} and standard deviation {self.K50[1]}."
    1216
    1217        if isinstance(self.Km, (float, int)):
    1218            Km_het_str = "Homogeneous process with respect to Km."
    1219        elif isinstance(self.k, list):
    1220            Km_het_str = f"Heterogeneous process with respect to Km " \
    1221                         f"with {len(self.Km)} distinct subspecies."
    1222        else:
    1223            Km_het_str = f"Heterogeneous process with normally-distributed Km with " \
    1224                         f"mean {self.Km[0]} and standard deviation {self.Km[1]}."
    1225
    1226        return super().__str__() + f" Regulating Species: {self.regulating_species}, " \
    1227                                   f"alpha = {self.alpha}, nH = {self.nH}, " \
    1228                                   f"K50 = {self.K50}, {K50_het_str}, " \
    1229                                   f"Catalyst: {self.catalyst}, " \
    1230                                   f"Km = {self.Km}, {Km_het_str}"
    1231
    1232    def __eq__(self, other):
    1233        if isinstance(other, RegulatedMichaelisMentenProcess):
    1234            is_equal = (self.k == other.k and
    1235                        self.order == other.order and
    1236                        self.reactants == other.reactants and
    1237                        self.products == other.products and
    1238                        self.regulating_species == other.regulating_species and
    1239                        self.alpha == other.alpha and
    1240                        self.K50 == other.K50 and
    1241                        self.nH == other.nH and
    1242                        self.catalyst == other.catalyst and
    1243                        self.Km == other.Km and
    1244                        self.species == other.species and
    1245                        self.volume == other.volume)
    1246            return is_equal
    1247        elif isinstance(other, str):
    1248            return self._str == other or self._str.replace(' ', '') == other
    1249        else:
    1250            print(f"{type(self)} and {type(other)} are instances of different classes.")
    1251            return False
    1252
    1253    def __hash__(self):
    1254        return hash(self._str)
    1255
    1256
    1257class NullSpeciesNameError(Exception):
    1258    """ Error when the species name is an empty string. """
    1259
    1260    def __str__(self):
    1261        return "A species name cannot be an empty string."
    1262
    1263
    1264def update_all_species(procs: tuple[Process, ...]) -> tuple[set, dict, dict]:
    1265    """ Categorize all species in a list of processes.
    1266
    1267    Extract all species from a list of processes. Then categorize each of them
    1268    as a reactant or product and list the process(es) it takes part in.
    1269
    1270    Parameters
    1271    ----------
    1272    procs : tuple
    1273        A tuple of objects of type `Process` or its subclasses.
    1274
    1275    Returns
    1276    -------
    1277    tuple
    1278        all_species : set of strings
    1279            A set of all species present in the processes.
    1280        procs_by_reactant : dict
    1281            A dictionary whose keys are the species that are
    1282            reactants in one or more processes. The value for each
    1283            key is a list of processes.
    1284        procs_by_product : dict
    1285            A dictionary whose keys are the species that are
    1286            products in one or more processes. The value for each
    1287            key is a list of processes.
    1288    """
    1289    procs = list(procs)
    1290    for proc in procs:
    1291        # For a reversible process, replace it with separate instances
    1292        # of Process objects representing the forward and reverse reactions.
    1293        if isinstance(proc, ReversibleProcess):
    1294            forward_proc = Process(proc.reactants, proc.products, proc.k)
    1295            reverse_proc = Process(proc.products, proc.reactants, proc.k_rev)
    1296            procs.remove(proc)
    1297            procs.extend([forward_proc, reverse_proc])
    1298
    1299    assert len(set(procs)) == len(procs), \
    1300        "WARNING: Duplicate processes found. Examine the list of processes to resolve this."
    1301
    1302    all_species, rspecies, pspecies = set(), set(), set()
    1303    procs_by_reactant, procs_by_product = dict(), dict()
    1304
    1305    for proc in procs:
    1306        all_species = all_species.union(proc.species)
    1307        rspecies = rspecies.union(proc.reactants)
    1308        pspecies = pspecies.union(proc.products)
    1309
    1310    # Make a list containing the processes each reactant species takes part in.
    1311    # This will be used when solving the system ODEs.
    1312    for rspec in rspecies:
    1313        if rspec != '':  # omit reactant species parsed from 0th order processes
    1314            procs_by_reactant[rspec] = [proc for proc in procs if rspec in proc.reactants]
    1315            # deleted 1st clause in above `if`: `rspec != '' and`
    1316
    1317    # Make a list containing the processes each product species takes part in.
    1318    # This will be used for solving the system ODEs.
    1319    for pspec in pspecies:
    1320        if pspec != '':  # omitting product species parsed from degradation processes
    1321            procs_by_product[pspec] = [proc for proc in procs if pspec in proc.products]
    1322
    1323    return all_species, procs_by_reactant, procs_by_product

class Process: View Source

     28class Process:
     29    """
     30    Define a unidirectional process: Reactants -> Products, where the
     31    Reactants and Products are specified using standard chemical notation.
     32    That is, stoichiometric coefficients (integers) and species names are
     33    specified. For example: 2A + B -> C.
     34
     35    Attributes
     36    ----------
     37    reactants : dict
     38        The reactants of a given process are specified with
     39        key-value pairs describing each species name and its
     40        stoichiometric coefficient, respectively.
     41    products : dict
     42        The products of a given process are specified with
     43        key-value pairs describing each species name and its
     44        stoichiometric coefficient, respectively.
     45    k : float, int, list of floats, tuple of floats
     46        The *microscopic* rate constant(s) for the given process. The data
     47        type of `k` determines the "structure" of the population as follows:
     48            - A homogeneous population: if `k` is a single value (float or int),
     49              then the population is assumed to be homogeneous with all agents
     50              of the reactant species having kinetics defined by this value.
     51            - A heterogeneous population with a distinct number of subspecies
     52              (each with a corresponding `k` value): if `k` is a list of floats,
     53              then the population is assumed to be heterogeneous with a number
     54              of subspecies equal to the length of the list.
     55            - A heterogeneous population with normally-distributed `k` values:
     56              If `k` is a tuple whose length is 2, then the population is
     57              assumed to be heterogeneous with a normally distributed `k` value.
     58              The two entries in the tuple represent the mean and standard
     59              deviation (in that order) of the desired normal distribution.
     60    volume : float, default : None, optional
     61        The volume *in liters* of the compartment in which the processes
     62        are taking place.
     63    order : int
     64        The order of the process (or the molecularity of an elementary process).
     65        It is the sum of the stoichiometric coefficients of the reactants.
     66    species : set of strings
     67        A set of all species in a process.
     68    reacts_ : list of strings
     69        A list containing all the reactants in a process.
     70    prods_ : list of strings
     71        A list containing all the products in a process.
     72
     73    Methods
     74    -------
     75    from_string
     76        Class method for creating a Process object from a string.
     77    """
     78
     79    def __init__(self,
     80                 /,
     81                 reactants: dict[str, int],
     82                 products: dict[str, int],
     83                 k: float | int | list[float, ...] | tuple[float, float],
     84                 *,
     85                 volume: float | None = None,
     86                 **kwargs):
     87
     88        self.reactants = reactants
     89        self.products = products
     90        self.k = k
     91        self.volume = volume
     92
     93        self._validate_nums()  # make sure there are no errors in given numbers
     94
     95        # For consistency with processes instantiated using the class method `from_string()`,
     96        # species denoted as 'None' for a 0th order process are renamed to ''.
     97        if 'None' in self.reactants.keys():
     98            self.reactants[''] = self.reactants.pop('None')
     99        if 'None' in self.products.keys():
    100            self.products[''] = self.products.pop('None')
    101
    102        self.order = sum(self.reactants.values())
    103
    104        self.is_heterogeneous = False if isinstance(self.k, (int, float)) else True
    105
    106        if self.order == 0:
    107            msg = "Since a birth process does not depended on the presence of agents, " \
    108                  "heterogeneity does not make sense in this context. Please define " \
    109                  "the rate constant k as a number. "
    110            assert not self.is_heterogeneous, msg
    111
    112        # Convert macroscopic to microscopic rate constant
    113        if self.volume is not None:
    114            self.k = macro_to_micro(self.k, self.volume, self.order)
    115
    116        # Two ways of storing the involved species:
    117        # 1) A set of all species
    118        self.species = set((self.reactants | self.products).keys())
    119        with contextlib.suppress(KeyError):
    120            self.species.remove('')  # remove empty species name from any 0th order processes
    121
    122        # 2) Separate lists
    123        self.reacts_ = list()  # [reactant species]
    124        self.prods_ = list()  # [product species]
    125        self._get_reacts_prods_()
    126
    127        """ Because Process objects are used as keys in dictionaries used
    128        in an AbStochKin simulation, it's much faster to generate the object's
    129        string representation once, and then access it whenever it's needed
    130        (which could be thousands of times during a simulation). """
    131        self._str = self.__str__().split(';')[0]
    132
    133        if len(kwargs) > 0:
    134            self._lsp(kwargs)
    135
    136    def _get_reacts_prods_(self):
    137        """ Make lists of the reactant and product species. Repeated
    138        elements of a list reflect the order or molecularity of the
    139        species in the given process. For example, for the process
    140        `2A + B -> C + D, reacts_ = ['A', 'A', 'B'], prods_ = ['C', 'D']`. """
    141        for r, m in self.reactants.items():
    142            for i in range(m):
    143                self.reacts_.append(r)
    144
    145        for p, m in self.products.items():
    146            for i in range(m):
    147                self.prods_.append(p)
    148
    149        if '' in self.reacts_:  # remove empty reactant species names
    150            self.reacts_.remove('')  # from 0th order processes
    151        if '' in self.prods_:  # remove empty product species names
    152            self.prods_.remove('')  # from degradation processes
    153
    154    @classmethod
    155    def from_string(cls,
    156                    proc_str: str,
    157                    /,
    158                    k: float | int | list[float, ...] | tuple[float, float],
    159                    *,
    160                    volume: float | None = None,
    161                    sep: str = '->',
    162                    **kwargs) -> Self:
    163        """ Create a process from a string.
    164
    165        Parameters
    166        ----------
    167        proc_str : str
    168            A string describing the process in standard chemical notation
    169            (e.g., 'A + B -> C')
    170        k : float or int or list of floats or 2-tuple of floats
    171            The rate constant for the given process. If `k` is a float or
    172            int, then the process is homogeneous. If `k` is a list, then
    173            the population of the reactants constsists of distinct subspecies
    174            or subinteractions depending on the order. If `k` is a 2-tuple,
    175            then the constant is normally-distributed with a mean and standard
    176            deviation specified in the tuple's elements.
    177        volume : float, default : None, optional
    178            The volume *in liters* of the compartment in which the processes
    179            are taking place.
    180        sep : str, default: '->'
    181            Specifies the characters that distinguish the reactants from the
    182            products. The default is '->'. The code also treats `-->` as a
    183            default, if it's present in `proc_str`.
    184
    185        Notes
    186        -----
    187        - Species names should not contain spaces, dashes, and
    188          should start with a non-numeric character.
    189        - Zeroth order processes should be specified by an empty space or 'None'.
    190
    191        Examples
    192        --------
    193        >>> Process.from_string("2A + B -> X", 0.3)
    194        >>> Process.from_string(" -> Y", 0.1)  # for a 0th order (birth) process.
    195        >>> Process.from_string("Protein_X -> None", 0.15)  # for a 1st order degradation process.
    196        """
    197
    198        if len(kwargs) > 0:
    199            cls._lsp(kwargs)
    200
    201        sep = '-->' if '-->' in proc_str else sep
    202        if sep not in proc_str:
    203            raise Exception("Cannot distinguish the reactants from the products.\n"
    204                            "Please use the *sep* keyword: e.g. sep='->'.")
    205
    206        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
    207        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
    208        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side
    209
    210        return cls(reactants=cls._to_dict(lhs_terms),
    211                   products=cls._to_dict(rhs_terms),
    212                   k=k,
    213                   volume=volume)
    214
    215    @staticmethod
    216    def _lsp(kwargs: dict):
    217        """
    218        The `Process` class accepts additional arguments (`**kwargs`).
    219        Since the Process class is a base class for other subclasses,
    220        this is done so that the Liskov Substitution Principle (LSP)
    221        is not violated.
    222        (https://en.wikipedia.org/wiki/Liskov_substitution_principle).
    223        This way, subclasses override the `from_string` method and have
    224        additional parameters while remaining consistent with this method
    225        from their base class. Calling the base instance with the additional
    226        parameters gives a warning that they will have no effect so that
    227        the user can intervene, if that's desired.
    228        """
    229        msg = f"Warning: Additional parameters {','.join([str(i) for i in kwargs.items()])} " \
    230              f"will have no effect. "
    231        if 'k_rev' in kwargs.keys():
    232            msg += f"If that's not what you intended, define the process " \
    233                   f"using ReversibleProcess()."
    234        if 'regulating_species' in kwargs.keys() or 'alpha' in kwargs.keys() or \
    235                'nH' in kwargs.keys() or 'K50' in kwargs.keys():
    236            msg += f"If that's not what you intended, define the process " \
    237                   f"using RegulatedProcess()."
    238        if 'catalyst' in kwargs.keys():
    239            msg += f"If that's not what you intended, define the process " \
    240                   f"using MichaelisMentenProcess()."
    241        print(msg)
    242
    243    @staticmethod
    244    def _to_dict(terms: list) -> dict:
    245        """ Convert the information for a side (left, right) of a process
    246        to a dictionary. """
    247        side_terms = dict()  # for storing the information of a side of a process
    248        patt = '^[\\-]*[1-9]+'  # regex pattern (accounts for leading erroneous minus sign)
    249
    250        if len(terms) == 1 and terms[0].strip().lower() in ['', 'none']:
    251            spec = ''  # Zeroth order process
    252            side_terms[spec] = 0
    253        else:
    254            for term in terms:
    255                term = term.strip()
    256                try:
    257                    match = re.search(patt, term)
    258                    stoic_coef = term[slice(*match.span())]  # extract stoichiometric coef
    259                    spec = re.split(patt, term)[-1].strip()  # extract species name
    260                    if spec == '' and stoic_coef != 0:
    261                        raise NullSpeciesNameError()
    262                    stoic_coef = int(stoic_coef)
    263                except AttributeError:  # when there is no specified stoichiometric coefficient
    264                    spec = re.split(patt, term)[-1]  # extract species name
    265                    stoic_coef = 1
    266
    267                if spec not in side_terms.keys():
    268                    side_terms[spec] = stoic_coef
    269                else:
    270                    side_terms[spec] += stoic_coef
    271
    272        return side_terms
    273
    274    def _validate_nums(self):
    275        """ Make sure coefficients, rate constant, and volume values are not negative. """
    276        # Check coefficients
    277        for r, val in (self.reactants | self.products).items():
    278            assert val >= 0, f"Coefficient cannot be negative: {val} {r}."
    279
    280        # Check rate constants
    281        error_msg = f"Rate constant values have to be positive: k = {self.k}."
    282        if isinstance(self.k, (list, tuple)):  # heterogeneous population
    283            assert all(array(self.k) > 0), error_msg
    284        else:  # when k is a float or int, the population is homogeneous
    285            assert self.k > 0, error_msg
    286
    287        # For normally-distributed k values, specification is a 2-tuple.
    288        if isinstance(self.k, tuple):  # normal distribution of k values
    289            assert len(self.k) == 2, "Please specify the mean and standard deviation " \
    290                                     "of k in a 2-tuple: (mean, std)."
    291
    292        # Check volume
    293        if self.volume is not None:
    294            assert self.volume > 0, f"Volume cannot be negative: {self.volume}."
    295
    296    def __eq__(self, other):
    297        if isinstance(other, Process):
    298            is_equal = (self.k == other.k and
    299                        self.order == other.order and
    300                        self.reactants == other.reactants and
    301                        self.products == other.products and
    302                        self.species == other.species and
    303                        self.volume == other.volume)
    304            return is_equal
    305        elif isinstance(other, str):
    306            return self._str == other or self._str.replace(' ', '') == other
    307        else:
    308            print(f"{type(self)} and {type(other)} are instances of different classes.")
    309            return False
    310
    311    def __hash__(self):
    312        return hash(self._str)
    313
    314    def __contains__(self, item):
    315        return True if item in self.species else False
    316
    317    def __repr__(self):
    318        repr_k = macro_to_micro(self.k, self.volume, self.order, inverse=True) if self.volume is not None else self.k
    319        return f"Process Object: Process.from_string('{self._str.split(',')[0]}', " \
    320               f"k={repr_k}, " \
    321               f"volume={self.volume})"
    322
    323    def __str__(self):
    324        if isinstance(self.k, (float, int)):
    325            het_str = "Homogeneous process."
    326        elif isinstance(self.k, list):
    327            het_str = f"Heterogeneous process with {len(self.k)} distinct subspecies."
    328        else:
    329            het_str = f"Heterogeneous process with normally-distributed k with " \
    330                      f"mean {self.k[0]} and standard deviation {self.k[1]}."
    331
    332        lhs, rhs = self._reconstruct_string()
    333
    334        vol_str = f", volume = {self.volume} L" if self.volume is not None else ""
    335        return ' -> '.join([lhs, rhs]) + f', k = {self.k}{vol_str}; {het_str}'
    336
    337    def _reconstruct_string(self):
    338        lhs = ' + '.join([f"{str(val) + ' ' if val not in [0, 1] else ''}{key}" for key, val in
    339                          self.reactants.items()])
    340        rhs = ' + '.join([f"{str(val) + ' ' if val not in [0, 1] else ''}{key}" for key, val in
    341                          self.products.items()])
    342        return lhs, rhs

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.
- volume : float, default (None, optional): The volume in liters of the compartment in which the processes are taking place.
- 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.

Process( reactants: dict[str, int], products: dict[str, int], k: float | int | list[float, ...] | tuple[float, float], *, volume: float | None = None, **kwargs) View Source

     79    def __init__(self,
     80                 /,
     81                 reactants: dict[str, int],
     82                 products: dict[str, int],
     83                 k: float | int | list[float, ...] | tuple[float, float],
     84                 *,
     85                 volume: float | None = None,
     86                 **kwargs):
     87
     88        self.reactants = reactants
     89        self.products = products
     90        self.k = k
     91        self.volume = volume
     92
     93        self._validate_nums()  # make sure there are no errors in given numbers
     94
     95        # For consistency with processes instantiated using the class method `from_string()`,
     96        # species denoted as 'None' for a 0th order process are renamed to ''.
     97        if 'None' in self.reactants.keys():
     98            self.reactants[''] = self.reactants.pop('None')
     99        if 'None' in self.products.keys():
    100            self.products[''] = self.products.pop('None')
    101
    102        self.order = sum(self.reactants.values())
    103
    104        self.is_heterogeneous = False if isinstance(self.k, (int, float)) else True
    105
    106        if self.order == 0:
    107            msg = "Since a birth process does not depended on the presence of agents, " \
    108                  "heterogeneity does not make sense in this context. Please define " \
    109                  "the rate constant k as a number. "
    110            assert not self.is_heterogeneous, msg
    111
    112        # Convert macroscopic to microscopic rate constant
    113        if self.volume is not None:
    114            self.k = macro_to_micro(self.k, self.volume, self.order)
    115
    116        # Two ways of storing the involved species:
    117        # 1) A set of all species
    118        self.species = set((self.reactants | self.products).keys())
    119        with contextlib.suppress(KeyError):
    120            self.species.remove('')  # remove empty species name from any 0th order processes
    121
    122        # 2) Separate lists
    123        self.reacts_ = list()  # [reactant species]
    124        self.prods_ = list()  # [product species]
    125        self._get_reacts_prods_()
    126
    127        """ Because Process objects are used as keys in dictionaries used
    128        in an AbStochKin simulation, it's much faster to generate the object's
    129        string representation once, and then access it whenever it's needed
    130        (which could be thousands of times during a simulation). """
    131        self._str = self.__str__().split(';')[0]
    132
    133        if len(kwargs) > 0:
    134            self._lsp(kwargs)

reactants
products
k
volume
order
is_heterogeneous
species
reacts_
prods_
@classmethod
def from_string( cls, proc_str: str, /, k: float | int | list[float, ...] | tuple[float, float], *, volume: float | None = None, sep: str = '->', **kwargs) -> Self: View Source

    154    @classmethod
    155    def from_string(cls,
    156                    proc_str: str,
    157                    /,
    158                    k: float | int | list[float, ...] | tuple[float, float],
    159                    *,
    160                    volume: float | None = None,
    161                    sep: str = '->',
    162                    **kwargs) -> Self:
    163        """ Create a process from a string.
    164
    165        Parameters
    166        ----------
    167        proc_str : str
    168            A string describing the process in standard chemical notation
    169            (e.g., 'A + B -> C')
    170        k : float or int or list of floats or 2-tuple of floats
    171            The rate constant for the given process. If `k` is a float or
    172            int, then the process is homogeneous. If `k` is a list, then
    173            the population of the reactants constsists of distinct subspecies
    174            or subinteractions depending on the order. If `k` is a 2-tuple,
    175            then the constant is normally-distributed with a mean and standard
    176            deviation specified in the tuple's elements.
    177        volume : float, default : None, optional
    178            The volume *in liters* of the compartment in which the processes
    179            are taking place.
    180        sep : str, default: '->'
    181            Specifies the characters that distinguish the reactants from the
    182            products. The default is '->'. The code also treats `-->` as a
    183            default, if it's present in `proc_str`.
    184
    185        Notes
    186        -----
    187        - Species names should not contain spaces, dashes, and
    188          should start with a non-numeric character.
    189        - Zeroth order processes should be specified by an empty space or 'None'.
    190
    191        Examples
    192        --------
    193        >>> Process.from_string("2A + B -> X", 0.3)
    194        >>> Process.from_string(" -> Y", 0.1)  # for a 0th order (birth) process.
    195        >>> Process.from_string("Protein_X -> None", 0.15)  # for a 1st order degradation process.
    196        """
    197
    198        if len(kwargs) > 0:
    199            cls._lsp(kwargs)
    200
    201        sep = '-->' if '-->' in proc_str else sep
    202        if sep not in proc_str:
    203            raise Exception("Cannot distinguish the reactants from the products.\n"
    204                            "Please use the *sep* keyword: e.g. sep='->'.")
    205
    206        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
    207        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
    208        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side
    209
    210        return cls(reactants=cls._to_dict(lhs_terms),
    211                   products=cls._to_dict(rhs_terms),
    212                   k=k,
    213                   volume=volume)

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.
- volume : float, default (None, optional): The volume in liters of the compartment in which the processes are taking place.
- 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.

class ReversibleProcess(Process): View Source

    345class ReversibleProcess(Process):
    346    """ Define a reversible process.
    347
    348    The class-specific attributes are listed below.
    349
    350    Attributes
    351    ----------
    352    k_rev : float or int or list of floats or 2-tuple of floats
    353        The *microscopic* rate constant for the reverse process.
    354    is_heterogeneous_rev : bool
    355        Denotes if the parameter `k_rev` exhibits heterogeneity
    356        (distinct subspecies/interactions or normally-distributed).
    357
    358    Notes
    359    -----
    360    A `ReversibleProcess` object gets split into two `Process` objects
    361    (forward and reverse process) when the algorithm runs.
    362    """
    363
    364    def __init__(self,
    365                 /,
    366                 reactants: dict[str, int],
    367                 products: dict[str, int],
    368                 k: float | int | list[float, ...] | tuple[float, float],
    369                 k_rev: float | int | list[float, ...] | tuple[float, float],
    370                 *,
    371                 volume: float | None = None):
    372
    373        self.k_rev = k_rev  # rate constant for reverse process
    374
    375        super().__init__(reactants=reactants,
    376                         products=products,
    377                         k=k,
    378                         volume=volume)
    379
    380        self.is_heterogeneous_rev = False if isinstance(self.k_rev, (int, float)) else True
    381        self.order_rev = sum(self.products.values())
    382
    383        if self.volume is not None:  # Convert macroscopic to microscopic rate constants
    384            self.k_rev = macro_to_micro(k_rev, self.volume, self.order_rev)
    385
    386    @classmethod
    387    def from_string(cls,
    388                    proc_str: str,
    389                    /,
    390                    k: float | int | list[float, ...] | tuple[float, float],
    391                    *,
    392                    k_rev: float | int | list[float, ...] | tuple[float, float] = 0,
    393                    volume: float | None = None,
    394                    sep: str = '<->') -> Self:
    395        """ Create a reversible process from a string.
    396
    397        Parameters
    398        ----------
    399        proc_str : str
    400            A string describing the process in standard chemical notation
    401            (e.g., 'A + B <-> C')
    402        k : float or int or list of floats or 2-tuple of floats
    403            The *microscopic* rate constant for the forward process.
    404        k_rev : float or int or list of floats or 2-tuple of floats
    405            The *microscopic* rate constant for the reverse process.
    406        volume : float, default : None, optional
    407            The volume *in liters* of the compartment in which the processes
    408            are taking place.
    409        sep : str, default: '<->'
    410            Specifies the characters that distinguish the reactants from the
    411            products. The default is '<->'. The code also treats `<-->` as a
    412            default, if it's present in `proc_str`.
    413
    414        Notes
    415        -----
    416        - Species names should not contain spaces, dashes, and
    417          should start with a non-numeric character.
    418
    419        Examples
    420        --------
    421        >>> ReversibleProcess.from_string("2A + B <-> X", 0.3, k_rev=0.2)
    422        """
    423        for s in ['<-->', '<=>', '<==>']:
    424            sep = s if s in proc_str else sep
    425        if sep not in proc_str:
    426            raise Exception("Cannot distinguish the reactants from the products.\n"
    427                            "Please use the *sep* keyword: e.g. sep='<->'.")
    428
    429        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
    430        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
    431        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side
    432
    433        return cls(reactants=cls._to_dict(lhs_terms),
    434                   products=cls._to_dict(rhs_terms),
    435                   k=k,
    436                   k_rev=k_rev,
    437                   volume=volume)
    438
    439    def __repr__(self):
    440        repr_k = macro_to_micro(self.k, self.volume, self.order, inverse=True) if self.volume is not None else self.k
    441        repr_k_rev = macro_to_micro(self.k_rev, self.volume, self.order_rev,
    442                                    inverse=True) if self.volume is not None else self.k_rev
    443        return f"ReversibleProcess Object: ReversibleProcess.from_string(" \
    444               f"'{self._str.split(',')[0]}', " \
    445               f"k={repr_k}, " \
    446               f"k_rev={repr_k_rev}, " \
    447               f"volume={self.volume})"
    448
    449    def __str__(self):
    450        if isinstance(self.k, (float, int)):
    451            het_str = "Forward homogeneous process."
    452        elif isinstance(self.k, list):
    453            het_str = f"Forward heterogeneous process with {len(self.k)} " \
    454                      f"distinct subspecies."
    455        else:
    456            het_str = f"Forward heterogeneous process with normally-distributed " \
    457                      f"k with mean {self.k[0]} and standard deviation {self.k[1]}."
    458
    459        if isinstance(self.k_rev, (float, int)):
    460            het_rev_str = "Reverse homogeneous process."
    461        elif isinstance(self.k_rev, list):
    462            het_rev_str = f"Reverse heterogeneous process with {len(self.k_rev)} " \
    463                          f"distinct subspecies."
    464        else:
    465            het_rev_str = f"Reverse heterogeneous process with normally-distributed " \
    466                          f"k with mean {self.k_rev[0]} and standard deviation {self.k_rev[1]}."
    467
    468        lhs, rhs = self._reconstruct_string()
    469        vol_str = f", volume = {self.volume} L" if self.volume is not None else ""
    470        return " <-> ".join([lhs, rhs]) + f", k = {self.k}, k_rev = {self.k_rev}{vol_str}; " \
    471                                          f"{het_str} {het_rev_str}"
    472
    473    def _reconstruct_string(self):
    474        lhs = ' + '.join([f"{str(val) + ' ' if val not in [0, 1] else ''}{key}" for key, val in
    475                          self.reactants.items()])
    476        rhs = ' + '.join([f"{str(val) + ' ' if val not in [0, 1] else ''}{key}" for key, val in
    477                          self.products.items()])
    478        return lhs, rhs
    479
    480    def __eq__(self, other):
    481        if isinstance(other, ReversibleProcess):
    482            is_equal = (self.k == other.k and
    483                        self.order == other.order and
    484                        self.k_rev == other.k_rev and
    485                        self.order_rev == other.order_rev and
    486                        self.reactants == other.reactants and
    487                        self.products == other.products and
    488                        self.species == other.species and
    489                        self.volume == other.volume)
    490            return is_equal
    491        elif isinstance(other, str):
    492            return self._str == other or self._str.replace(' ', '') == other
    493        else:
    494            print(f"{type(self)} and {type(other)} are instances of different classes.")
    495            return False
    496
    497    def __hash__(self):
    498        return hash(self._str)

Define a reversible process.

The class-specific attributes are listed below.

###### 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.

ReversibleProcess( reactants: dict[str, int], products: dict[str, int], k: float | int | list[float, ...] | tuple[float, float], k_rev: float | int | list[float, ...] | tuple[float, float], *, volume: float | None = None) View Source

    364    def __init__(self,
    365                 /,
    366                 reactants: dict[str, int],
    367                 products: dict[str, int],
    368                 k: float | int | list[float, ...] | tuple[float, float],
    369                 k_rev: float | int | list[float, ...] | tuple[float, float],
    370                 *,
    371                 volume: float | None = None):
    372
    373        self.k_rev = k_rev  # rate constant for reverse process
    374
    375        super().__init__(reactants=reactants,
    376                         products=products,
    377                         k=k,
    378                         volume=volume)
    379
    380        self.is_heterogeneous_rev = False if isinstance(self.k_rev, (int, float)) else True
    381        self.order_rev = sum(self.products.values())
    382
    383        if self.volume is not None:  # Convert macroscopic to microscopic rate constants
    384            self.k_rev = macro_to_micro(k_rev, self.volume, self.order_rev)

k_rev
is_heterogeneous_rev
order_rev
@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, volume: float | None = None, sep: str = '<->') -> Self: View Source

    386    @classmethod
    387    def from_string(cls,
    388                    proc_str: str,
    389                    /,
    390                    k: float | int | list[float, ...] | tuple[float, float],
    391                    *,
    392                    k_rev: float | int | list[float, ...] | tuple[float, float] = 0,
    393                    volume: float | None = None,
    394                    sep: str = '<->') -> Self:
    395        """ Create a reversible process from a string.
    396
    397        Parameters
    398        ----------
    399        proc_str : str
    400            A string describing the process in standard chemical notation
    401            (e.g., 'A + B <-> C')
    402        k : float or int or list of floats or 2-tuple of floats
    403            The *microscopic* rate constant for the forward process.
    404        k_rev : float or int or list of floats or 2-tuple of floats
    405            The *microscopic* rate constant for the reverse process.
    406        volume : float, default : None, optional
    407            The volume *in liters* of the compartment in which the processes
    408            are taking place.
    409        sep : str, default: '<->'
    410            Specifies the characters that distinguish the reactants from the
    411            products. The default is '<->'. The code also treats `<-->` as a
    412            default, if it's present in `proc_str`.
    413
    414        Notes
    415        -----
    416        - Species names should not contain spaces, dashes, and
    417          should start with a non-numeric character.
    418
    419        Examples
    420        --------
    421        >>> ReversibleProcess.from_string("2A + B <-> X", 0.3, k_rev=0.2)
    422        """
    423        for s in ['<-->', '<=>', '<==>']:
    424            sep = s if s in proc_str else sep
    425        if sep not in proc_str:
    426            raise Exception("Cannot distinguish the reactants from the products.\n"
    427                            "Please use the *sep* keyword: e.g. sep='<->'.")
    428
    429        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
    430        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
    431        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side
    432
    433        return cls(reactants=cls._to_dict(lhs_terms),
    434                   products=cls._to_dict(rhs_terms),
    435                   k=k,
    436                   k_rev=k_rev,
    437                   volume=volume)

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.
- volume : float, default (None, optional): The volume in liters of the compartment in which the processes are taking place.
- 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)

##### Inherited Members

Process
  reactants
  products
  k
  volume
  order
  is_heterogeneous
  species
  reacts_
  prods_

class MichaelisMentenProcess(Process): View Source

    501class MichaelisMentenProcess(Process):
    502    """ Define a process that obeys Michaelis-Menten kinetics.
    503
    504    The class-specific attributes are listed below.
    505
    506    Attributes
    507    ----------
    508    catalyst : str
    509        Name of the species acting as a catalyst for this process.
    510    Km : float or int or list of floats or 2-tuple of floats
    511        *Microscopic* Michaelis constant. Corresponds to the number
    512        of `catalyst` agents that would produce half-maximal activity.
    513        Heterogeneity in this parameter is determined by the type of `Km`,
    514        using the same rules as for parameter `k`.
    515    is_heterogeneous_Km : bool
    516        Denotes if the parameter `Km` exhibits heterogeneity
    517        (distinct subspecies/interactions or normally-distributed).
    518    """
    519
    520    def __init__(self,
    521                 /,
    522                 reactants: dict[str, int],
    523                 products: dict[str, int],
    524                 k: float | int | list[float | int, ...] | tuple[float | int, float | int],
    525                 *,
    526                 catalyst: str,
    527                 Km: float | int | list[float | int, ...] | tuple[float | int, float | int],
    528                 volume: float | None = None):
    529
    530        self.catalyst = catalyst
    531        self.Km = Km
    532
    533        super().__init__(reactants=reactants,
    534                         products=products,
    535                         k=k,
    536                         volume=volume)
    537
    538        self.is_heterogeneous_Km = False if isinstance(self.Km, (int, float)) else True
    539        self.species.add(self.catalyst)
    540        self._str += f", catalyst = {self.catalyst}, Km = {self.Km}"
    541
    542        assert self.order != 0, "A 0th order process has no substrate for a catalyst " \
    543                                "to act on, therefore it cannot follow Michaelis-Menten kinetics."
    544        if self.order == 2:
    545            raise NotImplementedError
    546
    547        if self.volume is not None:  # Convert macroscopic to microscopic Km value
    548            self.Km = macro_to_micro(Km, self.volume)
    549
    550    @classmethod
    551    def from_string(cls,
    552                    proc_str: str,
    553                    /,
    554                    k: float | int | list[float | int, ...] | tuple[float | int, float | int],
    555                    *,
    556                    catalyst: str = None,
    557                    Km: float | int | list[float | int, ...] | tuple[
    558                        float | int, float | int] = None,
    559                    volume: float | None = None,
    560                    sep: str = '->') -> Self:
    561        """ Create a Michaelis-Menten process from a string.
    562
    563        Parameters
    564        ----------
    565        proc_str : str
    566            A string describing the process in standard chemical notation
    567            (e.g., 'A + B -> C')
    568        k : float or int or list of floats or 2-tuple of floats
    569            The *microscopic* rate constant for the given process. If `k` is a
    570            float or int, then the process is homogeneous. If `k` is a list, then
    571            the population of the reactants constsists of distinct subspecies
    572            or subinteractions depending on the order. If `k` is a 2-tuple,
    573            then the constant is normally-distributed with a mean and standard
    574            deviation specified in the tuple's elements.
    575        catalyst : str
    576            Name of species acting as a catalyst.
    577        Km : float or int or list of floats or 2-tuple of floats
    578            *Microscopic* Michaelis constant for the process.
    579            Heterogeneity in this parameter is determined by the type of `Km`,
    580            using the same rules as for parameter `k`.
    581        volume : float, default : None, optional
    582            The volume *in liters* of the compartment in which the processes
    583            are taking place.
    584        sep : str, default: '->'
    585            Specifies the characters that distinguish the reactants from the
    586            products. The default is '->'. The code also treats `-->` as a
    587            default, if it's present in `proc_str`.
    588
    589        Notes
    590        -----
    591        - Species names should not contain spaces, dashes, and
    592          should start with a non-numeric character.
    593        - Zeroth order processes should be specified by an empty space or 'None'.
    594
    595        Examples
    596        --------
    597        >>> MichaelisMentenProcess.from_string("A -> X", k=0.3, catalyst='E', Km=10)
    598        >>> MichaelisMentenProcess.from_string("A -> X", k=0.3, catalyst='alpha', Km=(10, 1))
    599        """
    600        sep = '-->' if '-->' in proc_str else sep
    601        if sep not in proc_str:
    602            raise Exception("Cannot distinguish the reactants from the products.\n"
    603                            "Please use the *sep* keyword: e.g. sep='->'.")
    604
    605        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
    606        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
    607        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side
    608
    609        return cls(reactants=cls._to_dict(lhs_terms),
    610                   products=cls._to_dict(rhs_terms),
    611                   k=k,
    612                   catalyst=catalyst,
    613                   Km=Km,
    614                   volume=volume)
    615
    616    def __repr__(self):
    617        repr_k = macro_to_micro(self.k, self.volume, self.order, inverse=True) if self.volume is not None else self.k
    618        repr_Km = macro_to_micro(self.Km, self.volume, inverse=True) if self.volume is not None else self.Km
    619        return f"MichaelisMentenProcess Object: " \
    620               f"MichaelisMentenProcess.from_string('{self._str.split(',')[0]}', " \
    621               f"k={repr_k}, " \
    622               f"catalyst='{self.catalyst}', " \
    623               f"Km={repr_Km}, " \
    624               f"volume={self.volume})"
    625
    626    def __str__(self):
    627        if isinstance(self.Km, (float, int)):
    628            Km_het_str = "Homogeneous process with respect to Km."
    629        elif isinstance(self.k, list):
    630            Km_het_str = f"Heterogeneous process with respect to Km " \
    631                         f"with {len(self.Km)} distinct subspecies."
    632        else:
    633            Km_het_str = f"Heterogeneous process with normally-distributed Km with " \
    634                         f"mean {self.Km[0]} and standard deviation {self.Km[1]}."
    635
    636        return super().__str__() + f" Catalyst: {self.catalyst}, " \
    637                                   f"Km = {self.Km}, {Km_het_str}"
    638
    639    def __eq__(self, other):
    640        if isinstance(other, MichaelisMentenProcess):
    641            is_equal = (self.k == other.k and
    642                        self.order == other.order and
    643                        self.reactants == other.reactants and
    644                        self.products == other.products and
    645                        self.catalyst == other.catalyst and
    646                        self.Km == other.Km and
    647                        self.species == other.species and
    648                        self.volume == other.volume)
    649            return is_equal
    650        elif isinstance(other, str):
    651            return self._str == other or self._str.replace(' ', '') == other
    652        else:
    653            print(f"{type(self)} and {type(other)} are instances of different classes.")
    654            return False
    655
    656    def __hash__(self):
    657        return hash(self._str)

Define a process that obeys Michaelis-Menten kinetics.

The class-specific attributes are listed below.

###### 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 `Km`, 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).

MichaelisMentenProcess( reactants: dict[str, int], products: dict[str, int], k: float | int | list[float | int, ...] | tuple[float | int, float | int], *, catalyst: str, Km: float | int | list[float | int, ...] | tuple[float | int, float | int], volume: float | None = None) View Source

    520    def __init__(self,
    521                 /,
    522                 reactants: dict[str, int],
    523                 products: dict[str, int],
    524                 k: float | int | list[float | int, ...] | tuple[float | int, float | int],
    525                 *,
    526                 catalyst: str,
    527                 Km: float | int | list[float | int, ...] | tuple[float | int, float | int],
    528                 volume: float | None = None):
    529
    530        self.catalyst = catalyst
    531        self.Km = Km
    532
    533        super().__init__(reactants=reactants,
    534                         products=products,
    535                         k=k,
    536                         volume=volume)
    537
    538        self.is_heterogeneous_Km = False if isinstance(self.Km, (int, float)) else True
    539        self.species.add(self.catalyst)
    540        self._str += f", catalyst = {self.catalyst}, Km = {self.Km}"
    541
    542        assert self.order != 0, "A 0th order process has no substrate for a catalyst " \
    543                                "to act on, therefore it cannot follow Michaelis-Menten kinetics."
    544        if self.order == 2:
    545            raise NotImplementedError
    546
    547        if self.volume is not None:  # Convert macroscopic to microscopic Km value
    548            self.Km = macro_to_micro(Km, self.volume)

catalyst
Km
is_heterogeneous_Km
@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, volume: float | None = None, sep: str = '->') -> Self: View Source

    550    @classmethod
    551    def from_string(cls,
    552                    proc_str: str,
    553                    /,
    554                    k: float | int | list[float | int, ...] | tuple[float | int, float | int],
    555                    *,
    556                    catalyst: str = None,
    557                    Km: float | int | list[float | int, ...] | tuple[
    558                        float | int, float | int] = None,
    559                    volume: float | None = None,
    560                    sep: str = '->') -> Self:
    561        """ Create a Michaelis-Menten process from a string.
    562
    563        Parameters
    564        ----------
    565        proc_str : str
    566            A string describing the process in standard chemical notation
    567            (e.g., 'A + B -> C')
    568        k : float or int or list of floats or 2-tuple of floats
    569            The *microscopic* rate constant for the given process. If `k` is a
    570            float or int, then the process is homogeneous. If `k` is a list, then
    571            the population of the reactants constsists of distinct subspecies
    572            or subinteractions depending on the order. If `k` is a 2-tuple,
    573            then the constant is normally-distributed with a mean and standard
    574            deviation specified in the tuple's elements.
    575        catalyst : str
    576            Name of species acting as a catalyst.
    577        Km : float or int or list of floats or 2-tuple of floats
    578            *Microscopic* Michaelis constant for the process.
    579            Heterogeneity in this parameter is determined by the type of `Km`,
    580            using the same rules as for parameter `k`.
    581        volume : float, default : None, optional
    582            The volume *in liters* of the compartment in which the processes
    583            are taking place.
    584        sep : str, default: '->'
    585            Specifies the characters that distinguish the reactants from the
    586            products. The default is '->'. The code also treats `-->` as a
    587            default, if it's present in `proc_str`.
    588
    589        Notes
    590        -----
    591        - Species names should not contain spaces, dashes, and
    592          should start with a non-numeric character.
    593        - Zeroth order processes should be specified by an empty space or 'None'.
    594
    595        Examples
    596        --------
    597        >>> MichaelisMentenProcess.from_string("A -> X", k=0.3, catalyst='E', Km=10)
    598        >>> MichaelisMentenProcess.from_string("A -> X", k=0.3, catalyst='alpha', Km=(10, 1))
    599        """
    600        sep = '-->' if '-->' in proc_str else sep
    601        if sep not in proc_str:
    602            raise Exception("Cannot distinguish the reactants from the products.\n"
    603                            "Please use the *sep* keyword: e.g. sep='->'.")
    604
    605        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
    606        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
    607        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side
    608
    609        return cls(reactants=cls._to_dict(lhs_terms),
    610                   products=cls._to_dict(rhs_terms),
    611                   k=k,
    612                   catalyst=catalyst,
    613                   Km=Km,
    614                   volume=volume)

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`.
- volume : float, default (None, optional): The volume in liters of the compartment in which the processes are taking place.
- 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))

##### Inherited Members

Process
  reactants
  products
  k
  volume
  order
  is_heterogeneous
  species
  reacts_
  prods_

class RegulatedProcess(Process): View Source

    660class RegulatedProcess(Process):
    661    """ Define a process that is regulated.
    662
    663    This class allows a Process to be defined in terms of how it is regulated.
    664    If there is only one regulating species, then the parameters have the same
    665    type as would be expected for a homogeneous/heterogeneous process.
    666    If there are multiple regulating species, then all parameters are a list
    667    of their expected type, with the length of the list being equal to the
    668    number of regulating species.
    669
    670    The class-specific attributes (except for `k`, which requires some
    671    additional notes) are listed below.
    672
    673    Attributes
    674    ----------
    675    k : float or int or list of floats or 2-tuple of floats
    676        The *microscopic* rate constant for the given process. It is the *basal*
    677        rate constant in the case of activation (or the minimum `k` value)
    678        and the maximum rate constant in the case of repression.
    679    regulating_species : str or list of str
    680        Name of the regulating species. Multiple species can be specified as
    681        comma-separated in a string or a list of strings with the species names.
    682    alpha : float or int or list[float or int]
    683        Parameter denoting the degree of activation/repression.
    684
    685            - 0 <= alpha < 1: repression
    686            - alpha = 1: no regulation
    687            - alpha > 1: activation
    688
    689        alpha is a multiplier: in the case of activation, the maximum
    690        rate constant value will be `alpha * k`.
    691        In the case of repression, the minimum
    692        rate constant value will be `alpha * k`.
    693    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]
    694        *Microscopic* constant that corresponds to the number of
    695        `regulating_species` agents that would produce
    696        half-maximal activation/repression.
    697        Heterogeneity in this parameter is determined by the type of `K50`,
    698        using the same rules as for parameter `k`.
    699    nH : float or int or list[float or int]
    700        Hill coefficient for the given process. Indicates the degree of
    701        cooperativity in the regulatory interaction.
    702    is_heterogeneous_K50 : bool or list of bool
    703        Denotes if the parameter `K50` exhibits heterogeneity
    704        (distinct subspecies/interactions or normally-distributed).
    705    regulation_type : str or list of str
    706        The type of regulation for this process based on the value of alpha:
    707        'activation' or 'repression' or 'no regulation'.
    708
    709    Notes
    710    -----
    711    Allowing a 0th order process to be regulated. However, heterogeneity
    712    in `k` and `K50` (or any other parameters) is not allowed for such
    713    a process.
    714    """
    715
    716    def __init__(self,
    717                 /,
    718                 reactants: dict[str, int],
    719                 products: dict[str, int],
    720                 k: float | int | list[float, ...] | tuple[float, float],
    721                 *,
    722                 regulating_species: str | list[str, ...],
    723                 alpha: float | int | list[float | int, ...],
    724                 K50: float | int | list[float | int, ...] | tuple[float | int, float | int] |
    725                      list[float | int | list[float | int, ...] | tuple[float | int, float | int]],
    726                 nH: float | int | list[float | int, ...],
    727                 volume: float | None = None):
    728
    729        if isinstance(regulating_species, str):
    730            reg_sp_list = regulating_species.replace(' ', '').split(',')
    731            self.regulating_species = reg_sp_list[0] if len(reg_sp_list) == 1 else reg_sp_list
    732        else:  # if it is a list
    733            self.regulating_species = regulating_species
    734
    735        self.alpha = alpha
    736        self.K50 = K50
    737        self.nH = nH
    738
    739        if isinstance(K50, list):
    740            self.is_heterogeneous_K50 = [False if isinstance(val, (int, float)) else True for val
    741                                         in K50]
    742        else:
    743            self.is_heterogeneous_K50 = False if isinstance(self.K50, (int, float)) else True
    744
    745        super().__init__(reactants=reactants,
    746                         products=products,
    747                         k=k,
    748                         volume=volume)
    749
    750        if self.volume is not None:  # Convert macroscopic to microscopic K50 value
    751            self.K50 = macro_to_micro(K50, self.volume)
    752
    753        self._str += f", regulating_species = {self.regulating_species}, alpha = {self.alpha}, " \
    754                     f"K50 = {self.K50}, nH = {self.nH}"
    755
    756        self._validate_reg_params()
    757
    758        if isinstance(self.alpha, list):
    759            self.regulation_type = list()
    760            for a in self.alpha:
    761                reg_type = 'activation' if a > 1 else 'repression' if a < 1 else 'no regulation'
    762                self.regulation_type.append(reg_type)
    763        else:
    764            self.regulation_type = 'activation' if self.alpha > 1 else 'repression' if self.alpha < 1 else 'no regulation'
    765
    766    def _validate_reg_params(self):
    767        """ Validate the parameters specific to the regulation. """
    768        if isinstance(self.regulating_species, list):  # multiple regulating species
    769            # First check that the right number of values for each parameter are specified
    770            rs_num = len(self.regulating_species)
    771            msg = f"Must specify {rs_num} # values when there are {rs_num} regulating species."
    772            assert len(self.alpha) == rs_num, msg.replace('#', 'alpha')
    773            assert len(self.K50) == rs_num, msg.replace('#', 'K50')
    774            assert len(self.nH) == rs_num, msg.replace('#', 'nH')
    775
    776            for i in range(len(self.regulating_species)):
    777                assert self.alpha[i] >= 0, "The alpha parameter must be nonnegative."
    778                if self.alpha[i] == 1:
    779                    print("Warning: alpha=1 means the process is not regulated.")
    780
    781                if isinstance(self.K50[i], (float, int)):
    782                    assert self.K50[i] > 0, "K50 has to be positive."
    783                elif isinstance(self.K50[i], list):
    784                    assert all([True if val > 0 else False for val in self.K50[i]]), \
    785                        "Subspecies K50 values have to be positive."
    786                else:  # isinstance(self.K50, tuple)
    787                    assert self.K50[i][0] > 0 and self.K50[i][1] > 0, \
    788                        "Mean and std of K50 have to be positive."
    789
    790                if self.order == 0:
    791                    assert not self.is_heterogeneous_K50[i], \
    792                        "Heterogeneity in parameter K50 is not allowed for a 0th order process."
    793
    794                assert self.nH[i] > 0, "nH has to be positive."
    795
    796        else:  # just one regulating species
    797            assert self.alpha >= 0, "The alpha parameter must be nonnegative."
    798            if self.alpha == 1:
    799                print("Warning: alpha=1 means the process is not regulated.")
    800
    801            if isinstance(self.K50, (float, int)):
    802                assert self.K50 > 0, "K50 has to be positive."
    803            elif isinstance(self.K50, list):
    804                assert all([True if val > 0 else False for val in self.K50]), \
    805                    "Subspecies K50 values have to be positive."
    806            else:  # isinstance(self.K50, tuple)
    807                assert self.K50[0] > 0 and self.K50[1] > 0, \
    808                    "Mean and std of K50 have to be positive."
    809
    810            if self.order == 0:
    811                assert not self.is_heterogeneous_K50, \
    812                    "Heterogeneity in parameter K50 is not allowed for a 0th order process."
    813
    814            assert self.nH > 0, "nH has to be positive."
    815
    816    @classmethod
    817    def from_string(cls,
    818                    proc_str: str,
    819                    /,
    820                    k: float | int | list[float, ...] | tuple[float, float],
    821                    *,
    822                    regulating_species: str | list[str, ...] = None,
    823                    alpha: float | int | list[float | int, ...] = 1,
    824                    K50: float | int | list[float | int, ...] | tuple[float | int, float | int] |
    825                         list[float | int | list[float | int, ...] | tuple[
    826                             float | int, float | int]] = None,
    827                    nH: float | int | list[float | int, ...] = None,
    828                    volume: float | None = None,
    829                    sep: str = '->') -> Self:
    830        """ Create a regulated process from a string.
    831
    832        Parameters
    833        ----------
    834        proc_str : str
    835            A string describing the process in standard chemical notation
    836            (e.g., 'A + B -> C')
    837        k : float or int or list of floats or 2-tuple of floats
    838            The *microscopic* rate constant for the given process. It is the *basal*
    839            rate constant in the case of activation (or the minimum `k` value)
    840            and the maximum rate constant in the case of repression.
    841            If `k` is a float or int, then the process is homogeneous.
    842            If `k` is a list, then the population of the reactants
    843            constsists of distinct subspecies or subinteractions
    844            depending on the order. If `k` is a 2-tuple,
    845            then the constant is normally-distributed with a mean and standard
    846            deviation specified in the tuple's elements. Note that `k` cannot
    847            be zero for this form of regulation.
    848        regulating_species : str or list of str
    849            Name of the regulating species.
    850        alpha : float or int or list[float or int]
    851            Parameter denoting the degree of activation/repression.
    852
    853                - 0 <= alpha < 1: repression
    854                - alpha = 1: no regulation
    855                - alpha > 1: activation
    856
    857            alpha is a multiplier: in the case of activation, the maximum
    858            rate constant value will be `alpha * k`.
    859            In the case of repression, the minimum
    860            rate constant value will be `alpha * k`.
    861        K50 : float or int or list of floats or 2-tuple of floats or list of each of the previous types
    862            *Microscopic* constant that corresponds to the number of
    863            `regulating_species` agents that would produce
    864            half-maximal activation/repression.
    865            Heterogeneity in this parameter is determined by the type of `K50`,
    866            using the same rules as for parameter `k`.
    867        nH : float or int or list[float or int]
    868            Hill coefficient for the given process. Indicates the degree of
    869            cooperativity in the regulatory interaction.
    870        volume : float, default : None, optional
    871            The volume *in liters* of the compartment in which the processes
    872            are taking place.
    873        sep : str, default: '->'
    874            Specifies the characters that distinguish the reactants from the
    875            products. The default is '->'. The code also treats `-->` as a
    876            default, if it's present in `proc_str`.
    877
    878        Notes
    879        -----
    880        - Species names should not contain spaces, dashes, and
    881          should start with a non-numeric character.
    882        - Zeroth order processes should be specified by an empty space or 'None'.
    883
    884        Examples
    885        --------
    886        >>> RegulatedProcess.from_string("A -> X", k=0.2, regulating_species='X', alpha=2, K50=10, nH=1)
    887        >>> RegulatedProcess.from_string("A -> X", k=0.3, regulating_species='X', alpha=0.5, K50=[10, 15], nH=2)
    888        >>> RegulatedProcess.from_string("A + B -> X", k=0.5, regulating_species='B, X', alpha=[2, 0], K50=[(15, 5), [10, 15]], nH=[1, 2])
    889        """
    890        sep = '-->' if '-->' in proc_str else sep
    891        if sep not in proc_str:
    892            raise Exception("Cannot distinguish the reactants from the products.\n"
    893                            "Please use the *sep* keyword: e.g. sep='->'.")
    894
    895        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
    896        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
    897        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side
    898
    899        return cls(reactants=cls._to_dict(lhs_terms),
    900                   products=cls._to_dict(rhs_terms),
    901                   k=k,
    902                   regulating_species=regulating_species,
    903                   alpha=alpha,
    904                   K50=K50,
    905                   nH=nH,
    906                   volume=volume)
    907
    908    def __repr__(self):
    909        repr_k = macro_to_micro(self.k, self.volume, self.order, inverse=True) if self.volume is not None else self.k
    910        repr_K50 = macro_to_micro(self.K50, self.volume, inverse=True) if self.volume is not None else self.K50
    911        return f"RegulatedProcess Object: " \
    912               f"RegulatedProcess.from_string('{self._str.split(',')[0]}', " \
    913               f"k={repr_k}, " \
    914               f"regulating_species='{self.regulating_species}', " \
    915               f"alpha={self.alpha}, " \
    916               f"K50={repr_K50}, " \
    917               f"nH={self.nH}," \
    918               f"volume={self.volume})"
    919
    920    def __str__(self):
    921        if isinstance(self.regulating_species, list):
    922            K50_het_str = ""
    923            for i, sp in enumerate(self.regulating_species):
    924                if isinstance(self.K50[i], (float, int)):
    925                    K50_het_str += f"Homogeneous process with respect to species {sp} K50. "
    926                elif isinstance(self.K50[i], list):
    927                    K50_het_str += f"Heterogeneous process with respect to species {sp} K50 " \
    928                                   f"with {len(self.K50[i])} distinct subspecies. "
    929                else:
    930                    K50_het_str += f"Heterogeneous process with normally-distributed " \
    931                                   f"species {sp} K50 with mean {self.K50[i][0]} and " \
    932                                   f"standard deviation {self.K50[i][1]}. "
    933        else:
    934            if isinstance(self.K50, (float, int)):
    935                K50_het_str = "Homogeneous process with respect to K50."
    936            elif isinstance(self.K50, list):
    937                K50_het_str = f"Heterogeneous process with respect to K50 " \
    938                              f"with {len(self.K50)} distinct subspecies."
    939            else:
    940                K50_het_str = f"Heterogeneous process with normally-distributed K50 with " \
    941                              f"mean {self.K50[0]} and standard deviation {self.K50[1]}."
    942
    943        return super().__str__() + f" Regulating Species: {self.regulating_species}, " \
    944                                   f"alpha = {self.alpha}, nH = {self.nH}, " \
    945                                   f"K50 = {self.K50}, {K50_het_str}"
    946
    947    def __eq__(self, other):
    948        if isinstance(other, RegulatedProcess):
    949            is_equal = (self.k == other.k and
    950                        self.order == other.order and
    951                        self.reactants == other.reactants and
    952                        self.products == other.products and
    953                        self.regulating_species == other.regulating_species and
    954                        self.alpha == other.alpha and
    955                        self.K50 == other.K50 and
    956                        self.nH == other.nH and
    957                        self.species == other.species and
    958                        self.volume == other.volume)
    959            return is_equal
    960        elif isinstance(other, str):
    961            return self._str == other or self._str.replace(' ', '') == other
    962        else:
    963            print(f"{type(self)} and {type(other)} are instances of different classes.")
    964            return False
    965
    966    def __hash__(self):
    967        return hash(self._str)

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.

The class-specific attributes (except for `k`, which requires some additional notes) are listed below.

###### 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.

RegulatedProcess( reactants: dict[str, int], products: dict[str, int], 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, ...], volume: float | None = None) View Source

    716    def __init__(self,
    717                 /,
    718                 reactants: dict[str, int],
    719                 products: dict[str, int],
    720                 k: float | int | list[float, ...] | tuple[float, float],
    721                 *,
    722                 regulating_species: str | list[str, ...],
    723                 alpha: float | int | list[float | int, ...],
    724                 K50: float | int | list[float | int, ...] | tuple[float | int, float | int] |
    725                      list[float | int | list[float | int, ...] | tuple[float | int, float | int]],
    726                 nH: float | int | list[float | int, ...],
    727                 volume: float | None = None):
    728
    729        if isinstance(regulating_species, str):
    730            reg_sp_list = regulating_species.replace(' ', '').split(',')
    731            self.regulating_species = reg_sp_list[0] if len(reg_sp_list) == 1 else reg_sp_list
    732        else:  # if it is a list
    733            self.regulating_species = regulating_species
    734
    735        self.alpha = alpha
    736        self.K50 = K50
    737        self.nH = nH
    738
    739        if isinstance(K50, list):
    740            self.is_heterogeneous_K50 = [False if isinstance(val, (int, float)) else True for val
    741                                         in K50]
    742        else:
    743            self.is_heterogeneous_K50 = False if isinstance(self.K50, (int, float)) else True
    744
    745        super().__init__(reactants=reactants,
    746                         products=products,
    747                         k=k,
    748                         volume=volume)
    749
    750        if self.volume is not None:  # Convert macroscopic to microscopic K50 value
    751            self.K50 = macro_to_micro(K50, self.volume)
    752
    753        self._str += f", regulating_species = {self.regulating_species}, alpha = {self.alpha}, " \
    754                     f"K50 = {self.K50}, nH = {self.nH}"
    755
    756        self._validate_reg_params()
    757
    758        if isinstance(self.alpha, list):
    759            self.regulation_type = list()
    760            for a in self.alpha:
    761                reg_type = 'activation' if a > 1 else 'repression' if a < 1 else 'no regulation'
    762                self.regulation_type.append(reg_type)
    763        else:
    764            self.regulation_type = 'activation' if self.alpha > 1 else 'repression' if self.alpha < 1 else 'no regulation'

alpha
K50
nH
@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, volume: float | None = None, sep: str = '->') -> Self: View Source

    816    @classmethod
    817    def from_string(cls,
    818                    proc_str: str,
    819                    /,
    820                    k: float | int | list[float, ...] | tuple[float, float],
    821                    *,
    822                    regulating_species: str | list[str, ...] = None,
    823                    alpha: float | int | list[float | int, ...] = 1,
    824                    K50: float | int | list[float | int, ...] | tuple[float | int, float | int] |
    825                         list[float | int | list[float | int, ...] | tuple[
    826                             float | int, float | int]] = None,
    827                    nH: float | int | list[float | int, ...] = None,
    828                    volume: float | None = None,
    829                    sep: str = '->') -> Self:
    830        """ Create a regulated process from a string.
    831
    832        Parameters
    833        ----------
    834        proc_str : str
    835            A string describing the process in standard chemical notation
    836            (e.g., 'A + B -> C')
    837        k : float or int or list of floats or 2-tuple of floats
    838            The *microscopic* rate constant for the given process. It is the *basal*
    839            rate constant in the case of activation (or the minimum `k` value)
    840            and the maximum rate constant in the case of repression.
    841            If `k` is a float or int, then the process is homogeneous.
    842            If `k` is a list, then the population of the reactants
    843            constsists of distinct subspecies or subinteractions
    844            depending on the order. If `k` is a 2-tuple,
    845            then the constant is normally-distributed with a mean and standard
    846            deviation specified in the tuple's elements. Note that `k` cannot
    847            be zero for this form of regulation.
    848        regulating_species : str or list of str
    849            Name of the regulating species.
    850        alpha : float or int or list[float or int]
    851            Parameter denoting the degree of activation/repression.
    852
    853                - 0 <= alpha < 1: repression
    854                - alpha = 1: no regulation
    855                - alpha > 1: activation
    856
    857            alpha is a multiplier: in the case of activation, the maximum
    858            rate constant value will be `alpha * k`.
    859            In the case of repression, the minimum
    860            rate constant value will be `alpha * k`.
    861        K50 : float or int or list of floats or 2-tuple of floats or list of each of the previous types
    862            *Microscopic* constant that corresponds to the number of
    863            `regulating_species` agents that would produce
    864            half-maximal activation/repression.
    865            Heterogeneity in this parameter is determined by the type of `K50`,
    866            using the same rules as for parameter `k`.
    867        nH : float or int or list[float or int]
    868            Hill coefficient for the given process. Indicates the degree of
    869            cooperativity in the regulatory interaction.
    870        volume : float, default : None, optional
    871            The volume *in liters* of the compartment in which the processes
    872            are taking place.
    873        sep : str, default: '->'
    874            Specifies the characters that distinguish the reactants from the
    875            products. The default is '->'. The code also treats `-->` as a
    876            default, if it's present in `proc_str`.
    877
    878        Notes
    879        -----
    880        - Species names should not contain spaces, dashes, and
    881          should start with a non-numeric character.
    882        - Zeroth order processes should be specified by an empty space or 'None'.
    883
    884        Examples
    885        --------
    886        >>> RegulatedProcess.from_string("A -> X", k=0.2, regulating_species='X', alpha=2, K50=10, nH=1)
    887        >>> RegulatedProcess.from_string("A -> X", k=0.3, regulating_species='X', alpha=0.5, K50=[10, 15], nH=2)
    888        >>> RegulatedProcess.from_string("A + B -> X", k=0.5, regulating_species='B, X', alpha=[2, 0], K50=[(15, 5), [10, 15]], nH=[1, 2])
    889        """
    890        sep = '-->' if '-->' in proc_str else sep
    891        if sep not in proc_str:
    892            raise Exception("Cannot distinguish the reactants from the products.\n"
    893                            "Please use the *sep* keyword: e.g. sep='->'.")
    894
    895        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
    896        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
    897        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side
    898
    899        return cls(reactants=cls._to_dict(lhs_terms),
    900                   products=cls._to_dict(rhs_terms),
    901                   k=k,
    902                   regulating_species=regulating_species,
    903                   alpha=alpha,
    904                   K50=K50,
    905                   nH=nH,
    906                   volume=volume)

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.
- volume : float, default (None, optional): The volume in liters of the compartment in which the processes are taking place.
- 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])

##### Inherited Members

Process
  reactants
  products
  k
  volume
  order
  is_heterogeneous
  species
  reacts_
  prods_

class RegulatedMichaelisMentenProcess(RegulatedProcess): View Source

     970class RegulatedMichaelisMentenProcess(RegulatedProcess):
     971    """ Define a process that is regulated and obeys Michaelis-Menten kinetics.
     972
     973    This class allows a Michaelis-Menten Process to be defined
     974    in terms of how it is regulated.
     975    If there is only one regulating species, then the parameters have the same
     976    type as would be expected for a homogeneous/heterogeneous process.
     977    If there are multiple regulating species, then all parameters are a list
     978    of their expected type, with the length of the list being equal to the
     979    number of regulating species.
     980
     981    The class-specific attributes (except for `k`, which requires some
     982    additional notes) are listed below.
     983
     984    Attributes
     985    ----------
     986    k : float or int or list of floats or 2-tuple of floats
     987        The *microscopic* rate constant for the given process. It is the *basal*
     988        rate constant in the case of activation (or the minimum `k` value)
     989        and the maximum rate constant in the case of repression.
     990    regulating_species : str or list of str
     991        Name of the regulating species. Multiple species can be specified as
     992        comma-separated in a string or a list of strings with the species names.
     993    alpha : float or int or list[float or int]
     994        Parameter denoting the degree of activation/repression.
     995
     996            - 0 <= alpha < 1: repression
     997            - alpha = 1: no regulation
     998            - alpha > 1: activation
     999
    1000        alpha is a multiplier: in the case of activation, the maximum
    1001        rate constant value will be `alpha * k`.
    1002        In the case of repression, the minimum
    1003        rate constant value will be `alpha * k`.
    1004    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]
    1005        *Microscopic* constant that corresponds to the number of
    1006        `regulating_species` agents that would produce
    1007        half-maximal activation/repression.
    1008        Heterogeneity in this parameter is determined by the type of `K50`,
    1009        using the same rules as for parameter `k`.
    1010    nH : float or int or list[float or int]
    1011        Hill coefficient for the given process. Indicates the degree of
    1012        cooperativity in the regulatory interaction.
    1013    is_heterogeneous_K50 : bool or list of bool
    1014        Denotes if the parameter `K50` exhibits heterogeneity
    1015        (distinct subspecies/interactions or normally-distributed).
    1016    regulation_type : str or list of str
    1017        The type of regulation for this process based on the value of alpha:
    1018        'activation' or 'repression' or 'no regulation'.
    1019    catalyst : str
    1020        Name of the species acting as a catalyst for this process.
    1021    Km : float or int or list of floats or 2-tuple of floats
    1022        *Microscopic* Michaelis constant. Corresponds to the number
    1023        of `catalyst` agents that would produce half-maximal activity.
    1024        Heterogeneity in this parameter is determined by the type of `K50`,
    1025        using the same rules as for parameter `k`.
    1026    is_heterogeneous_Km : bool
    1027        Denotes if the parameter `Km` exhibits heterogeneity
    1028        (distinct subspecies/interactions or normally-distributed).
    1029
    1030    Notes
    1031    -----
    1032    Currently only implemented for 1st order processes. 0th order processes
    1033    cannot obey Michaelis-Menten kinetics and 2nd order Michaelis-Menten
    1034    processes are not implemented yet.
    1035    """
    1036
    1037    def __init__(self,
    1038                 /,
    1039                 reactants: dict[str, int],
    1040                 products: dict[str, int],
    1041                 k: float | int | list[float, ...] | tuple[float, float],
    1042                 *,
    1043                 regulating_species: str | list[str, ...],
    1044                 alpha: float | int | list[float | int, ...],
    1045                 K50: float | int | list[float | int, ...] | tuple[float | int, float | int] |
    1046                      list[float | int | list[float | int, ...] | tuple[float | int, float | int]],
    1047                 nH: float | int | list[float | int, ...],
    1048                 catalyst: str,
    1049                 Km: float | int | list[float | int, ...] | tuple[float | int, float | int],
    1050                 volume: float | None = None):
    1051
    1052        self.catalyst = catalyst
    1053        self.Km = Km
    1054        self.is_heterogeneous_Km = False if isinstance(self.Km, (int, float)) else True
    1055
    1056        super().__init__(reactants=reactants,
    1057                         products=products,
    1058                         k=k,
    1059                         regulating_species=regulating_species,
    1060                         alpha=alpha,
    1061                         K50=K50,
    1062                         nH=nH,
    1063                         volume=volume)
    1064
    1065        super()._validate_reg_params()
    1066
    1067        assert self.order != 0, "A 0th order process has no substrate for a catalyst " \
    1068                                "to act on, therefore it cannot follow Michaelis-Menten kinetics."
    1069        if self.order == 2:
    1070            raise NotImplementedError
    1071
    1072        if self.volume is not None:  # Convert macroscopic to microscopic Km value
    1073            self.Km = macro_to_micro(Km, self.volume)
    1074
    1075        self.species.add(self.catalyst)
    1076        self._str += f", catalyst = {self.catalyst}, Km = {self.Km}"
    1077
    1078    @classmethod
    1079    def from_string(cls,
    1080                    proc_str: str,
    1081                    /,
    1082                    k: float | int | list[float, ...] | tuple[float, float],
    1083                    *,
    1084                    regulating_species: str | list[str, ...] = None,
    1085                    alpha: float | int | list[float | int, ...] = 1,
    1086                    K50: float | int | list[float | int, ...] | tuple[float | int, float | int] |
    1087                         list[float | int | list[float | int, ...] | tuple[
    1088                             float | int, float | int]] = None,
    1089                    nH: float | int | list[float | int, ...] = None,
    1090                    catalyst: str = None,
    1091                    Km: float | int | list[float | int, ...] | tuple[
    1092                        float | int, float | int] = None,
    1093                    volume: float | None = None,
    1094                    sep: str = '->') -> Self:
    1095        """ Create a regulated Michaelis-Menten process from a string.
    1096
    1097        Parameters
    1098        ----------
    1099        proc_str : str
    1100            A string describing the process in standard chemical notation
    1101            (e.g., 'A + B -> C')
    1102        k : float or int or list of floats or 2-tuple of floats
    1103            The *microscopic* rate constant for the given process. It is the *basal*
    1104            rate constant in the case of activation (or the minimum `k` value)
    1105            and the maximum rate constant in the case of repression.
    1106            If `k` is a float or int, then the process is homogeneous.
    1107            If `k` is a list, then the population of the reactants
    1108            constsists of distinct subspecies or subinteractions
    1109            depending on the order. If `k` is a 2-tuple,
    1110            then the constant is normally-distributed with a mean and standard
    1111            deviation specified in the tuple's elements. Note that `k` cannot
    1112            be zero for this form of regulation.
    1113        regulating_species : str or list of str
    1114            Name of the regulating species.
    1115        alpha : float or int or list[float or int]
    1116            Parameter denoting the degree of activation/repression.
    1117
    1118            - 0 <= alpha < 1: repression
    1119            - alpha = 1: no regulation
    1120            - alpha > 1: activation
    1121
    1122            alpha is a multiplier: in the case of activation, the maximum
    1123            rate constant value will be `alpha * k`.
    1124            In the case of repression, the minimum
    1125            rate constant value will be `alpha * k`.
    1126        K50 : float or int or list of floats or 2-tuple of floats or list of each of the previous types
    1127            *Microscopic* constant that corresponds to the number of
    1128            `regulating_species` agents that would produce
    1129            half-maximal activation/repression.
    1130            Heterogeneity in this parameter is determined by the type of `K50`,
    1131            using the same rules as for parameter `k`.
    1132        nH : float or int or list[float or int]
    1133            Hill coefficient for the given process. Indicates the degree of
    1134            cooperativity in the regulatory interaction.
    1135        catalyst : str
    1136            Name of species acting as a catalyst.
    1137        Km : float or int or list of floats or 2-tuple of floats
    1138            *Microscopic* Michaelis constant for the process.
    1139            Heterogeneity in this parameter is determined by the type of `Km`,
    1140            using the same rules as for parameter `k`.
    1141        volume : float, default : None, optional
    1142            The volume *in liters* of the compartment in which the processes
    1143            are taking place.
    1144        sep : str, default: '->'
    1145            Specifies the characters that distinguish the reactants from the
    1146            products. The default is '->'. The code also treats `-->` as a
    1147            default, if it's present in `proc_str`.
    1148
    1149        Notes
    1150        -----
    1151        - Species names should not contain spaces, dashes, and
    1152          should start with a non-numeric character.
    1153        - Zeroth order processes should be specified by an empty space or 'None'.
    1154
    1155        Examples
    1156        --------
    1157        >>> RegulatedMichaelisMentenProcess.from_string("A -> X", k=0.2, regulating_species='X', alpha=2, K50=10, nH=1, catalyst='E', Km=15)
    1158        >>> RegulatedMichaelisMentenProcess.from_string("A -> X", k=0.3, regulating_species='A', alpha=0.5, K50=[10, 15], nH=2, catalyst='C', Km=5)
    1159        """
    1160        sep = '-->' if '-->' in proc_str else sep
    1161        if sep not in proc_str:
    1162            raise Exception("Cannot distinguish the reactants from the products.\n"
    1163                            "Please use the *sep* keyword: e.g. sep='->'.")
    1164
    1165        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
    1166        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
    1167        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side
    1168
    1169        return cls(reactants=cls._to_dict(lhs_terms),
    1170                   products=cls._to_dict(rhs_terms),
    1171                   k=k,
    1172                   regulating_species=regulating_species,
    1173                   alpha=alpha,
    1174                   K50=K50,
    1175                   nH=nH,
    1176                   catalyst=catalyst,
    1177                   Km=Km,
    1178                   volume=volume)
    1179
    1180    def __repr__(self):
    1181        repr_k = macro_to_micro(self.k, self.volume, self.order, inverse=True) if self.volume is not None else self.k
    1182        repr_K50 = macro_to_micro(self.K50, self.volume, inverse=True) if self.volume is not None else self.K50
    1183        repr_Km = macro_to_micro(self.Km, self.volume, inverse=True) if self.volume is not None else self.Km
    1184        return f"RegulatedMichaelisMentenProcess Object: " \
    1185               f"RegulatedMichaelisMentenProcess.from_string('{self._str.split(',')[0]}', " \
    1186               f"k={repr_k}, " \
    1187               f"regulating_species='{self.regulating_species}', " \
    1188               f"alpha={self.alpha}, " \
    1189               f"K50={repr_K50}, " \
    1190               f"nH={self.nH}, " \
    1191               f"catalyst={self.catalyst}, " \
    1192               f"Km={repr_Km}," \
    1193               f"volume={self.volume})"
    1194
    1195    def __str__(self):
    1196        if isinstance(self.regulating_species, list):
    1197            K50_het_str = ""
    1198            for i, sp in enumerate(self.regulating_species):
    1199                if isinstance(self.K50[i], (float, int)):
    1200                    K50_het_str += f"Homogeneous process with respect to species {sp} K50. "
    1201                elif isinstance(self.K50[i], list):
    1202                    K50_het_str += f"Heterogeneous process with respect to species {sp} K50 " \
    1203                                   f"with {len(self.K50[i])} distinct subspecies. "
    1204                else:
    1205                    K50_het_str += f"Heterogeneous process with normally-distributed " \
    1206                                   f"species {sp} K50 with mean {self.K50[i][0]} and " \
    1207                                   f"standard deviation {self.K50[i][1]}. "
    1208        else:
    1209            if isinstance(self.K50, (float, int)):
    1210                K50_het_str = "Homogeneous process with respect to K50."
    1211            elif isinstance(self.K50, list):
    1212                K50_het_str = f"Heterogeneous process with respect to K50 " \
    1213                              f"with {len(self.K50)} distinct subspecies."
    1214            else:
    1215                K50_het_str = f"Heterogeneous process with normally-distributed K50 with " \
    1216                              f"mean {self.K50[0]} and standard deviation {self.K50[1]}."
    1217
    1218        if isinstance(self.Km, (float, int)):
    1219            Km_het_str = "Homogeneous process with respect to Km."
    1220        elif isinstance(self.k, list):
    1221            Km_het_str = f"Heterogeneous process with respect to Km " \
    1222                         f"with {len(self.Km)} distinct subspecies."
    1223        else:
    1224            Km_het_str = f"Heterogeneous process with normally-distributed Km with " \
    1225                         f"mean {self.Km[0]} and standard deviation {self.Km[1]}."
    1226
    1227        return super().__str__() + f" Regulating Species: {self.regulating_species}, " \
    1228                                   f"alpha = {self.alpha}, nH = {self.nH}, " \
    1229                                   f"K50 = {self.K50}, {K50_het_str}, " \
    1230                                   f"Catalyst: {self.catalyst}, " \
    1231                                   f"Km = {self.Km}, {Km_het_str}"
    1232
    1233    def __eq__(self, other):
    1234        if isinstance(other, RegulatedMichaelisMentenProcess):
    1235            is_equal = (self.k == other.k and
    1236                        self.order == other.order and
    1237                        self.reactants == other.reactants and
    1238                        self.products == other.products and
    1239                        self.regulating_species == other.regulating_species and
    1240                        self.alpha == other.alpha and
    1241                        self.K50 == other.K50 and
    1242                        self.nH == other.nH and
    1243                        self.catalyst == other.catalyst and
    1244                        self.Km == other.Km and
    1245                        self.species == other.species and
    1246                        self.volume == other.volume)
    1247            return is_equal
    1248        elif isinstance(other, str):
    1249            return self._str == other or self._str.replace(' ', '') == other
    1250        else:
    1251            print(f"{type(self)} and {type(other)} are instances of different classes.")
    1252            return False
    1253
    1254    def __hash__(self):
    1255        return hash(self._str)

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.

The class-specific attributes (except for `k`, which requires some additional notes) are listed below.

###### 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.

RegulatedMichaelisMentenProcess( reactants: dict[str, int], products: dict[str, int], 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], volume: float | None = None) View Source

    1037    def __init__(self,
    1038                 /,
    1039                 reactants: dict[str, int],
    1040                 products: dict[str, int],
    1041                 k: float | int | list[float, ...] | tuple[float, float],
    1042                 *,
    1043                 regulating_species: str | list[str, ...],
    1044                 alpha: float | int | list[float | int, ...],
    1045                 K50: float | int | list[float | int, ...] | tuple[float | int, float | int] |
    1046                      list[float | int | list[float | int, ...] | tuple[float | int, float | int]],
    1047                 nH: float | int | list[float | int, ...],
    1048                 catalyst: str,
    1049                 Km: float | int | list[float | int, ...] | tuple[float | int, float | int],
    1050                 volume: float | None = None):
    1051
    1052        self.catalyst = catalyst
    1053        self.Km = Km
    1054        self.is_heterogeneous_Km = False if isinstance(self.Km, (int, float)) else True
    1055
    1056        super().__init__(reactants=reactants,
    1057                         products=products,
    1058                         k=k,
    1059                         regulating_species=regulating_species,
    1060                         alpha=alpha,
    1061                         K50=K50,
    1062                         nH=nH,
    1063                         volume=volume)
    1064
    1065        super()._validate_reg_params()
    1066
    1067        assert self.order != 0, "A 0th order process has no substrate for a catalyst " \
    1068                                "to act on, therefore it cannot follow Michaelis-Menten kinetics."
    1069        if self.order == 2:
    1070            raise NotImplementedError
    1071
    1072        if self.volume is not None:  # Convert macroscopic to microscopic Km value
    1073            self.Km = macro_to_micro(Km, self.volume)
    1074
    1075        self.species.add(self.catalyst)
    1076        self._str += f", catalyst = {self.catalyst}, Km = {self.Km}"

catalyst
Km
is_heterogeneous_Km
@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, volume: float | None = None, sep: str = '->') -> Self: View Source

    1078    @classmethod
    1079    def from_string(cls,
    1080                    proc_str: str,
    1081                    /,
    1082                    k: float | int | list[float, ...] | tuple[float, float],
    1083                    *,
    1084                    regulating_species: str | list[str, ...] = None,
    1085                    alpha: float | int | list[float | int, ...] = 1,
    1086                    K50: float | int | list[float | int, ...] | tuple[float | int, float | int] |
    1087                         list[float | int | list[float | int, ...] | tuple[
    1088                             float | int, float | int]] = None,
    1089                    nH: float | int | list[float | int, ...] = None,
    1090                    catalyst: str = None,
    1091                    Km: float | int | list[float | int, ...] | tuple[
    1092                        float | int, float | int] = None,
    1093                    volume: float | None = None,
    1094                    sep: str = '->') -> Self:
    1095        """ Create a regulated Michaelis-Menten process from a string.
    1096
    1097        Parameters
    1098        ----------
    1099        proc_str : str
    1100            A string describing the process in standard chemical notation
    1101            (e.g., 'A + B -> C')
    1102        k : float or int or list of floats or 2-tuple of floats
    1103            The *microscopic* rate constant for the given process. It is the *basal*
    1104            rate constant in the case of activation (or the minimum `k` value)
    1105            and the maximum rate constant in the case of repression.
    1106            If `k` is a float or int, then the process is homogeneous.
    1107            If `k` is a list, then the population of the reactants
    1108            constsists of distinct subspecies or subinteractions
    1109            depending on the order. If `k` is a 2-tuple,
    1110            then the constant is normally-distributed with a mean and standard
    1111            deviation specified in the tuple's elements. Note that `k` cannot
    1112            be zero for this form of regulation.
    1113        regulating_species : str or list of str
    1114            Name of the regulating species.
    1115        alpha : float or int or list[float or int]
    1116            Parameter denoting the degree of activation/repression.
    1117
    1118            - 0 <= alpha < 1: repression
    1119            - alpha = 1: no regulation
    1120            - alpha > 1: activation
    1121
    1122            alpha is a multiplier: in the case of activation, the maximum
    1123            rate constant value will be `alpha * k`.
    1124            In the case of repression, the minimum
    1125            rate constant value will be `alpha * k`.
    1126        K50 : float or int or list of floats or 2-tuple of floats or list of each of the previous types
    1127            *Microscopic* constant that corresponds to the number of
    1128            `regulating_species` agents that would produce
    1129            half-maximal activation/repression.
    1130            Heterogeneity in this parameter is determined by the type of `K50`,
    1131            using the same rules as for parameter `k`.
    1132        nH : float or int or list[float or int]
    1133            Hill coefficient for the given process. Indicates the degree of
    1134            cooperativity in the regulatory interaction.
    1135        catalyst : str
    1136            Name of species acting as a catalyst.
    1137        Km : float or int or list of floats or 2-tuple of floats
    1138            *Microscopic* Michaelis constant for the process.
    1139            Heterogeneity in this parameter is determined by the type of `Km`,
    1140            using the same rules as for parameter `k`.
    1141        volume : float, default : None, optional
    1142            The volume *in liters* of the compartment in which the processes
    1143            are taking place.
    1144        sep : str, default: '->'
    1145            Specifies the characters that distinguish the reactants from the
    1146            products. The default is '->'. The code also treats `-->` as a
    1147            default, if it's present in `proc_str`.
    1148
    1149        Notes
    1150        -----
    1151        - Species names should not contain spaces, dashes, and
    1152          should start with a non-numeric character.
    1153        - Zeroth order processes should be specified by an empty space or 'None'.
    1154
    1155        Examples
    1156        --------
    1157        >>> RegulatedMichaelisMentenProcess.from_string("A -> X", k=0.2, regulating_species='X', alpha=2, K50=10, nH=1, catalyst='E', Km=15)
    1158        >>> RegulatedMichaelisMentenProcess.from_string("A -> X", k=0.3, regulating_species='A', alpha=0.5, K50=[10, 15], nH=2, catalyst='C', Km=5)
    1159        """
    1160        sep = '-->' if '-->' in proc_str else sep
    1161        if sep not in proc_str:
    1162            raise Exception("Cannot distinguish the reactants from the products.\n"
    1163                            "Please use the *sep* keyword: e.g. sep='->'.")
    1164
    1165        lhs, rhs = proc_str.strip().split(sep)  # Left- and Right-hand sides of process
    1166        lhs_terms = lhs.split('+')  # Separate the terms on the left-hand side
    1167        rhs_terms = rhs.split('+')  # Separate the terms on the right-hand side
    1168
    1169        return cls(reactants=cls._to_dict(lhs_terms),
    1170                   products=cls._to_dict(rhs_terms),
    1171                   k=k,
    1172                   regulating_species=regulating_species,
    1173                   alpha=alpha,
    1174                   K50=K50,
    1175                   nH=nH,
    1176                   catalyst=catalyst,
    1177                   Km=Km,
    1178                   volume=volume)

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`.
- volume : float, default (None, optional): The volume in liters of the compartment in which the processes are taking place.
- 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)

##### Inherited Members

RegulatedProcess
  alpha
  K50
  nH
Process
  reactants
  products
  k
  volume
  order
  is_heterogeneous
  species
  reacts_
  prods_

class NullSpeciesNameError(builtins.Exception): View Source

    1258class NullSpeciesNameError(Exception):
    1259    """ Error when the species name is an empty string. """
    1260
    1261    def __str__(self):
    1262        return "A species name cannot be an empty string."

Error when the species name is an empty string.

def update_all_species(procs: tuple[Process, ...]) -> tuple[set, dict, dict]: View Source

    1265def update_all_species(procs: tuple[Process, ...]) -> tuple[set, dict, dict]:
    1266    """ Categorize all species in a list of processes.
    1267
    1268    Extract all species from a list of processes. Then categorize each of them
    1269    as a reactant or product and list the process(es) it takes part in.
    1270
    1271    Parameters
    1272    ----------
    1273    procs : tuple
    1274        A tuple of objects of type `Process` or its subclasses.
    1275
    1276    Returns
    1277    -------
    1278    tuple
    1279        all_species : set of strings
    1280            A set of all species present in the processes.
    1281        procs_by_reactant : dict
    1282            A dictionary whose keys are the species that are
    1283            reactants in one or more processes. The value for each
    1284            key is a list of processes.
    1285        procs_by_product : dict
    1286            A dictionary whose keys are the species that are
    1287            products in one or more processes. The value for each
    1288            key is a list of processes.
    1289    """
    1290    procs = list(procs)
    1291    for proc in procs:
    1292        # For a reversible process, replace it with separate instances
    1293        # of Process objects representing the forward and reverse reactions.
    1294        if isinstance(proc, ReversibleProcess):
    1295            forward_proc = Process(proc.reactants, proc.products, proc.k)
    1296            reverse_proc = Process(proc.products, proc.reactants, proc.k_rev)
    1297            procs.remove(proc)
    1298            procs.extend([forward_proc, reverse_proc])
    1299
    1300    assert len(set(procs)) == len(procs), \
    1301        "WARNING: Duplicate processes found. Examine the list of processes to resolve this."
    1302
    1303    all_species, rspecies, pspecies = set(), set(), set()
    1304    procs_by_reactant, procs_by_product = dict(), dict()
    1305
    1306    for proc in procs:
    1307        all_species = all_species.union(proc.species)
    1308        rspecies = rspecies.union(proc.reactants)
    1309        pspecies = pspecies.union(proc.products)
    1310
    1311    # Make a list containing the processes each reactant species takes part in.
    1312    # This will be used when solving the system ODEs.
    1313    for rspec in rspecies:
    1314        if rspec != '':  # omit reactant species parsed from 0th order processes
    1315            procs_by_reactant[rspec] = [proc for proc in procs if rspec in proc.reactants]
    1316            # deleted 1st clause in above `if`: `rspec != '' and`
    1317
    1318    # Make a list containing the processes each product species takes part in.
    1319    # This will be used for solving the system ODEs.
    1320    for pspec in pspecies:
    1321        if pspec != '':  # omitting product species parsed from degradation processes
    1322            procs_by_product[pspec] = [proc for proc in procs if pspec in proc.products]
    1323
    1324    return all_species, procs_by_reactant, procs_by_product

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.