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
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: ifk
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 correspondingk
value): ifk
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-distributedk
values: Ifk
is a tuple whose length is 2, then the population is assumed to be heterogeneous with a normally distributedk
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.
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)
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. Ifk
is a list, then the population of the reactants constsists of distinct subspecies or subinteractions depending on the order. Ifk
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 inproc_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.
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.
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)
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 inproc_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)
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 ofKm
, using the same rules as for parameterk
. - is_heterogeneous_Km (bool):
Denotes if the parameter
Km
exhibits heterogeneity (distinct subspecies/interactions or normally-distributed).
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)
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. Ifk
is a list, then the population of the reactants constsists of distinct subspecies or subinteractions depending on the order. Ifk
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 parameterk
. - 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 inproc_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))
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 bealpha * 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 ofK50
, using the same rules as for parameterk
. - 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.
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'
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. Ifk
is a float or int, then the process is homogeneous. Ifk
is a list, then the population of the reactants constsists of distinct subspecies or subinteractions depending on the order. Ifk
is a 2-tuple, then the constant is normally-distributed with a mean and standard deviation specified in the tuple's elements. Note thatk
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 bealpha * 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 ofK50
, using the same rules as for parameterk
. - 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 inproc_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])
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 bealpha * 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 ofK50
, using the same rules as for parameterk
. - 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 ofK50
, using the same rules as for parameterk
. - 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.
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}"
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. Ifk
is a float or int, then the process is homogeneous. Ifk
is a list, then the population of the reactants constsists of distinct subspecies or subinteractions depending on the order. Ifk
is a 2-tuple, then the constant is normally-distributed with a mean and standard deviation specified in the tuple's elements. Note thatk
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 bealpha * 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 ofK50
, using the same rules as for parameterk
. - 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 parameterk
. - 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 inproc_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)
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.
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.