abstochkin.process

Define a process of the form Reactants -> Products.

   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:
 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)
 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:
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):
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)
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:
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)
class MichaelisMentenProcess(Process):
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)
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:
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))
class RegulatedProcess(Process):
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)
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:
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])
class RegulatedMichaelisMentenProcess(RegulatedProcess):
 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)
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:
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)
class NullSpeciesNameError(builtins.Exception):
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]:
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.