abstochkin.simulation API documentation

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

View Source

      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

class Simulation(abstochkin._simulation_methods.SimulationMethodsMixin): View Source

     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 be `species 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.

Simulation( p0: dict, t_max: float, dt: float, n: int, processes: list, *, random_state: int, do_solve_ODEs: bool, ODE_method: str, do_run: bool, show_graphs: bool, graph_backend: Literal['matplotlib', 'plotly'], use_multithreading: bool, max_agents: dict, max_agents_multiplier: float | int, time_unit: str) View Source

     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. If `False`, 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` is `True`). Available ODE methods: RK45, RK23, DOP853, Radau, BDF, LSODA.
- do_run (bool): Specify whether to run the AbStochKin simulation. If `False`, then a `Simulation` object is created but the simulation is not run. A user can then manually run it by calling the class method `run_simulation()`.
- show_graphs (bool): Specify whether to show graphs of the results.
- graph_backend (str): `Matplotlib` and `Plotly` are currently supported.

p0
t_min
t_max
dt
n
random_state
use_multithreading
max_agents
max_agents_multiplier
time_unit
streams
de_calcs
graph_backend
total_time
t_steps
time
trans_p
k_vals
Km_vals
K50_vals
k_het_metrics
Km_het_metrics
K50_het_metrics
algo_sequence
progress_bar
graphs
def run_simulation(self): View Source

    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.

def graph_results(self, /, graphs_to_show=None, species_to_show=None): View Source

    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`): If `None`, data for all species are plotted.

- graphs_to_show : `None` or string or list of string(s), default (`None`): If `None`, 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.