abstochkin.utils

Some utility functions on generating random number streams, measuring a function's runtime, calculating a statistic measuring the goodness of fit when comparing time series data, and performing unit conversion of kinetic parameters.

  1"""
  2Some utility functions on generating random number streams, measuring a
  3function's runtime, calculating a statistic measuring the goodness of fit
  4when comparing time series data, and performing unit conversion of
  5kinetic parameters.
  6"""
  7
  8#  Copyright (c) 2024-2025, Alex Plakantonakis.
  9#
 10#  This program is free software: you can redistribute it and/or modify
 11#  it under the terms of the GNU General Public License as published by
 12#  the Free Software Foundation, either version 3 of the License, or
 13#  (at your option) any later version.
 14#
 15#  This program is distributed in the hope that it will be useful,
 16#  but WITHOUT ANY WARRANTY; without even the implied warranty of
 17#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 18#  GNU General Public License for more details.
 19#
 20#  You should have received a copy of the GNU General Public License
 21#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 22
 23import functools
 24from time import perf_counter
 25
 26import numpy as np
 27from numpy import random
 28from scipy.constants import N_A
 29
 30
 31def rng_streams(n: int, random_state: int):
 32    """
 33    Generate independent streams of random numbers spawned from the
 34    same initial seed.
 35
 36    Parameters
 37    ----------
 38    n : int
 39        number of generators/streams to generate.
 40    random_state : int, optional
 41        initial seed from which n new seeds are spawned.
 42
 43    Returns
 44    -------
 45    list[PCG64DXSM Generator objects]
 46        List of `n` generators of independent streams of random numbers
 47
 48    Notes
 49    -----
 50    See https://numpy.org/doc/stable/reference/random/parallel.html for more info.
 51
 52    On PCG64DXSM:
 53        - https://numpy.org/doc/stable/reference/random/upgrading-pcg64.html#upgrading-pcg64
 54        - https://numpy.org/doc/stable/reference/random/bit_generators/pcg64dxsm.html
 55
 56    Examples
 57    --------
 58    >>> rng = rng_streams(5)  # make 5 random number generators
 59    >>> a = rng[1].integers(1, 10, 100)
 60    """
 61    ss = random.SeedSequence(random_state)
 62    seeds = ss.spawn(n)
 63
 64    # return [random.default_rng(seed) for seed in seeds]  # PCG64 generator objects
 65
 66    # Return PCG64DXSM generator objects
 67    return [random.Generator(random.PCG64DXSM(seed)) for seed in seeds]
 68
 69
 70def measure_runtime(fcn):
 71    """ Decorator for measuring the duration of a function's execution. """
 72    @functools.wraps(fcn)
 73    def inner(*args, **kwargs):
 74        start = perf_counter()
 75        fcn(*args, **kwargs)
 76        stop = perf_counter()
 77        duration = stop - start
 78        if duration < 60:
 79            msg = f"Simulation Runtime: {duration:.3f} sec"
 80        elif 60 <= duration < 3600:
 81            msg = f"Simulation Runtime: {duration / 60:.3f} min"
 82        else:
 83            msg = f"Simulation Runtime: {duration / 3600:.3f} hr"
 84        print(msg)
 85
 86    return inner
 87
 88
 89def r_squared(actual: np.array, theoretical: np.array) -> float:
 90    """
 91    Compute the coefficient of determination, $R^2$.
 92
 93    In the case of comparing the average AbStochKin-simulated species
 94    trajectory to its deterministic trajectory. Since the latter is only
 95    meaningful for a homogeneous population, R² should be
 96    close to `1` for a simulated homogeneous process.
 97    For a heterogeneous process, it can be interpreted as how close
 98    the simulated trajectory is to the deterministic trajectory of a
 99    *homogeneous* process. In this case, $R^2$ would not be expected
100    to be close to $1$ and the importance of looking at this metric
101    is questionable.
102
103    Parameters
104    ----------
105    actual : numpy.array
106        Actual data obtained through a simulation.
107    theoretical : numpy.array
108        Theoretical data to compare the actual data to.
109
110    Returns
111    -------
112    float
113        The coefficient of determination, $R^2$.
114    """
115    # sst: total sum of squares for simulation avg trajectory
116    sst = np.nansum((actual - np.nanmean(actual)) ** 2)
117    # ssr: sum of square residuals (data vs deterministic prediction)
118    ssr = np.nansum((actual - theoretical) ** 2)
119
120    return 1 - ssr / sst if sst != 0 else np.nan
121
122
123def macro_to_micro(macro_val: float | int | list[float | int, ...] | tuple[float | int, float | int],
124                   volume: float | int,
125                   order: int = 0,
126                   *,
127                   inverse: bool = False) -> float | list[float, ...] | tuple[float, float]:
128    """
129    Convert a kinetic parameter value from macroscopic to microscopic form.
130
131    The ABK algorithm uses microscopic kinetic constants, thus necessitating
132    the conversion of any molar quantities to their microscopic counterpart.
133    For a kinetic parameter, the microscopic form is interpreted as the number
134    of transition events per second (or whatever the time unit may be).
135    For a molar quantity, its microscopic form is the number of particles in
136    the given volume.
137
138    Parameters
139    ----------
140    macro_val : float or int
141        The value of the parameter to be converted, expressed in terms of
142        molar quantities.
143    volume : float or int
144        The volume, in liters, in which the process that the given parameter
145        value is a descriptor of.
146    order : int, default: 0
147        The order of the process whose kinetic parameter is to be converted.
148        The default value of 0 is for parameters (such as Km or K50) whose
149        units are molarity.
150    inverse : bool, default: False
151        Perform the inverse of this operation. That is, convert
152        from microscopic to macroscopic form.
153
154    Returns
155    --------
156    float
157        A kinetic parameter is returned in units of reciprocal seconds.
158        A molar quantity is returned as the number of particles in the
159        given volume.
160
161    Notes
162    -----
163    - A kinetic parameter for a 1st order process will remain unchanged
164    because its units are already reciprocal seconds.
165
166    Reference
167    ---------
168    Plakantonakis, Alex. “Agent-based Kinetics: A Nonspatial Stochastic Method
169    for Simulating the Dynamics of Heterogeneous Populations.”
170    OSF Preprints, 26 July 2019. Web. Section 2.1.
171    """
172    assert volume > 0, "The volume has to be a positive quantity."
173    assert order >= 0, "The process order cannot be negative."
174
175    denom = (N_A * volume) ** (order - 1) if not inverse else 1 / (N_A * volume) ** (order - 1)
176
177    if isinstance(macro_val, (list, tuple)):
178        assert all([True if val >= 0 else False for val in macro_val]), \
179            "The parameter values cannot be negative."
180        micro_vals = [val / denom for val in macro_val]
181        return micro_vals if isinstance(macro_val, list) else tuple(micro_vals)
182    else:
183        assert macro_val >= 0, "The parameter value cannot be negative."
184        return macro_val / denom
def rng_streams(n: int, random_state: int):
32def rng_streams(n: int, random_state: int):
33    """
34    Generate independent streams of random numbers spawned from the
35    same initial seed.
36
37    Parameters
38    ----------
39    n : int
40        number of generators/streams to generate.
41    random_state : int, optional
42        initial seed from which n new seeds are spawned.
43
44    Returns
45    -------
46    list[PCG64DXSM Generator objects]
47        List of `n` generators of independent streams of random numbers
48
49    Notes
50    -----
51    See https://numpy.org/doc/stable/reference/random/parallel.html for more info.
52
53    On PCG64DXSM:
54        - https://numpy.org/doc/stable/reference/random/upgrading-pcg64.html#upgrading-pcg64
55        - https://numpy.org/doc/stable/reference/random/bit_generators/pcg64dxsm.html
56
57    Examples
58    --------
59    >>> rng = rng_streams(5)  # make 5 random number generators
60    >>> a = rng[1].integers(1, 10, 100)
61    """
62    ss = random.SeedSequence(random_state)
63    seeds = ss.spawn(n)
64
65    # return [random.default_rng(seed) for seed in seeds]  # PCG64 generator objects
66
67    # Return PCG64DXSM generator objects
68    return [random.Generator(random.PCG64DXSM(seed)) for seed in seeds]

Generate independent streams of random numbers spawned from the same initial seed.

Parameters
  • n (int): number of generators/streams to generate.
  • random_state (int, optional): initial seed from which n new seeds are spawned.
Returns
  • list[PCG64DXSM Generator objects]: List of n generators of independent streams of random numbers
Notes

See https://numpy.org/doc/stable/reference/random/parallel.html for more info.

On PCG64DXSM: - https://numpy.org/doc/stable/reference/random/upgrading-pcg64.html#upgrading-pcg64 - https://numpy.org/doc/stable/reference/random/bit_generators/pcg64dxsm.html

Examples
>>> rng = rng_streams(5)  # make 5 random number generators
>>> a = rng[1].integers(1, 10, 100)
def measure_runtime(fcn):
71def measure_runtime(fcn):
72    """ Decorator for measuring the duration of a function's execution. """
73    @functools.wraps(fcn)
74    def inner(*args, **kwargs):
75        start = perf_counter()
76        fcn(*args, **kwargs)
77        stop = perf_counter()
78        duration = stop - start
79        if duration < 60:
80            msg = f"Simulation Runtime: {duration:.3f} sec"
81        elif 60 <= duration < 3600:
82            msg = f"Simulation Runtime: {duration / 60:.3f} min"
83        else:
84            msg = f"Simulation Runtime: {duration / 3600:.3f} hr"
85        print(msg)
86
87    return inner

Decorator for measuring the duration of a function's execution.

def r_squared( actual: <built-in function array>, theoretical: <built-in function array>) -> float:
 90def r_squared(actual: np.array, theoretical: np.array) -> float:
 91    """
 92    Compute the coefficient of determination, $R^2$.
 93
 94    In the case of comparing the average AbStochKin-simulated species
 95    trajectory to its deterministic trajectory. Since the latter is only
 96    meaningful for a homogeneous population, R² should be
 97    close to `1` for a simulated homogeneous process.
 98    For a heterogeneous process, it can be interpreted as how close
 99    the simulated trajectory is to the deterministic trajectory of a
100    *homogeneous* process. In this case, $R^2$ would not be expected
101    to be close to $1$ and the importance of looking at this metric
102    is questionable.
103
104    Parameters
105    ----------
106    actual : numpy.array
107        Actual data obtained through a simulation.
108    theoretical : numpy.array
109        Theoretical data to compare the actual data to.
110
111    Returns
112    -------
113    float
114        The coefficient of determination, $R^2$.
115    """
116    # sst: total sum of squares for simulation avg trajectory
117    sst = np.nansum((actual - np.nanmean(actual)) ** 2)
118    # ssr: sum of square residuals (data vs deterministic prediction)
119    ssr = np.nansum((actual - theoretical) ** 2)
120
121    return 1 - ssr / sst if sst != 0 else np.nan

Compute the coefficient of determination, $R^2$.

In the case of comparing the average AbStochKin-simulated species trajectory to its deterministic trajectory. Since the latter is only meaningful for a homogeneous population, R² should be close to 1 for a simulated homogeneous process. For a heterogeneous process, it can be interpreted as how close the simulated trajectory is to the deterministic trajectory of a homogeneous process. In this case, $R^2$ would not be expected to be close to $1$ and the importance of looking at this metric is questionable.

Parameters
  • actual (numpy.array): Actual data obtained through a simulation.
  • theoretical (numpy.array): Theoretical data to compare the actual data to.
Returns
  • float: The coefficient of determination, $R^2$.
def macro_to_micro( macro_val: float | int | list[float | int, ...] | tuple[float | int, float | int], volume: float | int, order: int = 0, *, inverse: bool = False) -> float | list[float, ...] | tuple[float, float]:
124def macro_to_micro(macro_val: float | int | list[float | int, ...] | tuple[float | int, float | int],
125                   volume: float | int,
126                   order: int = 0,
127                   *,
128                   inverse: bool = False) -> float | list[float, ...] | tuple[float, float]:
129    """
130    Convert a kinetic parameter value from macroscopic to microscopic form.
131
132    The ABK algorithm uses microscopic kinetic constants, thus necessitating
133    the conversion of any molar quantities to their microscopic counterpart.
134    For a kinetic parameter, the microscopic form is interpreted as the number
135    of transition events per second (or whatever the time unit may be).
136    For a molar quantity, its microscopic form is the number of particles in
137    the given volume.
138
139    Parameters
140    ----------
141    macro_val : float or int
142        The value of the parameter to be converted, expressed in terms of
143        molar quantities.
144    volume : float or int
145        The volume, in liters, in which the process that the given parameter
146        value is a descriptor of.
147    order : int, default: 0
148        The order of the process whose kinetic parameter is to be converted.
149        The default value of 0 is for parameters (such as Km or K50) whose
150        units are molarity.
151    inverse : bool, default: False
152        Perform the inverse of this operation. That is, convert
153        from microscopic to macroscopic form.
154
155    Returns
156    --------
157    float
158        A kinetic parameter is returned in units of reciprocal seconds.
159        A molar quantity is returned as the number of particles in the
160        given volume.
161
162    Notes
163    -----
164    - A kinetic parameter for a 1st order process will remain unchanged
165    because its units are already reciprocal seconds.
166
167    Reference
168    ---------
169    Plakantonakis, Alex. “Agent-based Kinetics: A Nonspatial Stochastic Method
170    for Simulating the Dynamics of Heterogeneous Populations.”
171    OSF Preprints, 26 July 2019. Web. Section 2.1.
172    """
173    assert volume > 0, "The volume has to be a positive quantity."
174    assert order >= 0, "The process order cannot be negative."
175
176    denom = (N_A * volume) ** (order - 1) if not inverse else 1 / (N_A * volume) ** (order - 1)
177
178    if isinstance(macro_val, (list, tuple)):
179        assert all([True if val >= 0 else False for val in macro_val]), \
180            "The parameter values cannot be negative."
181        micro_vals = [val / denom for val in macro_val]
182        return micro_vals if isinstance(macro_val, list) else tuple(micro_vals)
183    else:
184        assert macro_val >= 0, "The parameter value cannot be negative."
185        return macro_val / denom

Convert a kinetic parameter value from macroscopic to microscopic form.

The ABK algorithm uses microscopic kinetic constants, thus necessitating the conversion of any molar quantities to their microscopic counterpart. For a kinetic parameter, the microscopic form is interpreted as the number of transition events per second (or whatever the time unit may be). For a molar quantity, its microscopic form is the number of particles in the given volume.

Parameters
  • macro_val (float or int): The value of the parameter to be converted, expressed in terms of molar quantities.
  • volume (float or int): The volume, in liters, in which the process that the given parameter value is a descriptor of.
  • order : int, default (0): The order of the process whose kinetic parameter is to be converted. The default value of 0 is for parameters (such as Km or K50) whose units are molarity.
  • inverse : bool, default (False): Perform the inverse of this operation. That is, convert from microscopic to macroscopic form.
Returns
  • float: A kinetic parameter is returned in units of reciprocal seconds. A molar quantity is returned as the number of particles in the given volume.
Notes
  • A kinetic parameter for a 1st order process will remain unchanged because its units are already reciprocal seconds.
Reference

Plakantonakis, Alex. “Agent-based Kinetics: A Nonspatial Stochastic Method for Simulating the Dynamics of Heterogeneous Populations.” OSF Preprints, 26 July 2019. Web. Section 2.1.