abstochkin.base API documentation

# abstochkin.base

Base class, AbStochKin, for initializing and storing all data for performing stochastic simulations using the Agent-based Kinetics method. A simulation project can be initialized and run as shown in the examples below.

###### Example

    >>> from abstochkin import AbStochKin
    >>> sim = AbStochKin()
    >>> sim.add_process_from_str('A -> ', 0.2)  # degradation process
    >>> sim.simulate(p0={'A': 100},t_max=20,plot_backend=plotly)
    >>> # All data for the above simulation is stored in `sim.sims[0]`.
    >>>
    >>> # Now set up a new simulation without actually running it.
    >>> sim.simulate(p0={'A': 10},t_max=10,n=50,run=False,plot_backend=plotly)
    >>> # All data for the new simulation is stored in `sim.sims[1]`.
    >>> # The simulation can then be manually run using methods
    >>> # documented in the class `Simulation`.

View Source

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

class AbStochKin: View Source

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

Base class for Agent-based Kinetics (AbStochKin) simulator.

###### Attributes

- volume : float, default (None, optional): The volume in liters of the compartment in which the processes are taking place.
- volume_unit : str, default ('L', optional): A string of the volume unit. The default value is 'L' for liters.
- time_unit : str, default ('sec', optional): A string of the time unit to be used for describing the kinetics of the given processes.
- processes (list): A list of the processes that the AbStochKin object has.
- het_processes (list): A list of the processes where population heterogeneity in one of the parameters is to be modeled. This list is a subset of the `processes` attribute.
- sims (list): A list of all simulations performed for the given set of processes. Each member of the list is an object of the `Simulation` class and contains all data for that simulation.

AbStochKin(volume: float = None, volume_unit: str = 'L', time_unit: str = 'sec') View Source

    73    def __init__(self,
    74                 volume: float = None,
    75                 volume_unit: str = 'L',
    76                 time_unit: str = 'sec'):
    77        self.volume = volume
    78        self.volume_unit = volume_unit
    79        self.time_unit = time_unit
    80
    81        self.processes = list()
    82        self.het_processes = list()
    83        self.sims = list()

volume
volume_unit
time_unit
processes
het_processes
sims
def add_processes_from_file(self, filename: str): View Source

    85    def add_processes_from_file(self, filename: str):
    86        """ Add a batch of processes from a text file. """
    87        with open(filename) as f:
    88            lines = f.readlines()
    89
    90        for line in lines:
    91            self.extract_process_from_str(line)

Add a batch of processes from a text file.

def extract_process_from_str(self, process_str: str): View Source

     93    def extract_process_from_str(self, process_str: str):
     94        """
     95        Extract a process and all of its specified parameters from a string.
     96
     97        This functions parses a string specifying all values and parameters
     98        needed to define a process. It then creates a Process object
     99        based on the extracted data.
    100        """
    101        process_str = process_str.replace(' ', '').replace('"', '\'')
    102
    103        proc_str = process_str.split(',')[0]
    104
    105        # Extract string parameters first
    106        patt_str_params = r"(\w+)=('[\w\s,]+')"
    107        str_params = re.findall(patt_str_params, process_str)
    108
    109        process_str_remain = re.sub(r"\w+='[\w\s,]+'", '', process_str)
    110
    111        patt_num_params = r"(\w+)=([\[\(,.\s\d\)\]]+)"
    112        num_params = re.findall(patt_num_params, process_str_remain)
    113
    114        all_params = list()
    115        for name, val_str in num_params + str_params:
    116            while val_str.endswith(','):
    117                val_str = val_str[:-1]
    118            all_params.append((name, literal_eval(val_str)))
    119
    120        self.add_process_from_str(proc_str, **dict(all_params))

Extract a process and all of its specified parameters from a string.

This functions parses a string specifying all values and parameters needed to define a process. It then creates a Process object based on the extracted data.

def add_process_from_str( self, process_str: str, /, k: float | int | list[float | int, ...] | tuple[float | int, float | int], **kwargs): View Source

    122    def add_process_from_str(self,
    123                             process_str: str,
    124                             /,
    125                             k: float | int | list[float | int, ...] | tuple[float | int, float | int],
    126                             **kwargs):
    127        """
    128        Add a process by specifying a string: 'reactants -> products'.
    129        Additional arguments determine if a specialized process
    130        (such as a reversible, regulated, or Michaelis-Menten process)
    131        is to be defined.
    132        """
    133        kwargs.setdefault('volume', self.volume)
    134
    135        if '<->' in process_str or 'k_rev' in kwargs:  # reversible process
    136            self.processes.append(ReversibleProcess.from_string(process_str, k, **kwargs))
    137        elif 'catalyst' in kwargs and 'Km' in kwargs and 'regulating_species' not in kwargs:
    138            self.processes.append(MichaelisMentenProcess.from_string(process_str, k, **kwargs))
    139        elif 'regulating_species' in kwargs and 'alpha' in kwargs and 'K50' in kwargs and 'nH' in kwargs:
    140            if 'catalyst' in kwargs and 'Km' in kwargs:  # Regulated MM process
    141                self.processes.append(RegulatedMichaelisMentenProcess.from_string(process_str,
    142                                                                                  k, **kwargs))
    143            else:  # Regulated process
    144                self.processes.append(RegulatedProcess.from_string(process_str, k, **kwargs))
    145        else:  # simple unidirectional process
    146            self.processes.append(Process.from_string(process_str, k, **kwargs))

Add a process by specifying a string: 'reactants -> products'. Additional arguments determine if a specialized process (such as a reversible, regulated, or Michaelis-Menten process) is to be defined.

def add_process( self, /, reactants: dict, products: dict, k: float | int | list[float | int, ...] | tuple[float | int, float | int], **kwargs): View Source

    148    def add_process(self,
    149                    /,
    150                    reactants: dict,
    151                    products: dict,
    152                    k: float | int | list[float | int, ...] | tuple[float | int, float | int],
    153                    **kwargs):
    154        """
    155        Add a process by using a dictionary for the reactants and products.
    156        Additional arguments determine if a specialized process
    157        (such as a reversible, regulated, or Michaelis-Menten process)
    158        is to be defined.
    159        """
    160        kwargs.setdefault('volume', self.volume)
    161
    162        if 'k_rev' in kwargs:  # reversible process
    163            self.processes.append(ReversibleProcess(reactants, products, k, **kwargs))
    164        elif 'catalyst' in kwargs and 'Km' in kwargs and 'regulating_species' not in kwargs:
    165            self.processes.append(MichaelisMentenProcess(reactants, products, k, **kwargs))
    166        elif 'regulating_species' in kwargs and 'alpha' in kwargs and 'K50' in kwargs and 'nH' in kwargs:
    167            if 'catalyst' in kwargs and 'Km' in kwargs:  # Regulated MM process
    168                self.processes.append(
    169                    RegulatedMichaelisMentenProcess(reactants, products, k, **kwargs))
    170            else:  # Regulated process
    171                self.processes.append(RegulatedProcess(reactants, products, k, **kwargs))
    172        else:  # simple unidirectional process
    173            self.processes.append(Process(reactants, products, k, **kwargs))

Add a process by using a dictionary for the reactants and products. Additional arguments determine if a specialized process (such as a reversible, regulated, or Michaelis-Menten process) is to be defined.

def del_process_from_str( self, process_str: str, /, k: float | int | list[float | int, ...] | tuple[float | int], **kwargs): View Source

    175    def del_process_from_str(self,
    176                             process_str: str,
    177                             /,
    178                             k: float | int | list[float | int, ...] | tuple[float | int],
    179                             **kwargs):
    180        """ Delete a process by specifying a string: 'reactants -> products'. """
    181        kwargs.setdefault('volume', self.volume)
    182
    183        try:
    184            if '<->' in process_str or 'k_rev' in kwargs:  # reversible process
    185                self.processes.remove(ReversibleProcess.from_string(process_str, k, **kwargs))
    186            elif 'catalyst' in kwargs and 'Km' in kwargs and 'regulating_species' not in kwargs:
    187                self.processes.remove(MichaelisMentenProcess.from_string(process_str, k, **kwargs))
    188            elif 'regulating_species' in kwargs and 'alpha' in kwargs and 'K50' in kwargs and 'nH' in kwargs:
    189                if 'catalyst' in kwargs and 'Km' in kwargs:  # Regulated MM process
    190                    self.processes.remove(
    191                        RegulatedMichaelisMentenProcess.from_string(process_str, k, **kwargs))
    192                else:  # Regulated process
    193                    self.processes.remove(RegulatedProcess.from_string(process_str, k, **kwargs))
    194            else:  # simple unidirectional process
    195                self.processes.remove(Process.from_string(process_str, k, **kwargs))
    196        except ValueError:
    197            print("Process to be removed was not found.")
    198        else:
    199            print(f"Removed: {process_str}, k = {k}, kwargs = {kwargs}")

Delete a process by specifying a string: 'reactants -> products'.

def del_process( self, /, reactants: dict, products: dict, k: float | int | list[float | int, ...] | tuple[float | int, float | int], **kwargs): View Source

    201    def del_process(self,
    202                    /,
    203                    reactants: dict,
    204                    products: dict,
    205                    k: float | int | list[float | int, ...] | tuple[float | int, float | int],
    206                    **kwargs):
    207        """ Delete a process by using a dictionary for the reactants and products. """
    208        kwargs.setdefault('volume', self.volume)
    209
    210        try:
    211            if 'k_rev' in kwargs:  # reversible process
    212                self.processes.remove(ReversibleProcess(reactants, products, k, **kwargs))
    213            elif 'catalyst' in kwargs and 'Km' in kwargs and 'regulating_species' not in kwargs:
    214                self.processes.remove(MichaelisMentenProcess(reactants, products, k, **kwargs))
    215            elif 'regulating_species' in kwargs and 'alpha' in kwargs and 'K50' in kwargs and 'nH' in kwargs:
    216                if 'catalyst' in kwargs and 'Km' in kwargs:  # Regulated MM process
    217                    self.processes.remove(
    218                        RegulatedMichaelisMentenProcess(reactants, products, k, **kwargs))
    219                else:  # Regulated process
    220                    self.processes.remove(RegulatedProcess(reactants, products, k, **kwargs))
    221            else:  # simple unidirectional process
    222                self.processes.remove(Process(reactants, products, k, **kwargs))
    223        except ValueError:
    224            print("Process to be removed was not found.")
    225        else:
    226            lhs, rhs = Process(reactants, products, k)._reconstruct_string()
    227            print(f"Removed: " + " -> ".join([lhs, rhs]) + f", k = {k}, kwargs = {kwargs}")

Delete a process by using a dictionary for the reactants and products.

def simulate( self, /, p0: dict, t_max: float | int, dt: float = 0.01, n: int = 100, *, random_seed: int = 19, solve_odes: bool = True, ode_method: str = 'RK45', run: bool = True, show_plots: bool = True, plot_backend: Literal['matplotlib', 'plotly'] = 'matplotlib', multithreading: bool = True, max_agents_by_species: dict = None, max_agents_multiplier: int = 2, _return_simulation: bool = False): View Source

    229    def simulate(self,
    230                 /,
    231                 p0: dict,
    232                 t_max: float | int,
    233                 dt: float = 0.01,
    234                 n: int = 100,
    235                 *,
    236                 random_seed: int = 19,
    237                 solve_odes: bool = True,
    238                 ode_method: str = 'RK45',
    239                 run: bool = True,
    240                 show_plots: bool = True,
    241                 plot_backend: Literal['matplotlib', 'plotly'] = 'matplotlib',
    242                 multithreading: bool = True,
    243                 max_agents_by_species: dict = None,
    244                 max_agents_multiplier: int = 2,
    245                 _return_simulation: bool = False):
    246        """
    247        Start an AbStochKin simulation by creating an instance of the class
    248        `Simulation`. The resulting object is appended to the list
    249        in the class attribute `AbStochKin.sims`.
    250
    251        Parameters
    252        ----------
    253        p0 : dict[str: int]
    254            Dictionary specifying the initial population sizes of all
    255            species in the given processes.
    256        t_max : float or int
    257            Numerical value of the end of simulated time in the units
    258            specified in the class attribute `AbStochKin.time_unit`.
    259        dt : float, default: 0.1, optional
    260            The duration of the time interval that the simulation's
    261            algorithm considers. The current implementation only
    262            supports a fixed time step interval whose value is `dt`.
    263        n : int, default: 100, optional
    264            The number of repetitions of the simulation to be performed.
    265        random_seed : int, default: 19, optional
    266            A number used to seed the random number generator.
    267        solve_odes : bool, default: True, optional
    268            Specify whether to numerically solve the system of
    269            ODEs defined from the given set of processes.
    270        ode_method : str, default: RK45, optional
    271            Available ODE methods: RK45, RK23, DOP853, Radau, BDF, LSODA.
    272        run : bool, default: True, optional
    273            Specify whether to run an AbStochKin simulation.
    274        show_plots : bool, default: True, optional
    275            Specify whether to graph the results of the AbStochKin simulation.
    276        plot_backend : str, default: 'matplotlib', optional
    277            `Matplotlib` and `Plotly` are currently supported.
    278        multithreading : bool, default: True, optional
    279            Specify whether to parallelize the simulation
    280            using multithreading. If `False`, the ensemble
    281            of simulations is run sequentially.
    282        max_agents_by_species : None or dict, default: dict
    283            Specification of the maximum number of agents that each
    284            species should have when running the simulation.
    285            If `None`, that a default approach will
    286            be taken by the class `Simulation` and the number
    287            for each species will be automatically determined
    288            (see method `Simulation._setup_runtime_data()` for details).
    289            The entries in the dictionary should be
    290            `species name (string): number (int)`.
    291        max_agents_multiplier : float or int, default: 2
    292            This parameter is used to calculate the maximum number of
    293            agents of each species that the simulation engine allocates
    294            memory for. This be determined by multiplying the maximum value
    295            of the ODE time trajectory for this species by the
    296            multiplier value specified here.
    297        _return_simulation : bool
    298            Determines if the `self.simulate` method returns a `Simulation`
    299            object or appends it to the list `self.sims`.
    300            Returning a `Simulation` object is needed when calling the method
    301            `simulate_series_in_parallel`.
    302        """
    303
    304        if max_agents_by_species is None:
    305            max_agents_by_species = dict()
    306
    307        self.het_processes = get_het_processes(self.processes)
    308
    309        sim = Simulation(p0,
    310                         t_max,
    311                         dt,
    312                         n,
    313                         self.processes,
    314                         random_state=random_seed,
    315                         do_solve_ODEs=solve_odes,
    316                         ODE_method=ode_method,
    317                         do_run=run,
    318                         show_graphs=show_plots,
    319                         graph_backend=plot_backend,
    320                         use_multithreading=multithreading,
    321                         max_agents=max_agents_by_species,
    322                         max_agents_multiplier=max_agents_multiplier,
    323                         time_unit=self.time_unit)
    324
    325        if _return_simulation:
    326            assert run, "Must run individual simulations if a series of " \
    327                        "simulations is to be run with multiprocessing."
    328
    329            # Set un-pickleable objects to None for data serialization to work
    330            sim.algo_sequence = None
    331            sim.progress_bar = None
    332
    333            return sim
    334        else:
    335            self.sims.append(sim)

Start an AbStochKin simulation by creating an instance of the class `Simulation`. The resulting object is appended to the list in the class attribute `AbStochKin.sims`.

###### Parameters

- p0 : dict[str (int]): Dictionary specifying the initial population sizes of all species in the given processes.
- t_max (float or int): Numerical value of the end of simulated time in the units specified in the class attribute `AbStochKin.time_unit`.
- dt : float, default (0.1, optional): The duration of the time interval that the simulation's algorithm considers. The current implementation only supports a fixed time step interval whose value is `dt`.
- n : int, default (100, optional): The number of repetitions of the simulation to be performed.
- random_seed : int, default (19, optional): A number used to seed the random number generator.
- solve_odes : bool, default (True, optional): Specify whether to numerically solve the system of ODEs defined from the given set of processes.
- ode_method : str, default (RK45, optional): Available ODE methods: RK45, RK23, DOP853, Radau, BDF, LSODA.
- run : bool, default (True, optional): Specify whether to run an AbStochKin simulation.
- show_plots : bool, default (True, optional): Specify whether to graph the results of the AbStochKin simulation.
- plot_backend : str, default ('matplotlib', optional): `Matplotlib` and `Plotly` are currently supported.
- multithreading : bool, default (True, optional): Specify whether to parallelize the simulation using multithreading. If `False`, the ensemble of simulations is run sequentially.
- max_agents_by_species : None or dict, default (dict): Specification of the maximum number of agents that each species should have when running the simulation. If `None`, that a default approach will be taken by the class `Simulation` and the number for each species will be automatically determined (see method `Simulation._setup_runtime_data()` for details). The entries in the dictionary should be `species name (string): number (int)`.
- max_agents_multiplier : float or int, default (2): This parameter is used to calculate the maximum number of agents of each species that the simulation engine allocates memory for. This be determined by multiplying the maximum value of the ODE time trajectory for this species by the multiplier value specified here.
- _return_simulation (bool): Determines if the `self.simulate` method returns a `Simulation` object or appends it to the list `self.sims`. Returning a `Simulation` object is needed when calling the method `simulate_series_in_parallel`.

def simulate_series_in_parallel( self, series_kwargs: list[dict[str, typing.Any], ...], *, max_workers: int = None): View Source

    337    def simulate_series_in_parallel(self,
    338                                    series_kwargs: list[dict[str, Any], ...],
    339                                    *,
    340                                    max_workers: int = None):
    341        """
    342        Perform a series of simulations in parallel by initializing
    343        separate processes. Each process runs a simulation and appends
    344        a `Simulation` object in the list `self.sims`.
    345
    346        Parameters
    347        ----------
    348        series_kwargs : list of dict
    349            A list containing dictionaries of the keyword arguments for
    350            performing each simulation in the series. The number of elements
    351            in the list is the number of simulations that will be run.
    352        max_workers : int, default: None
    353            The maximum number of processes to be used for performing
    354            the given series of simulations. If None, then as many worker
    355            processes will be created as the machine has processors.
    356
    357        Examples
    358        --------
    359        - Run a series of simulations by varying the initial population size of A.
    360        >>>  from abstochkin import AbStochKin
    361        >>>
    362        >>>  sim = AbStochKin()
    363        >>>  sim.add_process_from_str("A -> B", 0.3, catalyst='E', Km=10)
    364        >>>  series_kwargs = [{"p0": {'A': i, 'B': 0, 'E': 10}, "t_max": 10} for i in range(40, 51)]
    365        >>>  sim.simulate_series_in_parallel(series_kwargs)
    366        """
    367        extra_opts = {"show_plots": False, "_return_simulation": True}
    368        with ProcessPoolExecutor(max_workers=max_workers) as executor:
    369            futures = [executor.submit(self.simulate, **(kwargs | extra_opts)) for kwargs in
    370                       series_kwargs]
    371            for future in futures:
    372                self.sims.append(future.result())

Perform a series of simulations in parallel by initializing separate processes. Each process runs a simulation and appends a `Simulation` object in the list `self.sims`.

###### Parameters

- series_kwargs (list of dict): A list containing dictionaries of the keyword arguments for performing each simulation in the series. The number of elements in the list is the number of simulations that will be run.
- max_workers : int, default (None): The maximum number of processes to be used for performing the given series of simulations. If None, then as many worker processes will be created as the machine has processors.

###### Examples

- Run a series of simulations by varying the initial population size of A.

      >>>  from abstochkin import AbStochKin
      >>>
      >>>  sim = AbStochKin()
      >>>  sim.add_process_from_str("A -> B", 0.3, catalyst='E', Km=10)
      >>>  series_kwargs = [{"p0": {'A': i, 'B': 0, 'E': 10}, "t_max": 10} for i in range(40, 51)]
      >>>  sim.simulate_series_in_parallel(series_kwargs)