abstochkin.simulation
Perform an Agent-based Kinetics simulation.
This module contains the code for the class Simulation
,
which, along with the SimulationMethodsMixin
class,
does everything that is needed to run an
Agent-based Kinetics simulation and store its results.
The class AgentStateData
is used by a Simulation
object to store and handle some of the necessary runtime data.
1""" 2Perform an Agent-based Kinetics simulation. 3 4This module contains the code for the class `Simulation`, 5which, along with the `SimulationMethodsMixin` class, 6does everything that is needed to run an 7Agent-based Kinetics simulation and store its results. 8 9The class `AgentStateData` is used by a `Simulation` 10object to store and handle some of the necessary runtime data. 11""" 12 13# Copyright (c) 2024-2025, Alex Plakantonakis. 14# 15# This program is free software: you can redistribute it and/or modify 16# it under the terms of the GNU General Public License as published by 17# the Free Software Foundation, either version 3 of the License, or 18# (at your option) any later version. 19# 20# This program is distributed in the hope that it will be useful, 21# but WITHOUT ANY WARRANTY; without even the implied warranty of 22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23# GNU General Public License for more details. 24# 25# You should have received a copy of the GNU General Public License 26# along with this program. If not, see <http://www.gnu.org/licenses/>. 27 28import contextlib 29from typing import Literal 30 31import numpy as np 32from tqdm import tqdm 33 34from ._simulation_methods import SimulationMethodsMixin 35from .de_calcs import DEcalcs 36from .graphing import Graph 37from .het_calcs import get_het_processes 38from .process import update_all_species, MichaelisMentenProcess, \ 39 RegulatedProcess, RegulatedMichaelisMentenProcess 40from .utils import rng_streams 41 42 43class Simulation(SimulationMethodsMixin): 44 """ 45 Run an Agent-based Kinetics simulation. 46 47 Attributes 48 ---------- 49 p0 : dict[str: int] 50 Dictionary specifying the initial population sizes of all 51 species in the given processes. 52 t_max : float or int 53 Numerical value of the end of simulated time in the units 54 specified in the class attribute `AbStochKin.time_unit`. 55 dt : float 56 The duration of the time interval that the simulation's 57 algorithm considers. The current implementation only 58 supports a fixed time step interval whose value is `dt`. 59 n : int 60 The number of repetitions of the simulation to be 61 performed. 62 random_state : float or int 63 A number used to seed the random number generator. 64 use_multithreading : bool 65 Specify whether to parallelize the simulation 66 using multithreading. If `False`, the ensemble 67 of simulations is run sequentially. 68 max_agents : dict 69 Specification of the maximum number of agents that each 70 species should have when running the simulation. An 71 empty dictionary signifies that a default approach will 72 be taken and the number for each species will be 73 automatically determined (see method `_setup_runtime_data()` 74 for details). The entries in the dictionary should be 75 `species name (string): number (int)`. 76 max_agents_multiplier : float or int 77 Set the number of possible agents for each species to be the 78 maximum value of the ODE solution for the species 79 times `max_agents_multiplier`. 80 time_unit : str 81 A string of the time unit to be used for describing the 82 kinetics of the given processes. 83 """ 84 85 def __init__(self, 86 /, 87 p0: dict, 88 t_max: float, 89 dt: float, 90 n: int, 91 processes: list, 92 *, 93 random_state: int, 94 do_solve_ODEs: bool, 95 ODE_method: str, 96 do_run: bool, 97 show_graphs: bool, 98 graph_backend: Literal['matplotlib', 'plotly'], 99 use_multithreading: bool, 100 max_agents: dict, 101 max_agents_multiplier: float | int, 102 time_unit: str): 103 """ 104 The parameters below are not class attributes, but are part of a 105 `Simulation` object's initialization to trigger specific actions 106 to be automatically performed. Note that these actions can also 107 be performed manually by calling the appropriate methods once a 108 class object has been instantiated. 109 110 Other Parameters 111 ---------------- 112 do_solve_ODEs : bool 113 If `True`, attempt to numerically solve the system 114 of ODEs defined from the given set of processes. 115 If `False`, do not attempt to solve the ODEs and 116 do not run the simulation. 117 ODE_method : str 118 Method to use when attempting to solve the system 119 of ODEs (if `do_solve_ODEs` is `True`). 120 Available ODE methods: RK45, RK23, DOP853, Radau, 121 BDF, LSODA. 122 do_run : bool 123 Specify whether to run the AbStochKin simulation. 124 If `False`, then a `Simulation` object is created but 125 the simulation is not run. A user can then manually 126 run it by calling the class method `run_simulation()`. 127 show_graphs : bool 128 Specify whether to show graphs of the results. 129 graph_backend : str 130 `Matplotlib` and `Plotly` are currently supported. 131 """ 132 133 self.p0 = p0 # dictionary of initial population sizes 134 self.t_min = 0 # start simulation at time 0 (by assumption) 135 self.t_max = t_max # end simulation at time t_max 136 self.dt = dt # fixed time interval 137 self.n = n # repeat the simulation this many times 138 self.random_state = random_state 139 self.use_multithreading = use_multithreading 140 self.max_agents = max_agents 141 self.max_agents_multiplier = max_agents_multiplier 142 self.time_unit = time_unit 143 144 self.all_species, self._procs_by_reactant, self._procs_by_product = \ 145 update_all_species(tuple(processes)) 146 147 """ Generate the list of processes to be used by the algorithm. 148 This is done because some processes (e.g., reversible processes) 149 need to be split up into the forward and reverse processes. """ 150 self._algo_processes = list() 151 self._gen_algo_processes(processes) 152 self._het_processes = get_het_processes(self._algo_processes) 153 self._het_processes_num = len(self._het_processes) 154 155 self._validate_p0() # validate initial population sizes 156 157 # Generate streams of random numbers given a seed 158 self.streams = rng_streams(self.n, self.random_state) 159 160 # ******************** Deterministic calculations ******************** 161 self.de_calcs = DEcalcs(self.p0, self.t_min, self.t_max, processes, 162 ode_method=ODE_method, time_unit=self.time_unit) 163 if do_solve_ODEs: 164 try: 165 self.de_calcs.solve_ODEs() 166 except Exception as exc: 167 print(f"ODE solver exception:\n{exc}") 168 else: 169 if do_run: 170 print("Warning: Must specify the maximum number of agents for " 171 "each species when not solving the system ODEs.") 172 # ******************************************************************** 173 174 self.graph_backend = graph_backend 175 176 self.total_time = self.t_max - self.t_min 177 self.t_steps = int(self.total_time / self.dt) 178 self.time = np.linspace(0, self.total_time, self.t_steps + 1) 179 180 # Initialize data structures for results, runtime data (rtd), 181 # process-specific k values, transition probabilities 182 # for 0th and 1st order processes, metrics of population 183 # heterogeneity, sequence of functions th algorithm will 184 # execute, and progress bar. 185 self.results, self.rtd = dict(), dict() 186 187 self.trans_p = dict() # Process-specific transition probabilities 188 189 # Process-specific parameters that can exhibit heterogeneity 190 self.k_vals = dict() 191 self.Km_vals = dict() 192 self.K50_vals = dict() 193 194 self.k_het_metrics = dict() 195 self.Km_het_metrics = dict() 196 self.K50_het_metrics = dict() 197 198 # Runtime-specific attributes 199 self.algo_sequence = list() 200 self.progress_bar = None 201 202 self.graphs = None # For storing any graphs that are shown 203 204 if do_run: 205 self.run_simulation() 206 if show_graphs: # Simulation must first be run before plotting 207 self.graphs = self.graph_results() 208 209 def run_simulation(self): 210 """ 211 Run an ensemble of simulations and compute statistics 212 of simulation data. 213 """ 214 bar_fmt = f"{{desc}}: {{percentage:3.0f}}% |{{bar}}| " \ 215 f"n={{n_fmt}}/{{total_fmt}} " \ 216 f"[{{elapsed}}{'' if self.use_multithreading else '<{remaining}'}, " \ 217 f"{{rate_fmt}}{{postfix}}]" 218 self.progress_bar = tqdm(total=self.n, 219 ncols=65 if self.use_multithreading else 71, 220 desc="Progress", 221 bar_format=bar_fmt, 222 colour='green') 223 # Note on progress bar info: Multi-threading makes calculating 224 # the remaining time of the simulation unreliable, so only 225 # the elapsed time is shown. 226 227 self._setup_data() # initialize data 228 self._gen_algo_sequence() # generate sequence of processes for algorithm 229 230 self._parallel_run() if self.use_multithreading else self._sequential_run() 231 232 self._compute_trajectory_stats() # Get statistics on simulation data 233 234 # self._compute_k_het_stats() # Get statistics on heterogeneity data (k) 235 # self._compute_Km_het_stats() # Get statistics on heterogeneity data (Km) 236 # self._compute_K50_het_stats() # Get statistics on heterogeneity data (Km) 237 self._compute_het_stats() # Get statistics on heterogeneity data 238 239 self._post_run_cleanup() # free up some memory 240 self.progress_bar.close() 241 242 def graph_results(self, 243 /, 244 graphs_to_show=None, 245 species_to_show=None): 246 """ 247 Make graphs of the results. 248 249 Parameters 250 ---------- 251 species_to_show : `None` or list of string(s), default: `None` 252 If `None`, data for all species are plotted. 253 254 graphs_to_show : `None` or string or list of string(s), default: `None` 255 If `None`, all graphs are shown. If a string is given then the 256 graph that matches the string is shown. A list of strings shows 257 all the graphs specified in the list. 258 Graph specifications: 259 260 'avg' : Plot the average trajectories and their 261 one-standard deviation envelopes. The ODE 262 trajectories are also shown. 263 'traj' : Plot the species trajectories of individual 264 simulations. 265 'ode' : Plot the deterministic species trajectories, 266 obtained by numerically solving the ODEs. 267 'eta' : Plot the coefficient of variation (CoV) and 268 the CoV assuming that all processes a species 269 participates in obey Poisson statistics. 270 'het' : Plot species- and process-specific metrics 271 of heterogeneity (`k` and `ψ`) and their 272 one-standard deviation envelopes. 273 """ 274 graphs_to_return = list() 275 276 if graphs_to_show is None: 277 graphs_to_show = ['avg', 'het'] 278 if species_to_show is None: 279 species_to_show = self.all_species 280 281 if 'avg' in graphs_to_show: 282 graph_avg = Graph(backend=self.graph_backend) 283 with contextlib.suppress(AttributeError): 284 graph_avg.plot_ODEs(self.de_calcs, species=species_to_show, show_plot=False) 285 graph_avg.plot_avg_std(self.time, self.results, species=species_to_show) 286 graphs_to_return.append(graph_avg) 287 288 if 'traj' in graphs_to_show: 289 graph_traj = Graph(backend=self.graph_backend) 290 graph_traj.plot_trajectories(self.time, self.results, species=species_to_show) 291 graphs_to_return.append(graph_traj) 292 293 if 'ode' in graphs_to_show: 294 graph_ode = Graph(backend=self.graph_backend) 295 graph_ode.plot_ODEs(self.de_calcs, species=species_to_show) 296 graphs_to_return.append(graph_ode) 297 298 if 'eta' in graphs_to_show: 299 graph_eta = Graph(backend=self.graph_backend) 300 graph_eta.plot_eta(self.time, self.results, species=species_to_show) 301 graphs_to_return.append(graph_eta) 302 303 if 'het' in graphs_to_show: 304 graph_het = None 305 306 for proc in self._algo_processes: 307 if proc.is_heterogeneous: 308 graph_het = Graph(backend=self.graph_backend) 309 graph_het.plot_het_metrics(self.time, 310 (str(proc), ''), 311 self.k_het_metrics[proc]) 312 313 if isinstance(proc, (MichaelisMentenProcess, RegulatedMichaelisMentenProcess)): 314 if proc.is_heterogeneous_Km: 315 graph_het = Graph(backend=self.graph_backend) 316 graph_het.plot_het_metrics(self.time, 317 (str(proc), f"K_m={proc.Km}"), 318 self.Km_het_metrics[proc], 319 het_attr='Km') 320 321 if isinstance(proc, (RegulatedProcess, RegulatedMichaelisMentenProcess)): 322 if isinstance(proc.is_heterogeneous_K50, list): # multiple regulators 323 for i in range(len(proc.regulating_species)): 324 if proc.is_heterogeneous_K50[i]: 325 graph_het = Graph(backend=self.graph_backend) 326 extra_str = f"\\textrm{{{proc.regulation_type[i]} by }}" \ 327 f"{proc.regulating_species[i]}, " \ 328 f"K_{{50}}={proc.K50[i]}" 329 graph_het.plot_het_metrics(self.time, 330 (str(proc), extra_str), 331 self.K50_het_metrics[proc][i], 332 het_attr='K50') 333 else: # one regulator 334 if proc.is_heterogeneous_K50: 335 graph_het = Graph(backend=self.graph_backend) 336 graph_het.plot_het_metrics(self.time, 337 (str(proc), f"K_{{50}}={proc.K50}"), 338 self.K50_het_metrics[proc], 339 het_attr='K50') 340 341 if graph_het is not None: 342 graphs_to_return.append(graph_het) 343 344 return graphs_to_return 345 346 def __repr__(self): 347 return f"AbStochKin Simulation(p0={self.p0},\n" \ 348 f" t_min={self.t_min},\n" \ 349 f" t_max={self.t_max},\n" \ 350 f" dt={self.dt},\n" \ 351 f" n={self.n},\n" \ 352 f" random_state={self.random_state})" 353 354 def __str__(self): 355 descr = f"AbStochKin Simulation object with\n" \ 356 f"Processes: {self._algo_processes}\n" \ 357 f"Number of heterogeneous processes: {len(self._het_processes)}\n" \ 358 f"Initial population sizes: {self.p0}\n" \ 359 f"Simulated time interval: {self.t_min} - {self.t_max} {self.time_unit} " \ 360 f"with fixed time step increment {self.dt} {self.time_unit}\n" \ 361 f"Simulation runs: {self.n}\n" \ 362 f"Use multi-threading: {self.use_multithreading}\n" \ 363 f"Random state seed: {self.random_state}\n" 364 365 if self.de_calcs.odes_sol is not None: 366 descr += f"Attempt to solve ODEs with method " \ 367 f"{self.de_calcs.ode_method}: " \ 368 f"{'Successful' if self.de_calcs.odes_sol.success else 'Failed'}\n" 369 370 return descr
44class Simulation(SimulationMethodsMixin): 45 """ 46 Run an Agent-based Kinetics simulation. 47 48 Attributes 49 ---------- 50 p0 : dict[str: int] 51 Dictionary specifying the initial population sizes of all 52 species in the given processes. 53 t_max : float or int 54 Numerical value of the end of simulated time in the units 55 specified in the class attribute `AbStochKin.time_unit`. 56 dt : float 57 The duration of the time interval that the simulation's 58 algorithm considers. The current implementation only 59 supports a fixed time step interval whose value is `dt`. 60 n : int 61 The number of repetitions of the simulation to be 62 performed. 63 random_state : float or int 64 A number used to seed the random number generator. 65 use_multithreading : bool 66 Specify whether to parallelize the simulation 67 using multithreading. If `False`, the ensemble 68 of simulations is run sequentially. 69 max_agents : dict 70 Specification of the maximum number of agents that each 71 species should have when running the simulation. An 72 empty dictionary signifies that a default approach will 73 be taken and the number for each species will be 74 automatically determined (see method `_setup_runtime_data()` 75 for details). The entries in the dictionary should be 76 `species name (string): number (int)`. 77 max_agents_multiplier : float or int 78 Set the number of possible agents for each species to be the 79 maximum value of the ODE solution for the species 80 times `max_agents_multiplier`. 81 time_unit : str 82 A string of the time unit to be used for describing the 83 kinetics of the given processes. 84 """ 85 86 def __init__(self, 87 /, 88 p0: dict, 89 t_max: float, 90 dt: float, 91 n: int, 92 processes: list, 93 *, 94 random_state: int, 95 do_solve_ODEs: bool, 96 ODE_method: str, 97 do_run: bool, 98 show_graphs: bool, 99 graph_backend: Literal['matplotlib', 'plotly'], 100 use_multithreading: bool, 101 max_agents: dict, 102 max_agents_multiplier: float | int, 103 time_unit: str): 104 """ 105 The parameters below are not class attributes, but are part of a 106 `Simulation` object's initialization to trigger specific actions 107 to be automatically performed. Note that these actions can also 108 be performed manually by calling the appropriate methods once a 109 class object has been instantiated. 110 111 Other Parameters 112 ---------------- 113 do_solve_ODEs : bool 114 If `True`, attempt to numerically solve the system 115 of ODEs defined from the given set of processes. 116 If `False`, do not attempt to solve the ODEs and 117 do not run the simulation. 118 ODE_method : str 119 Method to use when attempting to solve the system 120 of ODEs (if `do_solve_ODEs` is `True`). 121 Available ODE methods: RK45, RK23, DOP853, Radau, 122 BDF, LSODA. 123 do_run : bool 124 Specify whether to run the AbStochKin simulation. 125 If `False`, then a `Simulation` object is created but 126 the simulation is not run. A user can then manually 127 run it by calling the class method `run_simulation()`. 128 show_graphs : bool 129 Specify whether to show graphs of the results. 130 graph_backend : str 131 `Matplotlib` and `Plotly` are currently supported. 132 """ 133 134 self.p0 = p0 # dictionary of initial population sizes 135 self.t_min = 0 # start simulation at time 0 (by assumption) 136 self.t_max = t_max # end simulation at time t_max 137 self.dt = dt # fixed time interval 138 self.n = n # repeat the simulation this many times 139 self.random_state = random_state 140 self.use_multithreading = use_multithreading 141 self.max_agents = max_agents 142 self.max_agents_multiplier = max_agents_multiplier 143 self.time_unit = time_unit 144 145 self.all_species, self._procs_by_reactant, self._procs_by_product = \ 146 update_all_species(tuple(processes)) 147 148 """ Generate the list of processes to be used by the algorithm. 149 This is done because some processes (e.g., reversible processes) 150 need to be split up into the forward and reverse processes. """ 151 self._algo_processes = list() 152 self._gen_algo_processes(processes) 153 self._het_processes = get_het_processes(self._algo_processes) 154 self._het_processes_num = len(self._het_processes) 155 156 self._validate_p0() # validate initial population sizes 157 158 # Generate streams of random numbers given a seed 159 self.streams = rng_streams(self.n, self.random_state) 160 161 # ******************** Deterministic calculations ******************** 162 self.de_calcs = DEcalcs(self.p0, self.t_min, self.t_max, processes, 163 ode_method=ODE_method, time_unit=self.time_unit) 164 if do_solve_ODEs: 165 try: 166 self.de_calcs.solve_ODEs() 167 except Exception as exc: 168 print(f"ODE solver exception:\n{exc}") 169 else: 170 if do_run: 171 print("Warning: Must specify the maximum number of agents for " 172 "each species when not solving the system ODEs.") 173 # ******************************************************************** 174 175 self.graph_backend = graph_backend 176 177 self.total_time = self.t_max - self.t_min 178 self.t_steps = int(self.total_time / self.dt) 179 self.time = np.linspace(0, self.total_time, self.t_steps + 1) 180 181 # Initialize data structures for results, runtime data (rtd), 182 # process-specific k values, transition probabilities 183 # for 0th and 1st order processes, metrics of population 184 # heterogeneity, sequence of functions th algorithm will 185 # execute, and progress bar. 186 self.results, self.rtd = dict(), dict() 187 188 self.trans_p = dict() # Process-specific transition probabilities 189 190 # Process-specific parameters that can exhibit heterogeneity 191 self.k_vals = dict() 192 self.Km_vals = dict() 193 self.K50_vals = dict() 194 195 self.k_het_metrics = dict() 196 self.Km_het_metrics = dict() 197 self.K50_het_metrics = dict() 198 199 # Runtime-specific attributes 200 self.algo_sequence = list() 201 self.progress_bar = None 202 203 self.graphs = None # For storing any graphs that are shown 204 205 if do_run: 206 self.run_simulation() 207 if show_graphs: # Simulation must first be run before plotting 208 self.graphs = self.graph_results() 209 210 def run_simulation(self): 211 """ 212 Run an ensemble of simulations and compute statistics 213 of simulation data. 214 """ 215 bar_fmt = f"{{desc}}: {{percentage:3.0f}}% |{{bar}}| " \ 216 f"n={{n_fmt}}/{{total_fmt}} " \ 217 f"[{{elapsed}}{'' if self.use_multithreading else '<{remaining}'}, " \ 218 f"{{rate_fmt}}{{postfix}}]" 219 self.progress_bar = tqdm(total=self.n, 220 ncols=65 if self.use_multithreading else 71, 221 desc="Progress", 222 bar_format=bar_fmt, 223 colour='green') 224 # Note on progress bar info: Multi-threading makes calculating 225 # the remaining time of the simulation unreliable, so only 226 # the elapsed time is shown. 227 228 self._setup_data() # initialize data 229 self._gen_algo_sequence() # generate sequence of processes for algorithm 230 231 self._parallel_run() if self.use_multithreading else self._sequential_run() 232 233 self._compute_trajectory_stats() # Get statistics on simulation data 234 235 # self._compute_k_het_stats() # Get statistics on heterogeneity data (k) 236 # self._compute_Km_het_stats() # Get statistics on heterogeneity data (Km) 237 # self._compute_K50_het_stats() # Get statistics on heterogeneity data (Km) 238 self._compute_het_stats() # Get statistics on heterogeneity data 239 240 self._post_run_cleanup() # free up some memory 241 self.progress_bar.close() 242 243 def graph_results(self, 244 /, 245 graphs_to_show=None, 246 species_to_show=None): 247 """ 248 Make graphs of the results. 249 250 Parameters 251 ---------- 252 species_to_show : `None` or list of string(s), default: `None` 253 If `None`, data for all species are plotted. 254 255 graphs_to_show : `None` or string or list of string(s), default: `None` 256 If `None`, all graphs are shown. If a string is given then the 257 graph that matches the string is shown. A list of strings shows 258 all the graphs specified in the list. 259 Graph specifications: 260 261 'avg' : Plot the average trajectories and their 262 one-standard deviation envelopes. The ODE 263 trajectories are also shown. 264 'traj' : Plot the species trajectories of individual 265 simulations. 266 'ode' : Plot the deterministic species trajectories, 267 obtained by numerically solving the ODEs. 268 'eta' : Plot the coefficient of variation (CoV) and 269 the CoV assuming that all processes a species 270 participates in obey Poisson statistics. 271 'het' : Plot species- and process-specific metrics 272 of heterogeneity (`k` and `ψ`) and their 273 one-standard deviation envelopes. 274 """ 275 graphs_to_return = list() 276 277 if graphs_to_show is None: 278 graphs_to_show = ['avg', 'het'] 279 if species_to_show is None: 280 species_to_show = self.all_species 281 282 if 'avg' in graphs_to_show: 283 graph_avg = Graph(backend=self.graph_backend) 284 with contextlib.suppress(AttributeError): 285 graph_avg.plot_ODEs(self.de_calcs, species=species_to_show, show_plot=False) 286 graph_avg.plot_avg_std(self.time, self.results, species=species_to_show) 287 graphs_to_return.append(graph_avg) 288 289 if 'traj' in graphs_to_show: 290 graph_traj = Graph(backend=self.graph_backend) 291 graph_traj.plot_trajectories(self.time, self.results, species=species_to_show) 292 graphs_to_return.append(graph_traj) 293 294 if 'ode' in graphs_to_show: 295 graph_ode = Graph(backend=self.graph_backend) 296 graph_ode.plot_ODEs(self.de_calcs, species=species_to_show) 297 graphs_to_return.append(graph_ode) 298 299 if 'eta' in graphs_to_show: 300 graph_eta = Graph(backend=self.graph_backend) 301 graph_eta.plot_eta(self.time, self.results, species=species_to_show) 302 graphs_to_return.append(graph_eta) 303 304 if 'het' in graphs_to_show: 305 graph_het = None 306 307 for proc in self._algo_processes: 308 if proc.is_heterogeneous: 309 graph_het = Graph(backend=self.graph_backend) 310 graph_het.plot_het_metrics(self.time, 311 (str(proc), ''), 312 self.k_het_metrics[proc]) 313 314 if isinstance(proc, (MichaelisMentenProcess, RegulatedMichaelisMentenProcess)): 315 if proc.is_heterogeneous_Km: 316 graph_het = Graph(backend=self.graph_backend) 317 graph_het.plot_het_metrics(self.time, 318 (str(proc), f"K_m={proc.Km}"), 319 self.Km_het_metrics[proc], 320 het_attr='Km') 321 322 if isinstance(proc, (RegulatedProcess, RegulatedMichaelisMentenProcess)): 323 if isinstance(proc.is_heterogeneous_K50, list): # multiple regulators 324 for i in range(len(proc.regulating_species)): 325 if proc.is_heterogeneous_K50[i]: 326 graph_het = Graph(backend=self.graph_backend) 327 extra_str = f"\\textrm{{{proc.regulation_type[i]} by }}" \ 328 f"{proc.regulating_species[i]}, " \ 329 f"K_{{50}}={proc.K50[i]}" 330 graph_het.plot_het_metrics(self.time, 331 (str(proc), extra_str), 332 self.K50_het_metrics[proc][i], 333 het_attr='K50') 334 else: # one regulator 335 if proc.is_heterogeneous_K50: 336 graph_het = Graph(backend=self.graph_backend) 337 graph_het.plot_het_metrics(self.time, 338 (str(proc), f"K_{{50}}={proc.K50}"), 339 self.K50_het_metrics[proc], 340 het_attr='K50') 341 342 if graph_het is not None: 343 graphs_to_return.append(graph_het) 344 345 return graphs_to_return 346 347 def __repr__(self): 348 return f"AbStochKin Simulation(p0={self.p0},\n" \ 349 f" t_min={self.t_min},\n" \ 350 f" t_max={self.t_max},\n" \ 351 f" dt={self.dt},\n" \ 352 f" n={self.n},\n" \ 353 f" random_state={self.random_state})" 354 355 def __str__(self): 356 descr = f"AbStochKin Simulation object with\n" \ 357 f"Processes: {self._algo_processes}\n" \ 358 f"Number of heterogeneous processes: {len(self._het_processes)}\n" \ 359 f"Initial population sizes: {self.p0}\n" \ 360 f"Simulated time interval: {self.t_min} - {self.t_max} {self.time_unit} " \ 361 f"with fixed time step increment {self.dt} {self.time_unit}\n" \ 362 f"Simulation runs: {self.n}\n" \ 363 f"Use multi-threading: {self.use_multithreading}\n" \ 364 f"Random state seed: {self.random_state}\n" 365 366 if self.de_calcs.odes_sol is not None: 367 descr += f"Attempt to solve ODEs with method " \ 368 f"{self.de_calcs.ode_method}: " \ 369 f"{'Successful' if self.de_calcs.odes_sol.success else 'Failed'}\n" 370 371 return descr
Run an Agent-based Kinetics simulation.
Attributes
- 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):
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): The number of repetitions of the simulation to be performed.
- random_state (float or int): A number used to seed the random number generator.
- use_multithreading (bool):
Specify whether to parallelize the simulation
using multithreading. If
False
, the ensemble of simulations is run sequentially. - max_agents (dict):
Specification of the maximum number of agents that each
species should have when running the simulation. An
empty dictionary signifies that a default approach will
be taken and the number for each species will be
automatically determined (see method
_setup_runtime_data()
for details). The entries in the dictionary should bespecies name (string): number (int)
. - max_agents_multiplier (float or int):
Set the number of possible agents for each species to be the
maximum value of the ODE solution for the species
times
max_agents_multiplier
. - time_unit (str): A string of the time unit to be used for describing the kinetics of the given processes.
86 def __init__(self, 87 /, 88 p0: dict, 89 t_max: float, 90 dt: float, 91 n: int, 92 processes: list, 93 *, 94 random_state: int, 95 do_solve_ODEs: bool, 96 ODE_method: str, 97 do_run: bool, 98 show_graphs: bool, 99 graph_backend: Literal['matplotlib', 'plotly'], 100 use_multithreading: bool, 101 max_agents: dict, 102 max_agents_multiplier: float | int, 103 time_unit: str): 104 """ 105 The parameters below are not class attributes, but are part of a 106 `Simulation` object's initialization to trigger specific actions 107 to be automatically performed. Note that these actions can also 108 be performed manually by calling the appropriate methods once a 109 class object has been instantiated. 110 111 Other Parameters 112 ---------------- 113 do_solve_ODEs : bool 114 If `True`, attempt to numerically solve the system 115 of ODEs defined from the given set of processes. 116 If `False`, do not attempt to solve the ODEs and 117 do not run the simulation. 118 ODE_method : str 119 Method to use when attempting to solve the system 120 of ODEs (if `do_solve_ODEs` is `True`). 121 Available ODE methods: RK45, RK23, DOP853, Radau, 122 BDF, LSODA. 123 do_run : bool 124 Specify whether to run the AbStochKin simulation. 125 If `False`, then a `Simulation` object is created but 126 the simulation is not run. A user can then manually 127 run it by calling the class method `run_simulation()`. 128 show_graphs : bool 129 Specify whether to show graphs of the results. 130 graph_backend : str 131 `Matplotlib` and `Plotly` are currently supported. 132 """ 133 134 self.p0 = p0 # dictionary of initial population sizes 135 self.t_min = 0 # start simulation at time 0 (by assumption) 136 self.t_max = t_max # end simulation at time t_max 137 self.dt = dt # fixed time interval 138 self.n = n # repeat the simulation this many times 139 self.random_state = random_state 140 self.use_multithreading = use_multithreading 141 self.max_agents = max_agents 142 self.max_agents_multiplier = max_agents_multiplier 143 self.time_unit = time_unit 144 145 self.all_species, self._procs_by_reactant, self._procs_by_product = \ 146 update_all_species(tuple(processes)) 147 148 """ Generate the list of processes to be used by the algorithm. 149 This is done because some processes (e.g., reversible processes) 150 need to be split up into the forward and reverse processes. """ 151 self._algo_processes = list() 152 self._gen_algo_processes(processes) 153 self._het_processes = get_het_processes(self._algo_processes) 154 self._het_processes_num = len(self._het_processes) 155 156 self._validate_p0() # validate initial population sizes 157 158 # Generate streams of random numbers given a seed 159 self.streams = rng_streams(self.n, self.random_state) 160 161 # ******************** Deterministic calculations ******************** 162 self.de_calcs = DEcalcs(self.p0, self.t_min, self.t_max, processes, 163 ode_method=ODE_method, time_unit=self.time_unit) 164 if do_solve_ODEs: 165 try: 166 self.de_calcs.solve_ODEs() 167 except Exception as exc: 168 print(f"ODE solver exception:\n{exc}") 169 else: 170 if do_run: 171 print("Warning: Must specify the maximum number of agents for " 172 "each species when not solving the system ODEs.") 173 # ******************************************************************** 174 175 self.graph_backend = graph_backend 176 177 self.total_time = self.t_max - self.t_min 178 self.t_steps = int(self.total_time / self.dt) 179 self.time = np.linspace(0, self.total_time, self.t_steps + 1) 180 181 # Initialize data structures for results, runtime data (rtd), 182 # process-specific k values, transition probabilities 183 # for 0th and 1st order processes, metrics of population 184 # heterogeneity, sequence of functions th algorithm will 185 # execute, and progress bar. 186 self.results, self.rtd = dict(), dict() 187 188 self.trans_p = dict() # Process-specific transition probabilities 189 190 # Process-specific parameters that can exhibit heterogeneity 191 self.k_vals = dict() 192 self.Km_vals = dict() 193 self.K50_vals = dict() 194 195 self.k_het_metrics = dict() 196 self.Km_het_metrics = dict() 197 self.K50_het_metrics = dict() 198 199 # Runtime-specific attributes 200 self.algo_sequence = list() 201 self.progress_bar = None 202 203 self.graphs = None # For storing any graphs that are shown 204 205 if do_run: 206 self.run_simulation() 207 if show_graphs: # Simulation must first be run before plotting 208 self.graphs = self.graph_results()
The parameters below are not class attributes, but are part of a
Simulation
object's initialization to trigger specific actions
to be automatically performed. Note that these actions can also
be performed manually by calling the appropriate methods once a
class object has been instantiated.
Other Parameters
- do_solve_ODEs (bool):
If
True
, attempt to numerically solve the system of ODEs defined from the given set of processes. IfFalse
, do not attempt to solve the ODEs and do not run the simulation. - ODE_method (str):
Method to use when attempting to solve the system
of ODEs (if
do_solve_ODEs
isTrue
). Available ODE methods: RK45, RK23, DOP853, Radau, BDF, LSODA. - do_run (bool):
Specify whether to run the AbStochKin simulation.
If
False
, then aSimulation
object is created but the simulation is not run. A user can then manually run it by calling the class methodrun_simulation()
. - show_graphs (bool): Specify whether to show graphs of the results.
- graph_backend (str):
Matplotlib
andPlotly
are currently supported.
210 def run_simulation(self): 211 """ 212 Run an ensemble of simulations and compute statistics 213 of simulation data. 214 """ 215 bar_fmt = f"{{desc}}: {{percentage:3.0f}}% |{{bar}}| " \ 216 f"n={{n_fmt}}/{{total_fmt}} " \ 217 f"[{{elapsed}}{'' if self.use_multithreading else '<{remaining}'}, " \ 218 f"{{rate_fmt}}{{postfix}}]" 219 self.progress_bar = tqdm(total=self.n, 220 ncols=65 if self.use_multithreading else 71, 221 desc="Progress", 222 bar_format=bar_fmt, 223 colour='green') 224 # Note on progress bar info: Multi-threading makes calculating 225 # the remaining time of the simulation unreliable, so only 226 # the elapsed time is shown. 227 228 self._setup_data() # initialize data 229 self._gen_algo_sequence() # generate sequence of processes for algorithm 230 231 self._parallel_run() if self.use_multithreading else self._sequential_run() 232 233 self._compute_trajectory_stats() # Get statistics on simulation data 234 235 # self._compute_k_het_stats() # Get statistics on heterogeneity data (k) 236 # self._compute_Km_het_stats() # Get statistics on heterogeneity data (Km) 237 # self._compute_K50_het_stats() # Get statistics on heterogeneity data (Km) 238 self._compute_het_stats() # Get statistics on heterogeneity data 239 240 self._post_run_cleanup() # free up some memory 241 self.progress_bar.close()
Run an ensemble of simulations and compute statistics of simulation data.
243 def graph_results(self, 244 /, 245 graphs_to_show=None, 246 species_to_show=None): 247 """ 248 Make graphs of the results. 249 250 Parameters 251 ---------- 252 species_to_show : `None` or list of string(s), default: `None` 253 If `None`, data for all species are plotted. 254 255 graphs_to_show : `None` or string or list of string(s), default: `None` 256 If `None`, all graphs are shown. If a string is given then the 257 graph that matches the string is shown. A list of strings shows 258 all the graphs specified in the list. 259 Graph specifications: 260 261 'avg' : Plot the average trajectories and their 262 one-standard deviation envelopes. The ODE 263 trajectories are also shown. 264 'traj' : Plot the species trajectories of individual 265 simulations. 266 'ode' : Plot the deterministic species trajectories, 267 obtained by numerically solving the ODEs. 268 'eta' : Plot the coefficient of variation (CoV) and 269 the CoV assuming that all processes a species 270 participates in obey Poisson statistics. 271 'het' : Plot species- and process-specific metrics 272 of heterogeneity (`k` and `ψ`) and their 273 one-standard deviation envelopes. 274 """ 275 graphs_to_return = list() 276 277 if graphs_to_show is None: 278 graphs_to_show = ['avg', 'het'] 279 if species_to_show is None: 280 species_to_show = self.all_species 281 282 if 'avg' in graphs_to_show: 283 graph_avg = Graph(backend=self.graph_backend) 284 with contextlib.suppress(AttributeError): 285 graph_avg.plot_ODEs(self.de_calcs, species=species_to_show, show_plot=False) 286 graph_avg.plot_avg_std(self.time, self.results, species=species_to_show) 287 graphs_to_return.append(graph_avg) 288 289 if 'traj' in graphs_to_show: 290 graph_traj = Graph(backend=self.graph_backend) 291 graph_traj.plot_trajectories(self.time, self.results, species=species_to_show) 292 graphs_to_return.append(graph_traj) 293 294 if 'ode' in graphs_to_show: 295 graph_ode = Graph(backend=self.graph_backend) 296 graph_ode.plot_ODEs(self.de_calcs, species=species_to_show) 297 graphs_to_return.append(graph_ode) 298 299 if 'eta' in graphs_to_show: 300 graph_eta = Graph(backend=self.graph_backend) 301 graph_eta.plot_eta(self.time, self.results, species=species_to_show) 302 graphs_to_return.append(graph_eta) 303 304 if 'het' in graphs_to_show: 305 graph_het = None 306 307 for proc in self._algo_processes: 308 if proc.is_heterogeneous: 309 graph_het = Graph(backend=self.graph_backend) 310 graph_het.plot_het_metrics(self.time, 311 (str(proc), ''), 312 self.k_het_metrics[proc]) 313 314 if isinstance(proc, (MichaelisMentenProcess, RegulatedMichaelisMentenProcess)): 315 if proc.is_heterogeneous_Km: 316 graph_het = Graph(backend=self.graph_backend) 317 graph_het.plot_het_metrics(self.time, 318 (str(proc), f"K_m={proc.Km}"), 319 self.Km_het_metrics[proc], 320 het_attr='Km') 321 322 if isinstance(proc, (RegulatedProcess, RegulatedMichaelisMentenProcess)): 323 if isinstance(proc.is_heterogeneous_K50, list): # multiple regulators 324 for i in range(len(proc.regulating_species)): 325 if proc.is_heterogeneous_K50[i]: 326 graph_het = Graph(backend=self.graph_backend) 327 extra_str = f"\\textrm{{{proc.regulation_type[i]} by }}" \ 328 f"{proc.regulating_species[i]}, " \ 329 f"K_{{50}}={proc.K50[i]}" 330 graph_het.plot_het_metrics(self.time, 331 (str(proc), extra_str), 332 self.K50_het_metrics[proc][i], 333 het_attr='K50') 334 else: # one regulator 335 if proc.is_heterogeneous_K50: 336 graph_het = Graph(backend=self.graph_backend) 337 graph_het.plot_het_metrics(self.time, 338 (str(proc), f"K_{{50}}={proc.K50}"), 339 self.K50_het_metrics[proc], 340 het_attr='K50') 341 342 if graph_het is not None: 343 graphs_to_return.append(graph_het) 344 345 return graphs_to_return
Make graphs of the results.
Parameters
- species_to_show :
None
or list of string(s), default (None
): IfNone
, data for all species are plotted. graphs_to_show :
None
or string or list of string(s), default (None
): IfNone
, all graphs are shown. If a string is given then the graph that matches the string is shown. A list of strings shows all the graphs specified in the list. Graph specifications:'avg' : Plot the average trajectories and their one-standard deviation envelopes. The ODE trajectories are also shown. 'traj' : Plot the species trajectories of individual simulations. 'ode' : Plot the deterministic species trajectories, obtained by numerically solving the ODEs. 'eta' : Plot the coefficient of variation (CoV) and the CoV assuming that all processes a species participates in obey Poisson statistics. 'het' : Plot species- and process-specific metrics of heterogeneity (
k
andψ
) and their one-standard deviation envelopes.