abstochkin.base
Base class, AbStochKin, for initializing and storing all data for performing stochastic simulations using the Agent-based Kinetics method. A simulation project can be initialized and run as shown in the examples below.
Example
>>> from abstochkin import AbStochKin
>>> sim = AbStochKin()
>>> sim.add_process_from_str('A -> ', 0.2) # degradation process
>>> sim.simulate(p0={'A': 100},t_max=20,plot_backend=plotly)
>>> # All data for the above simulation is stored in `sim.sims[0]`.
>>>
>>> # Now set up a new simulation without actually running it.
>>> sim.simulate(p0={'A': 10},t_max=10,n=50,run=False,plot_backend=plotly)
>>> # All data for the new simulation is stored in `sim.sims[1]`.
>>> # The simulation can then be manually run using methods
>>> # documented in the class `Simulation`.
1""" Base class, AbStochKin, for initializing and storing all data for 2performing stochastic simulations using the Agent-based Kinetics 3method. A simulation project can be initialized and run as shown in 4the examples below. 5 6Example 7------- 8>>> from abstochkin import AbStochKin 9>>> sim = AbStochKin() 10>>> sim.add_process_from_str('A -> ', 0.2) # degradation process 11>>> sim.simulate(p0={'A': 100},t_max=20,plot_backend=plotly) 12>>> # All data for the above simulation is stored in `sim.sims[0]`. 13>>> 14>>> # Now set up a new simulation without actually running it. 15>>> sim.simulate(p0={'A': 10},t_max=10,n=50,run=False,plot_backend=plotly) 16>>> # All data for the new simulation is stored in `sim.sims[1]`. 17>>> # The simulation can then be manually run using methods 18>>> # documented in the class `Simulation`. 19 20""" 21# Copyright (c) 2024-2025, Alex Plakantonakis. 22# 23# This program is free software: you can redistribute it and/or modify 24# it under the terms of the GNU General Public License as published by 25# the Free Software Foundation, either version 3 of the License, or 26# (at your option) any later version. 27# 28# This program is distributed in the hope that it will be useful, 29# but WITHOUT ANY WARRANTY; without even the implied warranty of 30# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 31# GNU General Public License for more details. 32# 33# You should have received a copy of the GNU General Public License 34# along with this program. If not, see <http://www.gnu.org/licenses/>. 35 36import re 37from ast import literal_eval 38from concurrent.futures import ProcessPoolExecutor 39from typing import Any, Literal 40 41from .het_calcs import get_het_processes 42from .process import Process, MichaelisMentenProcess, ReversibleProcess, \ 43 RegulatedProcess, RegulatedMichaelisMentenProcess 44from .simulation import Simulation 45 46 47class AbStochKin: 48 """ Base class for Agent-based Kinetics (AbStochKin) simulator. 49 50 Attributes 51 ---------- 52 volume : float, default : None, optional 53 The volume *in liters* of the compartment in which the processes 54 are taking place. 55 volume_unit : str, default : 'L', optional 56 A string of the volume unit. The default value is 'L' for liters. 57 time_unit : str, default : 'sec', optional 58 A string of the time unit to be used for describing the kinetics 59 of the given processes. 60 processes : list 61 A list of the processes that the AbStochKin object has. 62 het_processes : list 63 A list of the processes where population heterogeneity in one 64 of the parameters is to be modeled. This list is a subset of 65 the `processes` attribute. 66 sims : list 67 A list of all simulations performed for the given set of processes. 68 Each member of the list is an object of the `Simulation` class and 69 contains all data for that simulation. 70 """ 71 72 def __init__(self, 73 volume: float = None, 74 volume_unit: str = 'L', 75 time_unit: str = 'sec'): 76 self.volume = volume 77 self.volume_unit = volume_unit 78 self.time_unit = time_unit 79 80 self.processes = list() 81 self.het_processes = list() 82 self.sims = list() 83 84 def add_processes_from_file(self, filename: str): 85 """ Add a batch of processes from a text file. """ 86 with open(filename) as f: 87 lines = f.readlines() 88 89 for line in lines: 90 self.extract_process_from_str(line) 91 92 def extract_process_from_str(self, process_str: str): 93 """ 94 Extract a process and all of its specified parameters from a string. 95 96 This functions parses a string specifying all values and parameters 97 needed to define a process. It then creates a Process object 98 based on the extracted data. 99 """ 100 process_str = process_str.replace(' ', '').replace('"', '\'') 101 102 proc_str = process_str.split(',')[0] 103 104 # Extract string parameters first 105 patt_str_params = r"(\w+)=('[\w\s,]+')" 106 str_params = re.findall(patt_str_params, process_str) 107 108 process_str_remain = re.sub(r"\w+='[\w\s,]+'", '', process_str) 109 110 patt_num_params = r"(\w+)=([\[\(,.\s\d\)\]]+)" 111 num_params = re.findall(patt_num_params, process_str_remain) 112 113 all_params = list() 114 for name, val_str in num_params + str_params: 115 while val_str.endswith(','): 116 val_str = val_str[:-1] 117 all_params.append((name, literal_eval(val_str))) 118 119 self.add_process_from_str(proc_str, **dict(all_params)) 120 121 def add_process_from_str(self, 122 process_str: str, 123 /, 124 k: float | int | list[float | int, ...] | tuple[float | int, float | int], 125 **kwargs): 126 """ 127 Add a process by specifying a string: 'reactants -> products'. 128 Additional arguments determine if a specialized process 129 (such as a reversible, regulated, or Michaelis-Menten process) 130 is to be defined. 131 """ 132 kwargs.setdefault('volume', self.volume) 133 134 if '<->' in process_str or 'k_rev' in kwargs: # reversible process 135 self.processes.append(ReversibleProcess.from_string(process_str, k, **kwargs)) 136 elif 'catalyst' in kwargs and 'Km' in kwargs and 'regulating_species' not in kwargs: 137 self.processes.append(MichaelisMentenProcess.from_string(process_str, k, **kwargs)) 138 elif 'regulating_species' in kwargs and 'alpha' in kwargs and 'K50' in kwargs and 'nH' in kwargs: 139 if 'catalyst' in kwargs and 'Km' in kwargs: # Regulated MM process 140 self.processes.append(RegulatedMichaelisMentenProcess.from_string(process_str, 141 k, **kwargs)) 142 else: # Regulated process 143 self.processes.append(RegulatedProcess.from_string(process_str, k, **kwargs)) 144 else: # simple unidirectional process 145 self.processes.append(Process.from_string(process_str, k, **kwargs)) 146 147 def add_process(self, 148 /, 149 reactants: dict, 150 products: dict, 151 k: float | int | list[float | int, ...] | tuple[float | int, float | int], 152 **kwargs): 153 """ 154 Add a process by using a dictionary for the reactants and products. 155 Additional arguments determine if a specialized process 156 (such as a reversible, regulated, or Michaelis-Menten process) 157 is to be defined. 158 """ 159 kwargs.setdefault('volume', self.volume) 160 161 if 'k_rev' in kwargs: # reversible process 162 self.processes.append(ReversibleProcess(reactants, products, k, **kwargs)) 163 elif 'catalyst' in kwargs and 'Km' in kwargs and 'regulating_species' not in kwargs: 164 self.processes.append(MichaelisMentenProcess(reactants, products, k, **kwargs)) 165 elif 'regulating_species' in kwargs and 'alpha' in kwargs and 'K50' in kwargs and 'nH' in kwargs: 166 if 'catalyst' in kwargs and 'Km' in kwargs: # Regulated MM process 167 self.processes.append( 168 RegulatedMichaelisMentenProcess(reactants, products, k, **kwargs)) 169 else: # Regulated process 170 self.processes.append(RegulatedProcess(reactants, products, k, **kwargs)) 171 else: # simple unidirectional process 172 self.processes.append(Process(reactants, products, k, **kwargs)) 173 174 def del_process_from_str(self, 175 process_str: str, 176 /, 177 k: float | int | list[float | int, ...] | tuple[float | int], 178 **kwargs): 179 """ Delete a process by specifying a string: 'reactants -> products'. """ 180 kwargs.setdefault('volume', self.volume) 181 182 try: 183 if '<->' in process_str or 'k_rev' in kwargs: # reversible process 184 self.processes.remove(ReversibleProcess.from_string(process_str, k, **kwargs)) 185 elif 'catalyst' in kwargs and 'Km' in kwargs and 'regulating_species' not in kwargs: 186 self.processes.remove(MichaelisMentenProcess.from_string(process_str, k, **kwargs)) 187 elif 'regulating_species' in kwargs and 'alpha' in kwargs and 'K50' in kwargs and 'nH' in kwargs: 188 if 'catalyst' in kwargs and 'Km' in kwargs: # Regulated MM process 189 self.processes.remove( 190 RegulatedMichaelisMentenProcess.from_string(process_str, k, **kwargs)) 191 else: # Regulated process 192 self.processes.remove(RegulatedProcess.from_string(process_str, k, **kwargs)) 193 else: # simple unidirectional process 194 self.processes.remove(Process.from_string(process_str, k, **kwargs)) 195 except ValueError: 196 print("Process to be removed was not found.") 197 else: 198 print(f"Removed: {process_str}, k = {k}, kwargs = {kwargs}") 199 200 def del_process(self, 201 /, 202 reactants: dict, 203 products: dict, 204 k: float | int | list[float | int, ...] | tuple[float | int, float | int], 205 **kwargs): 206 """ Delete a process by using a dictionary for the reactants and products. """ 207 kwargs.setdefault('volume', self.volume) 208 209 try: 210 if 'k_rev' in kwargs: # reversible process 211 self.processes.remove(ReversibleProcess(reactants, products, k, **kwargs)) 212 elif 'catalyst' in kwargs and 'Km' in kwargs and 'regulating_species' not in kwargs: 213 self.processes.remove(MichaelisMentenProcess(reactants, products, k, **kwargs)) 214 elif 'regulating_species' in kwargs and 'alpha' in kwargs and 'K50' in kwargs and 'nH' in kwargs: 215 if 'catalyst' in kwargs and 'Km' in kwargs: # Regulated MM process 216 self.processes.remove( 217 RegulatedMichaelisMentenProcess(reactants, products, k, **kwargs)) 218 else: # Regulated process 219 self.processes.remove(RegulatedProcess(reactants, products, k, **kwargs)) 220 else: # simple unidirectional process 221 self.processes.remove(Process(reactants, products, k, **kwargs)) 222 except ValueError: 223 print("Process to be removed was not found.") 224 else: 225 lhs, rhs = Process(reactants, products, k)._reconstruct_string() 226 print(f"Removed: " + " -> ".join([lhs, rhs]) + f", k = {k}, kwargs = {kwargs}") 227 228 def simulate(self, 229 /, 230 p0: dict, 231 t_max: float | int, 232 dt: float = 0.01, 233 n: int = 100, 234 *, 235 random_seed: int = 19, 236 solve_odes: bool = True, 237 ode_method: str = 'RK45', 238 run: bool = True, 239 show_plots: bool = True, 240 plot_backend: Literal['matplotlib', 'plotly'] = 'matplotlib', 241 multithreading: bool = True, 242 max_agents_by_species: dict = None, 243 max_agents_multiplier: int = 2, 244 _return_simulation: bool = False): 245 """ 246 Start an AbStochKin simulation by creating an instance of the class 247 `Simulation`. The resulting object is appended to the list 248 in the class attribute `AbStochKin.sims`. 249 250 Parameters 251 ---------- 252 p0 : dict[str: int] 253 Dictionary specifying the initial population sizes of all 254 species in the given processes. 255 t_max : float or int 256 Numerical value of the end of simulated time in the units 257 specified in the class attribute `AbStochKin.time_unit`. 258 dt : float, default: 0.1, optional 259 The duration of the time interval that the simulation's 260 algorithm considers. The current implementation only 261 supports a fixed time step interval whose value is `dt`. 262 n : int, default: 100, optional 263 The number of repetitions of the simulation to be performed. 264 random_seed : int, default: 19, optional 265 A number used to seed the random number generator. 266 solve_odes : bool, default: True, optional 267 Specify whether to numerically solve the system of 268 ODEs defined from the given set of processes. 269 ode_method : str, default: RK45, optional 270 Available ODE methods: RK45, RK23, DOP853, Radau, BDF, LSODA. 271 run : bool, default: True, optional 272 Specify whether to run an AbStochKin simulation. 273 show_plots : bool, default: True, optional 274 Specify whether to graph the results of the AbStochKin simulation. 275 plot_backend : str, default: 'matplotlib', optional 276 `Matplotlib` and `Plotly` are currently supported. 277 multithreading : bool, default: True, optional 278 Specify whether to parallelize the simulation 279 using multithreading. If `False`, the ensemble 280 of simulations is run sequentially. 281 max_agents_by_species : None or dict, default: dict 282 Specification of the maximum number of agents that each 283 species should have when running the simulation. 284 If `None`, that a default approach will 285 be taken by the class `Simulation` and the number 286 for each species will be automatically determined 287 (see method `Simulation._setup_runtime_data()` for details). 288 The entries in the dictionary should be 289 `species name (string): number (int)`. 290 max_agents_multiplier : float or int, default: 2 291 This parameter is used to calculate the maximum number of 292 agents of each species that the simulation engine allocates 293 memory for. This be determined by multiplying the maximum value 294 of the ODE time trajectory for this species by the 295 multiplier value specified here. 296 _return_simulation : bool 297 Determines if the `self.simulate` method returns a `Simulation` 298 object or appends it to the list `self.sims`. 299 Returning a `Simulation` object is needed when calling the method 300 `simulate_series_in_parallel`. 301 """ 302 303 if max_agents_by_species is None: 304 max_agents_by_species = dict() 305 306 self.het_processes = get_het_processes(self.processes) 307 308 sim = Simulation(p0, 309 t_max, 310 dt, 311 n, 312 self.processes, 313 random_state=random_seed, 314 do_solve_ODEs=solve_odes, 315 ODE_method=ode_method, 316 do_run=run, 317 show_graphs=show_plots, 318 graph_backend=plot_backend, 319 use_multithreading=multithreading, 320 max_agents=max_agents_by_species, 321 max_agents_multiplier=max_agents_multiplier, 322 time_unit=self.time_unit) 323 324 if _return_simulation: 325 assert run, "Must run individual simulations if a series of " \ 326 "simulations is to be run with multiprocessing." 327 328 # Set un-pickleable objects to None for data serialization to work 329 sim.algo_sequence = None 330 sim.progress_bar = None 331 332 return sim 333 else: 334 self.sims.append(sim) 335 336 def simulate_series_in_parallel(self, 337 series_kwargs: list[dict[str, Any], ...], 338 *, 339 max_workers: int = None): 340 """ 341 Perform a series of simulations in parallel by initializing 342 separate processes. Each process runs a simulation and appends 343 a `Simulation` object in the list `self.sims`. 344 345 Parameters 346 ---------- 347 series_kwargs : list of dict 348 A list containing dictionaries of the keyword arguments for 349 performing each simulation in the series. The number of elements 350 in the list is the number of simulations that will be run. 351 max_workers : int, default: None 352 The maximum number of processes to be used for performing 353 the given series of simulations. If None, then as many worker 354 processes will be created as the machine has processors. 355 356 Examples 357 -------- 358 - Run a series of simulations by varying the initial population size of A. 359 >>> from abstochkin import AbStochKin 360 >>> 361 >>> sim = AbStochKin() 362 >>> sim.add_process_from_str("A -> B", 0.3, catalyst='E', Km=10) 363 >>> series_kwargs = [{"p0": {'A': i, 'B': 0, 'E': 10}, "t_max": 10} for i in range(40, 51)] 364 >>> sim.simulate_series_in_parallel(series_kwargs) 365 """ 366 extra_opts = {"show_plots": False, "_return_simulation": True} 367 with ProcessPoolExecutor(max_workers=max_workers) as executor: 368 futures = [executor.submit(self.simulate, **(kwargs | extra_opts)) for kwargs in 369 series_kwargs] 370 for future in futures: 371 self.sims.append(future.result())
48class AbStochKin: 49 """ Base class for Agent-based Kinetics (AbStochKin) simulator. 50 51 Attributes 52 ---------- 53 volume : float, default : None, optional 54 The volume *in liters* of the compartment in which the processes 55 are taking place. 56 volume_unit : str, default : 'L', optional 57 A string of the volume unit. The default value is 'L' for liters. 58 time_unit : str, default : 'sec', optional 59 A string of the time unit to be used for describing the kinetics 60 of the given processes. 61 processes : list 62 A list of the processes that the AbStochKin object has. 63 het_processes : list 64 A list of the processes where population heterogeneity in one 65 of the parameters is to be modeled. This list is a subset of 66 the `processes` attribute. 67 sims : list 68 A list of all simulations performed for the given set of processes. 69 Each member of the list is an object of the `Simulation` class and 70 contains all data for that simulation. 71 """ 72 73 def __init__(self, 74 volume: float = None, 75 volume_unit: str = 'L', 76 time_unit: str = 'sec'): 77 self.volume = volume 78 self.volume_unit = volume_unit 79 self.time_unit = time_unit 80 81 self.processes = list() 82 self.het_processes = list() 83 self.sims = list() 84 85 def add_processes_from_file(self, filename: str): 86 """ Add a batch of processes from a text file. """ 87 with open(filename) as f: 88 lines = f.readlines() 89 90 for line in lines: 91 self.extract_process_from_str(line) 92 93 def extract_process_from_str(self, process_str: str): 94 """ 95 Extract a process and all of its specified parameters from a string. 96 97 This functions parses a string specifying all values and parameters 98 needed to define a process. It then creates a Process object 99 based on the extracted data. 100 """ 101 process_str = process_str.replace(' ', '').replace('"', '\'') 102 103 proc_str = process_str.split(',')[0] 104 105 # Extract string parameters first 106 patt_str_params = r"(\w+)=('[\w\s,]+')" 107 str_params = re.findall(patt_str_params, process_str) 108 109 process_str_remain = re.sub(r"\w+='[\w\s,]+'", '', process_str) 110 111 patt_num_params = r"(\w+)=([\[\(,.\s\d\)\]]+)" 112 num_params = re.findall(patt_num_params, process_str_remain) 113 114 all_params = list() 115 for name, val_str in num_params + str_params: 116 while val_str.endswith(','): 117 val_str = val_str[:-1] 118 all_params.append((name, literal_eval(val_str))) 119 120 self.add_process_from_str(proc_str, **dict(all_params)) 121 122 def add_process_from_str(self, 123 process_str: str, 124 /, 125 k: float | int | list[float | int, ...] | tuple[float | int, float | int], 126 **kwargs): 127 """ 128 Add a process by specifying a string: 'reactants -> products'. 129 Additional arguments determine if a specialized process 130 (such as a reversible, regulated, or Michaelis-Menten process) 131 is to be defined. 132 """ 133 kwargs.setdefault('volume', self.volume) 134 135 if '<->' in process_str or 'k_rev' in kwargs: # reversible process 136 self.processes.append(ReversibleProcess.from_string(process_str, k, **kwargs)) 137 elif 'catalyst' in kwargs and 'Km' in kwargs and 'regulating_species' not in kwargs: 138 self.processes.append(MichaelisMentenProcess.from_string(process_str, k, **kwargs)) 139 elif 'regulating_species' in kwargs and 'alpha' in kwargs and 'K50' in kwargs and 'nH' in kwargs: 140 if 'catalyst' in kwargs and 'Km' in kwargs: # Regulated MM process 141 self.processes.append(RegulatedMichaelisMentenProcess.from_string(process_str, 142 k, **kwargs)) 143 else: # Regulated process 144 self.processes.append(RegulatedProcess.from_string(process_str, k, **kwargs)) 145 else: # simple unidirectional process 146 self.processes.append(Process.from_string(process_str, k, **kwargs)) 147 148 def add_process(self, 149 /, 150 reactants: dict, 151 products: dict, 152 k: float | int | list[float | int, ...] | tuple[float | int, float | int], 153 **kwargs): 154 """ 155 Add a process by using a dictionary for the reactants and products. 156 Additional arguments determine if a specialized process 157 (such as a reversible, regulated, or Michaelis-Menten process) 158 is to be defined. 159 """ 160 kwargs.setdefault('volume', self.volume) 161 162 if 'k_rev' in kwargs: # reversible process 163 self.processes.append(ReversibleProcess(reactants, products, k, **kwargs)) 164 elif 'catalyst' in kwargs and 'Km' in kwargs and 'regulating_species' not in kwargs: 165 self.processes.append(MichaelisMentenProcess(reactants, products, k, **kwargs)) 166 elif 'regulating_species' in kwargs and 'alpha' in kwargs and 'K50' in kwargs and 'nH' in kwargs: 167 if 'catalyst' in kwargs and 'Km' in kwargs: # Regulated MM process 168 self.processes.append( 169 RegulatedMichaelisMentenProcess(reactants, products, k, **kwargs)) 170 else: # Regulated process 171 self.processes.append(RegulatedProcess(reactants, products, k, **kwargs)) 172 else: # simple unidirectional process 173 self.processes.append(Process(reactants, products, k, **kwargs)) 174 175 def del_process_from_str(self, 176 process_str: str, 177 /, 178 k: float | int | list[float | int, ...] | tuple[float | int], 179 **kwargs): 180 """ Delete a process by specifying a string: 'reactants -> products'. """ 181 kwargs.setdefault('volume', self.volume) 182 183 try: 184 if '<->' in process_str or 'k_rev' in kwargs: # reversible process 185 self.processes.remove(ReversibleProcess.from_string(process_str, k, **kwargs)) 186 elif 'catalyst' in kwargs and 'Km' in kwargs and 'regulating_species' not in kwargs: 187 self.processes.remove(MichaelisMentenProcess.from_string(process_str, k, **kwargs)) 188 elif 'regulating_species' in kwargs and 'alpha' in kwargs and 'K50' in kwargs and 'nH' in kwargs: 189 if 'catalyst' in kwargs and 'Km' in kwargs: # Regulated MM process 190 self.processes.remove( 191 RegulatedMichaelisMentenProcess.from_string(process_str, k, **kwargs)) 192 else: # Regulated process 193 self.processes.remove(RegulatedProcess.from_string(process_str, k, **kwargs)) 194 else: # simple unidirectional process 195 self.processes.remove(Process.from_string(process_str, k, **kwargs)) 196 except ValueError: 197 print("Process to be removed was not found.") 198 else: 199 print(f"Removed: {process_str}, k = {k}, kwargs = {kwargs}") 200 201 def del_process(self, 202 /, 203 reactants: dict, 204 products: dict, 205 k: float | int | list[float | int, ...] | tuple[float | int, float | int], 206 **kwargs): 207 """ Delete a process by using a dictionary for the reactants and products. """ 208 kwargs.setdefault('volume', self.volume) 209 210 try: 211 if 'k_rev' in kwargs: # reversible process 212 self.processes.remove(ReversibleProcess(reactants, products, k, **kwargs)) 213 elif 'catalyst' in kwargs and 'Km' in kwargs and 'regulating_species' not in kwargs: 214 self.processes.remove(MichaelisMentenProcess(reactants, products, k, **kwargs)) 215 elif 'regulating_species' in kwargs and 'alpha' in kwargs and 'K50' in kwargs and 'nH' in kwargs: 216 if 'catalyst' in kwargs and 'Km' in kwargs: # Regulated MM process 217 self.processes.remove( 218 RegulatedMichaelisMentenProcess(reactants, products, k, **kwargs)) 219 else: # Regulated process 220 self.processes.remove(RegulatedProcess(reactants, products, k, **kwargs)) 221 else: # simple unidirectional process 222 self.processes.remove(Process(reactants, products, k, **kwargs)) 223 except ValueError: 224 print("Process to be removed was not found.") 225 else: 226 lhs, rhs = Process(reactants, products, k)._reconstruct_string() 227 print(f"Removed: " + " -> ".join([lhs, rhs]) + f", k = {k}, kwargs = {kwargs}") 228 229 def simulate(self, 230 /, 231 p0: dict, 232 t_max: float | int, 233 dt: float = 0.01, 234 n: int = 100, 235 *, 236 random_seed: int = 19, 237 solve_odes: bool = True, 238 ode_method: str = 'RK45', 239 run: bool = True, 240 show_plots: bool = True, 241 plot_backend: Literal['matplotlib', 'plotly'] = 'matplotlib', 242 multithreading: bool = True, 243 max_agents_by_species: dict = None, 244 max_agents_multiplier: int = 2, 245 _return_simulation: bool = False): 246 """ 247 Start an AbStochKin simulation by creating an instance of the class 248 `Simulation`. The resulting object is appended to the list 249 in the class attribute `AbStochKin.sims`. 250 251 Parameters 252 ---------- 253 p0 : dict[str: int] 254 Dictionary specifying the initial population sizes of all 255 species in the given processes. 256 t_max : float or int 257 Numerical value of the end of simulated time in the units 258 specified in the class attribute `AbStochKin.time_unit`. 259 dt : float, default: 0.1, optional 260 The duration of the time interval that the simulation's 261 algorithm considers. The current implementation only 262 supports a fixed time step interval whose value is `dt`. 263 n : int, default: 100, optional 264 The number of repetitions of the simulation to be performed. 265 random_seed : int, default: 19, optional 266 A number used to seed the random number generator. 267 solve_odes : bool, default: True, optional 268 Specify whether to numerically solve the system of 269 ODEs defined from the given set of processes. 270 ode_method : str, default: RK45, optional 271 Available ODE methods: RK45, RK23, DOP853, Radau, BDF, LSODA. 272 run : bool, default: True, optional 273 Specify whether to run an AbStochKin simulation. 274 show_plots : bool, default: True, optional 275 Specify whether to graph the results of the AbStochKin simulation. 276 plot_backend : str, default: 'matplotlib', optional 277 `Matplotlib` and `Plotly` are currently supported. 278 multithreading : bool, default: True, optional 279 Specify whether to parallelize the simulation 280 using multithreading. If `False`, the ensemble 281 of simulations is run sequentially. 282 max_agents_by_species : None or dict, default: dict 283 Specification of the maximum number of agents that each 284 species should have when running the simulation. 285 If `None`, that a default approach will 286 be taken by the class `Simulation` and the number 287 for each species will be automatically determined 288 (see method `Simulation._setup_runtime_data()` for details). 289 The entries in the dictionary should be 290 `species name (string): number (int)`. 291 max_agents_multiplier : float or int, default: 2 292 This parameter is used to calculate the maximum number of 293 agents of each species that the simulation engine allocates 294 memory for. This be determined by multiplying the maximum value 295 of the ODE time trajectory for this species by the 296 multiplier value specified here. 297 _return_simulation : bool 298 Determines if the `self.simulate` method returns a `Simulation` 299 object or appends it to the list `self.sims`. 300 Returning a `Simulation` object is needed when calling the method 301 `simulate_series_in_parallel`. 302 """ 303 304 if max_agents_by_species is None: 305 max_agents_by_species = dict() 306 307 self.het_processes = get_het_processes(self.processes) 308 309 sim = Simulation(p0, 310 t_max, 311 dt, 312 n, 313 self.processes, 314 random_state=random_seed, 315 do_solve_ODEs=solve_odes, 316 ODE_method=ode_method, 317 do_run=run, 318 show_graphs=show_plots, 319 graph_backend=plot_backend, 320 use_multithreading=multithreading, 321 max_agents=max_agents_by_species, 322 max_agents_multiplier=max_agents_multiplier, 323 time_unit=self.time_unit) 324 325 if _return_simulation: 326 assert run, "Must run individual simulations if a series of " \ 327 "simulations is to be run with multiprocessing." 328 329 # Set un-pickleable objects to None for data serialization to work 330 sim.algo_sequence = None 331 sim.progress_bar = None 332 333 return sim 334 else: 335 self.sims.append(sim) 336 337 def simulate_series_in_parallel(self, 338 series_kwargs: list[dict[str, Any], ...], 339 *, 340 max_workers: int = None): 341 """ 342 Perform a series of simulations in parallel by initializing 343 separate processes. Each process runs a simulation and appends 344 a `Simulation` object in the list `self.sims`. 345 346 Parameters 347 ---------- 348 series_kwargs : list of dict 349 A list containing dictionaries of the keyword arguments for 350 performing each simulation in the series. The number of elements 351 in the list is the number of simulations that will be run. 352 max_workers : int, default: None 353 The maximum number of processes to be used for performing 354 the given series of simulations. If None, then as many worker 355 processes will be created as the machine has processors. 356 357 Examples 358 -------- 359 - Run a series of simulations by varying the initial population size of A. 360 >>> from abstochkin import AbStochKin 361 >>> 362 >>> sim = AbStochKin() 363 >>> sim.add_process_from_str("A -> B", 0.3, catalyst='E', Km=10) 364 >>> series_kwargs = [{"p0": {'A': i, 'B': 0, 'E': 10}, "t_max": 10} for i in range(40, 51)] 365 >>> sim.simulate_series_in_parallel(series_kwargs) 366 """ 367 extra_opts = {"show_plots": False, "_return_simulation": True} 368 with ProcessPoolExecutor(max_workers=max_workers) as executor: 369 futures = [executor.submit(self.simulate, **(kwargs | extra_opts)) for kwargs in 370 series_kwargs] 371 for future in futures: 372 self.sims.append(future.result())
Base class for Agent-based Kinetics (AbStochKin) simulator.
Attributes
- volume : float, default (None, optional): The volume in liters of the compartment in which the processes are taking place.
- volume_unit : str, default ('L', optional): A string of the volume unit. The default value is 'L' for liters.
- time_unit : str, default ('sec', optional): A string of the time unit to be used for describing the kinetics of the given processes.
- processes (list): A list of the processes that the AbStochKin object has.
- het_processes (list):
A list of the processes where population heterogeneity in one
of the parameters is to be modeled. This list is a subset of
the
processes
attribute. - sims (list):
A list of all simulations performed for the given set of processes.
Each member of the list is an object of the
Simulation
class and contains all data for that simulation.
85 def add_processes_from_file(self, filename: str): 86 """ Add a batch of processes from a text file. """ 87 with open(filename) as f: 88 lines = f.readlines() 89 90 for line in lines: 91 self.extract_process_from_str(line)
Add a batch of processes from a text file.
93 def extract_process_from_str(self, process_str: str): 94 """ 95 Extract a process and all of its specified parameters from a string. 96 97 This functions parses a string specifying all values and parameters 98 needed to define a process. It then creates a Process object 99 based on the extracted data. 100 """ 101 process_str = process_str.replace(' ', '').replace('"', '\'') 102 103 proc_str = process_str.split(',')[0] 104 105 # Extract string parameters first 106 patt_str_params = r"(\w+)=('[\w\s,]+')" 107 str_params = re.findall(patt_str_params, process_str) 108 109 process_str_remain = re.sub(r"\w+='[\w\s,]+'", '', process_str) 110 111 patt_num_params = r"(\w+)=([\[\(,.\s\d\)\]]+)" 112 num_params = re.findall(patt_num_params, process_str_remain) 113 114 all_params = list() 115 for name, val_str in num_params + str_params: 116 while val_str.endswith(','): 117 val_str = val_str[:-1] 118 all_params.append((name, literal_eval(val_str))) 119 120 self.add_process_from_str(proc_str, **dict(all_params))
Extract a process and all of its specified parameters from a string.
This functions parses a string specifying all values and parameters needed to define a process. It then creates a Process object based on the extracted data.
122 def add_process_from_str(self, 123 process_str: str, 124 /, 125 k: float | int | list[float | int, ...] | tuple[float | int, float | int], 126 **kwargs): 127 """ 128 Add a process by specifying a string: 'reactants -> products'. 129 Additional arguments determine if a specialized process 130 (such as a reversible, regulated, or Michaelis-Menten process) 131 is to be defined. 132 """ 133 kwargs.setdefault('volume', self.volume) 134 135 if '<->' in process_str or 'k_rev' in kwargs: # reversible process 136 self.processes.append(ReversibleProcess.from_string(process_str, k, **kwargs)) 137 elif 'catalyst' in kwargs and 'Km' in kwargs and 'regulating_species' not in kwargs: 138 self.processes.append(MichaelisMentenProcess.from_string(process_str, k, **kwargs)) 139 elif 'regulating_species' in kwargs and 'alpha' in kwargs and 'K50' in kwargs and 'nH' in kwargs: 140 if 'catalyst' in kwargs and 'Km' in kwargs: # Regulated MM process 141 self.processes.append(RegulatedMichaelisMentenProcess.from_string(process_str, 142 k, **kwargs)) 143 else: # Regulated process 144 self.processes.append(RegulatedProcess.from_string(process_str, k, **kwargs)) 145 else: # simple unidirectional process 146 self.processes.append(Process.from_string(process_str, k, **kwargs))
Add a process by specifying a string: 'reactants -> products'. Additional arguments determine if a specialized process (such as a reversible, regulated, or Michaelis-Menten process) is to be defined.
148 def add_process(self, 149 /, 150 reactants: dict, 151 products: dict, 152 k: float | int | list[float | int, ...] | tuple[float | int, float | int], 153 **kwargs): 154 """ 155 Add a process by using a dictionary for the reactants and products. 156 Additional arguments determine if a specialized process 157 (such as a reversible, regulated, or Michaelis-Menten process) 158 is to be defined. 159 """ 160 kwargs.setdefault('volume', self.volume) 161 162 if 'k_rev' in kwargs: # reversible process 163 self.processes.append(ReversibleProcess(reactants, products, k, **kwargs)) 164 elif 'catalyst' in kwargs and 'Km' in kwargs and 'regulating_species' not in kwargs: 165 self.processes.append(MichaelisMentenProcess(reactants, products, k, **kwargs)) 166 elif 'regulating_species' in kwargs and 'alpha' in kwargs and 'K50' in kwargs and 'nH' in kwargs: 167 if 'catalyst' in kwargs and 'Km' in kwargs: # Regulated MM process 168 self.processes.append( 169 RegulatedMichaelisMentenProcess(reactants, products, k, **kwargs)) 170 else: # Regulated process 171 self.processes.append(RegulatedProcess(reactants, products, k, **kwargs)) 172 else: # simple unidirectional process 173 self.processes.append(Process(reactants, products, k, **kwargs))
Add a process by using a dictionary for the reactants and products. Additional arguments determine if a specialized process (such as a reversible, regulated, or Michaelis-Menten process) is to be defined.
175 def del_process_from_str(self, 176 process_str: str, 177 /, 178 k: float | int | list[float | int, ...] | tuple[float | int], 179 **kwargs): 180 """ Delete a process by specifying a string: 'reactants -> products'. """ 181 kwargs.setdefault('volume', self.volume) 182 183 try: 184 if '<->' in process_str or 'k_rev' in kwargs: # reversible process 185 self.processes.remove(ReversibleProcess.from_string(process_str, k, **kwargs)) 186 elif 'catalyst' in kwargs and 'Km' in kwargs and 'regulating_species' not in kwargs: 187 self.processes.remove(MichaelisMentenProcess.from_string(process_str, k, **kwargs)) 188 elif 'regulating_species' in kwargs and 'alpha' in kwargs and 'K50' in kwargs and 'nH' in kwargs: 189 if 'catalyst' in kwargs and 'Km' in kwargs: # Regulated MM process 190 self.processes.remove( 191 RegulatedMichaelisMentenProcess.from_string(process_str, k, **kwargs)) 192 else: # Regulated process 193 self.processes.remove(RegulatedProcess.from_string(process_str, k, **kwargs)) 194 else: # simple unidirectional process 195 self.processes.remove(Process.from_string(process_str, k, **kwargs)) 196 except ValueError: 197 print("Process to be removed was not found.") 198 else: 199 print(f"Removed: {process_str}, k = {k}, kwargs = {kwargs}")
Delete a process by specifying a string: 'reactants -> products'.
201 def del_process(self, 202 /, 203 reactants: dict, 204 products: dict, 205 k: float | int | list[float | int, ...] | tuple[float | int, float | int], 206 **kwargs): 207 """ Delete a process by using a dictionary for the reactants and products. """ 208 kwargs.setdefault('volume', self.volume) 209 210 try: 211 if 'k_rev' in kwargs: # reversible process 212 self.processes.remove(ReversibleProcess(reactants, products, k, **kwargs)) 213 elif 'catalyst' in kwargs and 'Km' in kwargs and 'regulating_species' not in kwargs: 214 self.processes.remove(MichaelisMentenProcess(reactants, products, k, **kwargs)) 215 elif 'regulating_species' in kwargs and 'alpha' in kwargs and 'K50' in kwargs and 'nH' in kwargs: 216 if 'catalyst' in kwargs and 'Km' in kwargs: # Regulated MM process 217 self.processes.remove( 218 RegulatedMichaelisMentenProcess(reactants, products, k, **kwargs)) 219 else: # Regulated process 220 self.processes.remove(RegulatedProcess(reactants, products, k, **kwargs)) 221 else: # simple unidirectional process 222 self.processes.remove(Process(reactants, products, k, **kwargs)) 223 except ValueError: 224 print("Process to be removed was not found.") 225 else: 226 lhs, rhs = Process(reactants, products, k)._reconstruct_string() 227 print(f"Removed: " + " -> ".join([lhs, rhs]) + f", k = {k}, kwargs = {kwargs}")
Delete a process by using a dictionary for the reactants and products.
229 def simulate(self, 230 /, 231 p0: dict, 232 t_max: float | int, 233 dt: float = 0.01, 234 n: int = 100, 235 *, 236 random_seed: int = 19, 237 solve_odes: bool = True, 238 ode_method: str = 'RK45', 239 run: bool = True, 240 show_plots: bool = True, 241 plot_backend: Literal['matplotlib', 'plotly'] = 'matplotlib', 242 multithreading: bool = True, 243 max_agents_by_species: dict = None, 244 max_agents_multiplier: int = 2, 245 _return_simulation: bool = False): 246 """ 247 Start an AbStochKin simulation by creating an instance of the class 248 `Simulation`. The resulting object is appended to the list 249 in the class attribute `AbStochKin.sims`. 250 251 Parameters 252 ---------- 253 p0 : dict[str: int] 254 Dictionary specifying the initial population sizes of all 255 species in the given processes. 256 t_max : float or int 257 Numerical value of the end of simulated time in the units 258 specified in the class attribute `AbStochKin.time_unit`. 259 dt : float, default: 0.1, optional 260 The duration of the time interval that the simulation's 261 algorithm considers. The current implementation only 262 supports a fixed time step interval whose value is `dt`. 263 n : int, default: 100, optional 264 The number of repetitions of the simulation to be performed. 265 random_seed : int, default: 19, optional 266 A number used to seed the random number generator. 267 solve_odes : bool, default: True, optional 268 Specify whether to numerically solve the system of 269 ODEs defined from the given set of processes. 270 ode_method : str, default: RK45, optional 271 Available ODE methods: RK45, RK23, DOP853, Radau, BDF, LSODA. 272 run : bool, default: True, optional 273 Specify whether to run an AbStochKin simulation. 274 show_plots : bool, default: True, optional 275 Specify whether to graph the results of the AbStochKin simulation. 276 plot_backend : str, default: 'matplotlib', optional 277 `Matplotlib` and `Plotly` are currently supported. 278 multithreading : bool, default: True, optional 279 Specify whether to parallelize the simulation 280 using multithreading. If `False`, the ensemble 281 of simulations is run sequentially. 282 max_agents_by_species : None or dict, default: dict 283 Specification of the maximum number of agents that each 284 species should have when running the simulation. 285 If `None`, that a default approach will 286 be taken by the class `Simulation` and the number 287 for each species will be automatically determined 288 (see method `Simulation._setup_runtime_data()` for details). 289 The entries in the dictionary should be 290 `species name (string): number (int)`. 291 max_agents_multiplier : float or int, default: 2 292 This parameter is used to calculate the maximum number of 293 agents of each species that the simulation engine allocates 294 memory for. This be determined by multiplying the maximum value 295 of the ODE time trajectory for this species by the 296 multiplier value specified here. 297 _return_simulation : bool 298 Determines if the `self.simulate` method returns a `Simulation` 299 object or appends it to the list `self.sims`. 300 Returning a `Simulation` object is needed when calling the method 301 `simulate_series_in_parallel`. 302 """ 303 304 if max_agents_by_species is None: 305 max_agents_by_species = dict() 306 307 self.het_processes = get_het_processes(self.processes) 308 309 sim = Simulation(p0, 310 t_max, 311 dt, 312 n, 313 self.processes, 314 random_state=random_seed, 315 do_solve_ODEs=solve_odes, 316 ODE_method=ode_method, 317 do_run=run, 318 show_graphs=show_plots, 319 graph_backend=plot_backend, 320 use_multithreading=multithreading, 321 max_agents=max_agents_by_species, 322 max_agents_multiplier=max_agents_multiplier, 323 time_unit=self.time_unit) 324 325 if _return_simulation: 326 assert run, "Must run individual simulations if a series of " \ 327 "simulations is to be run with multiprocessing." 328 329 # Set un-pickleable objects to None for data serialization to work 330 sim.algo_sequence = None 331 sim.progress_bar = None 332 333 return sim 334 else: 335 self.sims.append(sim)
Start an AbStochKin simulation by creating an instance of the class
Simulation
. The resulting object is appended to the list
in the class attribute AbStochKin.sims
.
Parameters
- p0 : dict[str (int]): Dictionary specifying the initial population sizes of all species in the given processes.
- t_max (float or int):
Numerical value of the end of simulated time in the units
specified in the class attribute
AbStochKin.time_unit
. - dt : float, default (0.1, optional):
The duration of the time interval that the simulation's
algorithm considers. The current implementation only
supports a fixed time step interval whose value is
dt
. - n : int, default (100, optional): The number of repetitions of the simulation to be performed.
- random_seed : int, default (19, optional): A number used to seed the random number generator.
- solve_odes : bool, default (True, optional): Specify whether to numerically solve the system of ODEs defined from the given set of processes.
- ode_method : str, default (RK45, optional): Available ODE methods: RK45, RK23, DOP853, Radau, BDF, LSODA.
- run : bool, default (True, optional): Specify whether to run an AbStochKin simulation.
- show_plots : bool, default (True, optional): Specify whether to graph the results of the AbStochKin simulation.
- plot_backend : str, default ('matplotlib', optional):
Matplotlib
andPlotly
are currently supported. - multithreading : bool, default (True, optional):
Specify whether to parallelize the simulation
using multithreading. If
False
, the ensemble of simulations is run sequentially. - max_agents_by_species : None or dict, default (dict):
Specification of the maximum number of agents that each
species should have when running the simulation.
If
None
, that a default approach will be taken by the classSimulation
and the number for each species will be automatically determined (see methodSimulation._setup_runtime_data()
for details). The entries in the dictionary should bespecies name (string): number (int)
. - max_agents_multiplier : float or int, default (2): This parameter is used to calculate the maximum number of agents of each species that the simulation engine allocates memory for. This be determined by multiplying the maximum value of the ODE time trajectory for this species by the multiplier value specified here.
- _return_simulation (bool):
Determines if the
self.simulate
method returns aSimulation
object or appends it to the listself.sims
. Returning aSimulation
object is needed when calling the methodsimulate_series_in_parallel
.
337 def simulate_series_in_parallel(self, 338 series_kwargs: list[dict[str, Any], ...], 339 *, 340 max_workers: int = None): 341 """ 342 Perform a series of simulations in parallel by initializing 343 separate processes. Each process runs a simulation and appends 344 a `Simulation` object in the list `self.sims`. 345 346 Parameters 347 ---------- 348 series_kwargs : list of dict 349 A list containing dictionaries of the keyword arguments for 350 performing each simulation in the series. The number of elements 351 in the list is the number of simulations that will be run. 352 max_workers : int, default: None 353 The maximum number of processes to be used for performing 354 the given series of simulations. If None, then as many worker 355 processes will be created as the machine has processors. 356 357 Examples 358 -------- 359 - Run a series of simulations by varying the initial population size of A. 360 >>> from abstochkin import AbStochKin 361 >>> 362 >>> sim = AbStochKin() 363 >>> sim.add_process_from_str("A -> B", 0.3, catalyst='E', Km=10) 364 >>> series_kwargs = [{"p0": {'A': i, 'B': 0, 'E': 10}, "t_max": 10} for i in range(40, 51)] 365 >>> sim.simulate_series_in_parallel(series_kwargs) 366 """ 367 extra_opts = {"show_plots": False, "_return_simulation": True} 368 with ProcessPoolExecutor(max_workers=max_workers) as executor: 369 futures = [executor.submit(self.simulate, **(kwargs | extra_opts)) for kwargs in 370 series_kwargs] 371 for future in futures: 372 self.sims.append(future.result())
Perform a series of simulations in parallel by initializing
separate processes. Each process runs a simulation and appends
a Simulation
object in the list self.sims
.
Parameters
- series_kwargs (list of dict): A list containing dictionaries of the keyword arguments for performing each simulation in the series. The number of elements in the list is the number of simulations that will be run.
- max_workers : int, default (None): The maximum number of processes to be used for performing the given series of simulations. If None, then as many worker processes will be created as the machine has processors.
Examples
- Run a series of simulations by varying the initial population size of A.
>>> from abstochkin import AbStochKin >>> >>> sim = AbStochKin() >>> sim.add_process_from_str("A -> B", 0.3, catalyst='E', Km=10) >>> series_kwargs = [{"p0": {'A': i, 'B': 0, 'E': 10}, "t_max": 10} for i in range(40, 51)] >>> sim.simulate_series_in_parallel(series_kwargs)