jabsim 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
jabsim-0.1.0/LICENCE ADDED
@@ -0,0 +1,21 @@
1
+ MIT LICENCE
2
+
3
+ Copyright © 2026 Kirill Sechkar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the “Software”), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
jabsim-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: jabsim
3
+ Version: 0.1.0
4
+ Summary: Simple ODE simulation with JAX for biological systems
5
+ Author-email: Kirill Sechkar <useful.instructive@proton.me>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENCE
9
+ Requires-Dist: jax>=0.4
10
+ Requires-Dist: numpy>=1.24
11
+ Requires-Dist: scipy>=1.10
12
+ Provides-Extra: test
13
+ Requires-Dist: pytest>=7; extra == "test"
14
+ Dynamic: license-file
15
+
16
+ # jabsim
17
+
18
+ A jax-based package for simulating ODE models of biological systems,
19
+ where **all variables are non-negative**. This enforcement of non-negativity, which neither `scipy.solve_ivp` nor `diffrax`
20
+ solvers can give you, is why you may need jabsim.
21
+ This is achieved by clamping the state variables to zero whenever they are negative
22
+
23
+ jabsim is powered by the [jax](https://github.com/jax-ml/jax) package for high-performance computing and parallelisation.
24
+ This means that jabsim simulations can be jit-compiled and parallelised on a GPU or TPU using `jax.vmap` or shard mapping.
25
+
26
+ ## How to use jabsim
27
+
28
+ 1. Make an ODE function which calculates the derivative $\frac{dx}{dt}$ from the arguments `t, x, par` in this order.
29
+ - What do the arguments stand for?
30
+ - `t` is the time at this point in the simulation
31
+ - `x` is the state vector at this time point
32
+ - `args` is a tuple of extra arguments passed on to the ODE function.
33
+ - **IMPORTANT**: if you don't know jax, there are a few differences to keep in mind:
34
+ - If you normally use `numpy` functions in your ODE, import `jax.numpy` as `jnp` and use it instead of `np`. jax with `jax.numpy` will get automatically installed as a dependency of jabsim when you install it.
35
+ - Be careful using loops and if-statements. If you're an amateur programmer, just avoid doing all that. Otherwise, have a look at the [JAX documentation](https://docs.jax.dev/en/latest/index.html) .
36
+ 2. Import `jabsim` and call `jabsim.sim` to simulate. The arguments are as follows:
37
+ - `par`: a list, array or dict of model parameters as in your ODE function
38
+ - `model_ode`: the ODE function you created
39
+ - `x0`: the initial state vector as a 1D `np.array` or `jnp.array`
40
+ - `tf`: tuple, array or list. The ODE will be simulated
41
+ - `savetimestep`: interval between the time points at which the trajectory is saved
42
+ - `simulator`: string specifyingthe simulation method to use
43
+ - `"euler"`: Euler simulator.
44
+ - `"rk4"`: Runge-Kutta 4th order simulator. Slower per ODE integration step but more accurate, hence allowing larger steps for the same accuracy.
45
+ - `ode_steps_in_savetimestep`: number of ODE integration steps within one timestep
46
+ - e.g. if `savetimestep=0.5` hours and `ode_step_in_savetimesteps=100`, there will be 100 integration steps per 0.5 hour, so the ODE integration step size will be 0.5/100=0.05 hours.
47
+ - high `ode_steps_in_savetimestep` number increases accuracy but increases runtimes
48
+ - `return_numpy`: if `True` (by default, it is) the output will be in the `np.array` format, otherwise it will be `jnp.array`.
49
+ 3. Running `jabsim.sim()` will return the arrays `ts` and `xs` as `np.array` or `jnp.array`, as well as a boolean value `success`.
50
+ - `ts`: array of timepoints between `tf[0]` and `tf[1]` with `savetimestep` hours, seconds or whatever units you are using between each two consecutive point
51
+ - `xs`: system trajectory saved as an array at the time points in `ts` - axis 0 for time, axis 1 for entries in the state vector (i.e. `xs.shape[0]=len(ts)`).
52
+ - `success`: boolean value; `True` if no entry in `xs` is `nan` or `inf`, `False` otherwise.
53
+
54
+ ## Notes
55
+ - In practice, 500 steps per hour (e.g. `savetimestep=0.5, ode_steps_in_savetimestep=250` or `savetimestep=1.0, ode_steps_in_savetimestep=500`) works well for the RK4 solver. For the Euler solver, 1e4 steps per hours is reasonably good.
56
+ - For benchmarking, you can also set `simulator="scipy"` to simulate your ODE with `scipy.solve_ivp` (but without any of the delicious jax features of the solvers above). In that case, don't use the arguments `ode_steps_in_savetimestep` and `savetimestep`. Instead, you can *optionally* specify:
57
+ - `solver`: string describing any solver which may be used with `scipy.solve_ivp`. By default, we have `solver="LSODA"`.
58
+ - `tols`: dictionary of relative and absolute tolerances for the scipy solver. By default, `tols={'rtol': 1e-6, 'atol': 1e-9}`.
59
+ - `dt0`: starting integration step size. By default, `dt0=0.1`.
60
+ - If you want to make use of jax parallelisation, make sure to set `return_numpy=False` so that the solver would operate with `jnp.array` objects only.
61
+
62
+ ## Example
63
+
64
+ Let us integrate a simple one-dimensional ODE $\frac{dx}{dt} = a x^2$. For the initial condition $x_0=1$ and $a=0.4$,
65
+ this has the analytical solution $x = \frac{1}{1-0.4t}$. This means we can verify that for `savetimestep=0.5`,
66
+ `jabsim.sim()` produces `ts=np.array([0, 0.5, 1.0])` and `xs=np.array([1.0, 1.25, 1.66666667])`.
67
+ All entries in `xs` are finite, hence `success=True`.
68
+
69
+ ```python
70
+ # import jabsim
71
+ import jabsim
72
+
73
+ # import jax.numpy for numpy operations
74
+ import jax.numpy as jnp
75
+
76
+ # our model ODE function returning a list of one element
77
+ def model_ode(t, x, args):
78
+ # unpack args - get the dictiory of parameters
79
+ par, = args
80
+
81
+ # use jnp to square x
82
+ # (here you could just as well use x[0]**2, we just want to make a point)
83
+ x_squared = jnp.square(x[0])
84
+
85
+ # return dx/dt as a list - with one entry for a one-dimensional ODE
86
+ return [par['a'] * x_squared]
87
+
88
+ # our dictionary of paramneters
89
+ par = {'a': 0.4}
90
+
91
+ ts, xs, success = jabsim.sim(
92
+ model_ode=model_ode,
93
+ args=(par,),
94
+ x0=jnp.array([1.0]),
95
+ tf=(0.0, 1.0),
96
+ savetimestep=0.5,
97
+ simulator='rk4',
98
+ ode_steps_in_savetimestep=10,
99
+ )
100
+
101
+ # print the timne
102
+ print(ts)
103
+ print(xs)
104
+ print(success)
105
+ ```
106
+
107
+ # Citation
108
+ If you find this package useful in your work, please cite the paper below:
109
+ code for its Showcase 2 served as jabsim's direct ideological precursor.
110
+ ```bibtex
111
+ @article{Gallup2024,
112
+ author = {Gallup, Olivia and Sechkar, Kirill and Towers, Sebastian and Steel, Harrison},
113
+ title = {Computational Synthetic Biology Enabled through JAX: A Showcase},
114
+ journal = {ACS Synth. Biol.},
115
+ volume = {13},
116
+ number = {9},
117
+ pages = {3046},
118
+ year = {2024},
119
+ doi = {10.1021/acssynbio.4c00307}
120
+ }
121
+ ```
122
+
123
+ The original JAX package should be cited as:
124
+ ```bibtex
125
+ @software{jax2018github,
126
+ author = {James Bradbury and Roy Frostig and Peter Hawkins and Matthew James Johnson and Yash Katariya and Chris Leary and Dougal Maclaurin and George Necula and Adam Paszke and Jake Vander{P}las and Skye Wanderman-{M}ilne and Qiao Zhang},
127
+ title = {{JAX}: composable transformations of {P}ython+{N}um{P}y programs},
128
+ url = {http://github.com/jax-ml/jax},
129
+ version = {0.3.13},
130
+ year = {2018},
131
+ }
132
+ ```
jabsim-0.1.0/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # jabsim
2
+
3
+ A jax-based package for simulating ODE models of biological systems,
4
+ where **all variables are non-negative**. This enforcement of non-negativity, which neither `scipy.solve_ivp` nor `diffrax`
5
+ solvers can give you, is why you may need jabsim.
6
+ This is achieved by clamping the state variables to zero whenever they are negative
7
+
8
+ jabsim is powered by the [jax](https://github.com/jax-ml/jax) package for high-performance computing and parallelisation.
9
+ This means that jabsim simulations can be jit-compiled and parallelised on a GPU or TPU using `jax.vmap` or shard mapping.
10
+
11
+ ## How to use jabsim
12
+
13
+ 1. Make an ODE function which calculates the derivative $\frac{dx}{dt}$ from the arguments `t, x, par` in this order.
14
+ - What do the arguments stand for?
15
+ - `t` is the time at this point in the simulation
16
+ - `x` is the state vector at this time point
17
+ - `args` is a tuple of extra arguments passed on to the ODE function.
18
+ - **IMPORTANT**: if you don't know jax, there are a few differences to keep in mind:
19
+ - If you normally use `numpy` functions in your ODE, import `jax.numpy` as `jnp` and use it instead of `np`. jax with `jax.numpy` will get automatically installed as a dependency of jabsim when you install it.
20
+ - Be careful using loops and if-statements. If you're an amateur programmer, just avoid doing all that. Otherwise, have a look at the [JAX documentation](https://docs.jax.dev/en/latest/index.html) .
21
+ 2. Import `jabsim` and call `jabsim.sim` to simulate. The arguments are as follows:
22
+ - `par`: a list, array or dict of model parameters as in your ODE function
23
+ - `model_ode`: the ODE function you created
24
+ - `x0`: the initial state vector as a 1D `np.array` or `jnp.array`
25
+ - `tf`: tuple, array or list. The ODE will be simulated
26
+ - `savetimestep`: interval between the time points at which the trajectory is saved
27
+ - `simulator`: string specifyingthe simulation method to use
28
+ - `"euler"`: Euler simulator.
29
+ - `"rk4"`: Runge-Kutta 4th order simulator. Slower per ODE integration step but more accurate, hence allowing larger steps for the same accuracy.
30
+ - `ode_steps_in_savetimestep`: number of ODE integration steps within one timestep
31
+ - e.g. if `savetimestep=0.5` hours and `ode_step_in_savetimesteps=100`, there will be 100 integration steps per 0.5 hour, so the ODE integration step size will be 0.5/100=0.05 hours.
32
+ - high `ode_steps_in_savetimestep` number increases accuracy but increases runtimes
33
+ - `return_numpy`: if `True` (by default, it is) the output will be in the `np.array` format, otherwise it will be `jnp.array`.
34
+ 3. Running `jabsim.sim()` will return the arrays `ts` and `xs` as `np.array` or `jnp.array`, as well as a boolean value `success`.
35
+ - `ts`: array of timepoints between `tf[0]` and `tf[1]` with `savetimestep` hours, seconds or whatever units you are using between each two consecutive point
36
+ - `xs`: system trajectory saved as an array at the time points in `ts` - axis 0 for time, axis 1 for entries in the state vector (i.e. `xs.shape[0]=len(ts)`).
37
+ - `success`: boolean value; `True` if no entry in `xs` is `nan` or `inf`, `False` otherwise.
38
+
39
+ ## Notes
40
+ - In practice, 500 steps per hour (e.g. `savetimestep=0.5, ode_steps_in_savetimestep=250` or `savetimestep=1.0, ode_steps_in_savetimestep=500`) works well for the RK4 solver. For the Euler solver, 1e4 steps per hours is reasonably good.
41
+ - For benchmarking, you can also set `simulator="scipy"` to simulate your ODE with `scipy.solve_ivp` (but without any of the delicious jax features of the solvers above). In that case, don't use the arguments `ode_steps_in_savetimestep` and `savetimestep`. Instead, you can *optionally* specify:
42
+ - `solver`: string describing any solver which may be used with `scipy.solve_ivp`. By default, we have `solver="LSODA"`.
43
+ - `tols`: dictionary of relative and absolute tolerances for the scipy solver. By default, `tols={'rtol': 1e-6, 'atol': 1e-9}`.
44
+ - `dt0`: starting integration step size. By default, `dt0=0.1`.
45
+ - If you want to make use of jax parallelisation, make sure to set `return_numpy=False` so that the solver would operate with `jnp.array` objects only.
46
+
47
+ ## Example
48
+
49
+ Let us integrate a simple one-dimensional ODE $\frac{dx}{dt} = a x^2$. For the initial condition $x_0=1$ and $a=0.4$,
50
+ this has the analytical solution $x = \frac{1}{1-0.4t}$. This means we can verify that for `savetimestep=0.5`,
51
+ `jabsim.sim()` produces `ts=np.array([0, 0.5, 1.0])` and `xs=np.array([1.0, 1.25, 1.66666667])`.
52
+ All entries in `xs` are finite, hence `success=True`.
53
+
54
+ ```python
55
+ # import jabsim
56
+ import jabsim
57
+
58
+ # import jax.numpy for numpy operations
59
+ import jax.numpy as jnp
60
+
61
+ # our model ODE function returning a list of one element
62
+ def model_ode(t, x, args):
63
+ # unpack args - get the dictiory of parameters
64
+ par, = args
65
+
66
+ # use jnp to square x
67
+ # (here you could just as well use x[0]**2, we just want to make a point)
68
+ x_squared = jnp.square(x[0])
69
+
70
+ # return dx/dt as a list - with one entry for a one-dimensional ODE
71
+ return [par['a'] * x_squared]
72
+
73
+ # our dictionary of paramneters
74
+ par = {'a': 0.4}
75
+
76
+ ts, xs, success = jabsim.sim(
77
+ model_ode=model_ode,
78
+ args=(par,),
79
+ x0=jnp.array([1.0]),
80
+ tf=(0.0, 1.0),
81
+ savetimestep=0.5,
82
+ simulator='rk4',
83
+ ode_steps_in_savetimestep=10,
84
+ )
85
+
86
+ # print the timne
87
+ print(ts)
88
+ print(xs)
89
+ print(success)
90
+ ```
91
+
92
+ # Citation
93
+ If you find this package useful in your work, please cite the paper below:
94
+ code for its Showcase 2 served as jabsim's direct ideological precursor.
95
+ ```bibtex
96
+ @article{Gallup2024,
97
+ author = {Gallup, Olivia and Sechkar, Kirill and Towers, Sebastian and Steel, Harrison},
98
+ title = {Computational Synthetic Biology Enabled through JAX: A Showcase},
99
+ journal = {ACS Synth. Biol.},
100
+ volume = {13},
101
+ number = {9},
102
+ pages = {3046},
103
+ year = {2024},
104
+ doi = {10.1021/acssynbio.4c00307}
105
+ }
106
+ ```
107
+
108
+ The original JAX package should be cited as:
109
+ ```bibtex
110
+ @software{jax2018github,
111
+ author = {James Bradbury and Roy Frostig and Peter Hawkins and Matthew James Johnson and Yash Katariya and Chris Leary and Dougal Maclaurin and George Necula and Adam Paszke and Jake Vander{P}las and Skye Wanderman-{M}ilne and Qiao Zhang},
112
+ title = {{JAX}: composable transformations of {P}ython+{N}um{P}y programs},
113
+ url = {http://github.com/jax-ml/jax},
114
+ version = {0.3.13},
115
+ year = {2018},
116
+ }
117
+ ```
@@ -0,0 +1,13 @@
1
+ """Simple JAX-based simulation of biosystem ODEs."""
2
+
3
+ # import simulator functions
4
+ from .simulators import (
5
+ loopy_euler,
6
+ loopy_rk4,
7
+ sim,
8
+ )
9
+
10
+ __all__ = [
11
+ 'sim'
12
+ ]
13
+
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.4
2
+ Name: jabsim
3
+ Version: 0.1.0
4
+ Summary: Simple ODE simulation with JAX for biological systems
5
+ Author-email: Kirill Sechkar <useful.instructive@proton.me>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENCE
9
+ Requires-Dist: jax>=0.4
10
+ Requires-Dist: numpy>=1.24
11
+ Requires-Dist: scipy>=1.10
12
+ Provides-Extra: test
13
+ Requires-Dist: pytest>=7; extra == "test"
14
+ Dynamic: license-file
15
+
16
+ # jabsim
17
+
18
+ A jax-based package for simulating ODE models of biological systems,
19
+ where **all variables are non-negative**. This enforcement of non-negativity, which neither `scipy.solve_ivp` nor `diffrax`
20
+ solvers can give you, is why you may need jabsim.
21
+ This is achieved by clamping the state variables to zero whenever they are negative
22
+
23
+ jabsim is powered by the [jax](https://github.com/jax-ml/jax) package for high-performance computing and parallelisation.
24
+ This means that jabsim simulations can be jit-compiled and parallelised on a GPU or TPU using `jax.vmap` or shard mapping.
25
+
26
+ ## How to use jabsim
27
+
28
+ 1. Make an ODE function which calculates the derivative $\frac{dx}{dt}$ from the arguments `t, x, par` in this order.
29
+ - What do the arguments stand for?
30
+ - `t` is the time at this point in the simulation
31
+ - `x` is the state vector at this time point
32
+ - `args` is a tuple of extra arguments passed on to the ODE function.
33
+ - **IMPORTANT**: if you don't know jax, there are a few differences to keep in mind:
34
+ - If you normally use `numpy` functions in your ODE, import `jax.numpy` as `jnp` and use it instead of `np`. jax with `jax.numpy` will get automatically installed as a dependency of jabsim when you install it.
35
+ - Be careful using loops and if-statements. If you're an amateur programmer, just avoid doing all that. Otherwise, have a look at the [JAX documentation](https://docs.jax.dev/en/latest/index.html) .
36
+ 2. Import `jabsim` and call `jabsim.sim` to simulate. The arguments are as follows:
37
+ - `par`: a list, array or dict of model parameters as in your ODE function
38
+ - `model_ode`: the ODE function you created
39
+ - `x0`: the initial state vector as a 1D `np.array` or `jnp.array`
40
+ - `tf`: tuple, array or list. The ODE will be simulated
41
+ - `savetimestep`: interval between the time points at which the trajectory is saved
42
+ - `simulator`: string specifyingthe simulation method to use
43
+ - `"euler"`: Euler simulator.
44
+ - `"rk4"`: Runge-Kutta 4th order simulator. Slower per ODE integration step but more accurate, hence allowing larger steps for the same accuracy.
45
+ - `ode_steps_in_savetimestep`: number of ODE integration steps within one timestep
46
+ - e.g. if `savetimestep=0.5` hours and `ode_step_in_savetimesteps=100`, there will be 100 integration steps per 0.5 hour, so the ODE integration step size will be 0.5/100=0.05 hours.
47
+ - high `ode_steps_in_savetimestep` number increases accuracy but increases runtimes
48
+ - `return_numpy`: if `True` (by default, it is) the output will be in the `np.array` format, otherwise it will be `jnp.array`.
49
+ 3. Running `jabsim.sim()` will return the arrays `ts` and `xs` as `np.array` or `jnp.array`, as well as a boolean value `success`.
50
+ - `ts`: array of timepoints between `tf[0]` and `tf[1]` with `savetimestep` hours, seconds or whatever units you are using between each two consecutive point
51
+ - `xs`: system trajectory saved as an array at the time points in `ts` - axis 0 for time, axis 1 for entries in the state vector (i.e. `xs.shape[0]=len(ts)`).
52
+ - `success`: boolean value; `True` if no entry in `xs` is `nan` or `inf`, `False` otherwise.
53
+
54
+ ## Notes
55
+ - In practice, 500 steps per hour (e.g. `savetimestep=0.5, ode_steps_in_savetimestep=250` or `savetimestep=1.0, ode_steps_in_savetimestep=500`) works well for the RK4 solver. For the Euler solver, 1e4 steps per hours is reasonably good.
56
+ - For benchmarking, you can also set `simulator="scipy"` to simulate your ODE with `scipy.solve_ivp` (but without any of the delicious jax features of the solvers above). In that case, don't use the arguments `ode_steps_in_savetimestep` and `savetimestep`. Instead, you can *optionally* specify:
57
+ - `solver`: string describing any solver which may be used with `scipy.solve_ivp`. By default, we have `solver="LSODA"`.
58
+ - `tols`: dictionary of relative and absolute tolerances for the scipy solver. By default, `tols={'rtol': 1e-6, 'atol': 1e-9}`.
59
+ - `dt0`: starting integration step size. By default, `dt0=0.1`.
60
+ - If you want to make use of jax parallelisation, make sure to set `return_numpy=False` so that the solver would operate with `jnp.array` objects only.
61
+
62
+ ## Example
63
+
64
+ Let us integrate a simple one-dimensional ODE $\frac{dx}{dt} = a x^2$. For the initial condition $x_0=1$ and $a=0.4$,
65
+ this has the analytical solution $x = \frac{1}{1-0.4t}$. This means we can verify that for `savetimestep=0.5`,
66
+ `jabsim.sim()` produces `ts=np.array([0, 0.5, 1.0])` and `xs=np.array([1.0, 1.25, 1.66666667])`.
67
+ All entries in `xs` are finite, hence `success=True`.
68
+
69
+ ```python
70
+ # import jabsim
71
+ import jabsim
72
+
73
+ # import jax.numpy for numpy operations
74
+ import jax.numpy as jnp
75
+
76
+ # our model ODE function returning a list of one element
77
+ def model_ode(t, x, args):
78
+ # unpack args - get the dictiory of parameters
79
+ par, = args
80
+
81
+ # use jnp to square x
82
+ # (here you could just as well use x[0]**2, we just want to make a point)
83
+ x_squared = jnp.square(x[0])
84
+
85
+ # return dx/dt as a list - with one entry for a one-dimensional ODE
86
+ return [par['a'] * x_squared]
87
+
88
+ # our dictionary of paramneters
89
+ par = {'a': 0.4}
90
+
91
+ ts, xs, success = jabsim.sim(
92
+ model_ode=model_ode,
93
+ args=(par,),
94
+ x0=jnp.array([1.0]),
95
+ tf=(0.0, 1.0),
96
+ savetimestep=0.5,
97
+ simulator='rk4',
98
+ ode_steps_in_savetimestep=10,
99
+ )
100
+
101
+ # print the timne
102
+ print(ts)
103
+ print(xs)
104
+ print(success)
105
+ ```
106
+
107
+ # Citation
108
+ If you find this package useful in your work, please cite the paper below:
109
+ code for its Showcase 2 served as jabsim's direct ideological precursor.
110
+ ```bibtex
111
+ @article{Gallup2024,
112
+ author = {Gallup, Olivia and Sechkar, Kirill and Towers, Sebastian and Steel, Harrison},
113
+ title = {Computational Synthetic Biology Enabled through JAX: A Showcase},
114
+ journal = {ACS Synth. Biol.},
115
+ volume = {13},
116
+ number = {9},
117
+ pages = {3046},
118
+ year = {2024},
119
+ doi = {10.1021/acssynbio.4c00307}
120
+ }
121
+ ```
122
+
123
+ The original JAX package should be cited as:
124
+ ```bibtex
125
+ @software{jax2018github,
126
+ author = {James Bradbury and Roy Frostig and Peter Hawkins and Matthew James Johnson and Yash Katariya and Chris Leary and Dougal Maclaurin and George Necula and Adam Paszke and Jake Vander{P}las and Skye Wanderman-{M}ilne and Qiao Zhang},
127
+ title = {{JAX}: composable transformations of {P}ython+{N}um{P}y programs},
128
+ url = {http://github.com/jax-ml/jax},
129
+ version = {0.3.13},
130
+ year = {2018},
131
+ }
132
+ ```
@@ -0,0 +1,13 @@
1
+ LICENCE
2
+ README.md
3
+ __init__.py
4
+ pyproject.toml
5
+ simulators.py
6
+ ./__init__.py
7
+ ./simulators.py
8
+ jabsim.egg-info/PKG-INFO
9
+ jabsim.egg-info/SOURCES.txt
10
+ jabsim.egg-info/dependency_links.txt
11
+ jabsim.egg-info/requires.txt
12
+ jabsim.egg-info/top_level.txt
13
+ tests/test_simulators.py
@@ -0,0 +1,6 @@
1
+ jax>=0.4
2
+ numpy>=1.24
3
+ scipy>=1.10
4
+
5
+ [test]
6
+ pytest>=7
@@ -0,0 +1 @@
1
+ jabsim
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "jabsim"
7
+ version = "0.1.0"
8
+ description = "Simple ODE simulation with JAX for biological systems"
9
+ readme = "README.md"
10
+ authors = [{name = "Kirill Sechkar", email = "useful.instructive@proton.me"}]
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "jax>=0.4",
14
+ "numpy>=1.24",
15
+ "scipy>=1.10",
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ test = ["pytest>=7"]
20
+
21
+ [tool.setuptools]
22
+ packages = ["jabsim"]
23
+ package-dir = {jabsim = "."}
jabsim-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,224 @@
1
+ # JABSIM/SIMULATORS.PY - functions for ODE simulation
2
+
3
+ # PACKAGE IMPORTS
4
+ # import configured jax
5
+ import jax
6
+ # import everything else
7
+ import numpy as np
8
+ import scipy.integrate
9
+ import jax.numpy as jnp
10
+
11
+
12
+ # SOLVERS --------------------------------------------------------------------------------------------------------------
13
+
14
+ # Euler solver using jax.lax.fori_loop
15
+ def loopy_euler(model_ode,
16
+ save_cntr,
17
+ sim_state_record,
18
+ ode_step,
19
+ ode_steps_in_savetimestep,
20
+ args):
21
+ """
22
+ Use Euler integration to get the simulator state to be recorded at the current saving point into
23
+ sim_state_record - to be used with jax.lax.fori_loop
24
+
25
+ Args:
26
+ model_ode: ODE function handle
27
+ save_cntr: current saving point
28
+ sim_state_record: dictionary containing a record of simulation time points and state vector values
29
+ ode_step: simulation time step duration
30
+ ode_steps_in_savetimestep: number of ODE integration steps between each recording
31
+ args: additional arguments to pass to the ODE function
32
+
33
+ Returns:
34
+ new_sim_state_record: record of simulation time points and state vector values - now with the right x at xs[save_cntr,:]
35
+ """
36
+
37
+ # EULER STEP FUNCTION (with non-negativity constraints)
38
+ def euler_step(step_cntr, t_x):
39
+ return {
40
+ # entries updated over the course of the Euler step
41
+ 't': t_x['t'] + ode_step,
42
+ 'x': jnp.maximum(t_x['x'] + ode_step * model_ode(t_x['t'], t_x['x'], args),0),
43
+ }
44
+ # GET THE PRESENT TIME POINT AND STATE
45
+ last_t_x = {'t': sim_state_record['ts'][save_cntr-1], 'x': sim_state_record['xs'][save_cntr-1, :]}
46
+ this_t_x = jax.lax.fori_loop(0, ode_steps_in_savetimestep, euler_step, last_t_x)
47
+
48
+ # RETURN UPDATED SIMULATOR STATE
49
+ new_sim_state_record = {'ts': sim_state_record['ts'], 'xs': sim_state_record['xs'].at[save_cntr, :].set(this_t_x['x'])}
50
+ return new_sim_state_record
51
+
52
+ # Fourth-order Runge-Kutta solver using jax.lax.fori_loop
53
+ def loopy_rk4(model_ode,
54
+ save_cntr,
55
+ sim_state_record,
56
+ ode_step,
57
+ ode_steps_in_savetimestep,
58
+ args):
59
+ """
60
+ Use fourth-order Runge-Kutta integration to get the simulator state to be recorded at the current saving point
61
+ into sim_state_record - to be used with jax.lax.fori_loop
62
+
63
+ Args:
64
+ model_ode: ODE function handle
65
+ save_cntr: current saving point
66
+ sim_state_record: dictionary containing a record of simulation time points and state vector values
67
+ ode_step: simulation time step duration
68
+ ode_steps_in_savetimestep: number of ODE integration steps between each recording
69
+ args: additional arguments to pass to the ODE function
70
+
71
+ Returns:
72
+ new_sim_state_record: record of simulation time points and state vector values - now with the right x at xs[save_cntr,:]
73
+ """
74
+
75
+ # FOURTH-ORDER RUNGE-KUTTA STEP FUNCTION (with non-negativity constraints)
76
+ def rk4_step(step_cntr, t_x):
77
+ k1 = model_ode(t_x['t'], t_x['x'], args)
78
+ k2 = model_ode(t_x['t'] + ode_step / 2, jnp.maximum(t_x['x'] + ode_step * k1 / 2, 0), args)
79
+ k3 = model_ode(t_x['t'] + ode_step / 2, jnp.maximum(t_x['x'] + ode_step * k2 / 2, 0), args)
80
+ k4 = model_ode(t_x['t'] + ode_step, jnp.maximum(t_x['x'] + ode_step * k3, 0), args)
81
+ return {
82
+ # entries updated over the course of the Runge-Kutta step
83
+ 't': t_x['t'] + ode_step,
84
+ 'x': jnp.maximum(t_x['x'] + ode_step * (k1 + 2 * k2 + 2 * k3 + k4) / 6, 0),
85
+ }
86
+
87
+ # GET THE PRESENT TIME POINT AND STATE
88
+ last_t_x = {'t': sim_state_record['ts'][save_cntr-1], 'x': sim_state_record['xs'][save_cntr-1, :]}
89
+ this_t_x = jax.lax.fori_loop(0, ode_steps_in_savetimestep, rk4_step, last_t_x)
90
+
91
+ # RETURN UPDATED SIMULATOR STATE
92
+ new_sim_state_record = {'ts': sim_state_record['ts'], 'xs': sim_state_record['xs'].at[save_cntr, :].set(this_t_x['x'])}
93
+ return new_sim_state_record
94
+
95
+
96
+ # SIMULATOR FUNCTION ---------------------------------------------------------------------------------------------------
97
+ # simulator function
98
+ def sim(model_ode, args, x0, tf, savetimestep, simulator='rk4', return_numpy=True, **kwargs):
99
+ """
100
+ Simulate an ODE model.
101
+
102
+ Args:
103
+ model_ode: ODE function handle
104
+ args: extra arguments for the ODE function
105
+ x0: initial condition
106
+ tf: time span of the simulation
107
+ savetimestep: saving the siulation every savetimestep hours
108
+ simulator: simulation method
109
+ return_numpy: True if returning numpy arrays, False is jax.numpy arrays
110
+ **kwargs: additional arguments to pass to the simulation method - specific to the simulator
111
+
112
+ Returns:
113
+ xs: system state at each time point specfied
114
+ """
115
+
116
+ if (simulator == 'euler'):
117
+ # define custom Euler parameters if not specified
118
+ ## number of ODE integration steps between each record point
119
+ ode_steps_in_savetimestep = kwargs.get('ode_steps_in_savetimestep', 1e4)
120
+
121
+ # define the time points at which we save the solution
122
+ ts = jnp.concatenate((jnp.arange(tf[0], tf[1], savetimestep), jnp.array([tf[1]])), dtype=jnp.float64)
123
+
124
+ # calculate the time step for the Euler integration
125
+ ode_step = savetimestep / ode_steps_in_savetimestep
126
+
127
+ # make the model ode function return a jnp.array
128
+ model_ode_jnp = lambda t, x, args: jnp.array(model_ode(t, x, args))
129
+
130
+ # make the retrieval of next x a lambda-function for jax.lax.scanning
131
+ loop_step = lambda save_cntr, sim_state_record: loopy_euler(model_ode_jnp,
132
+ save_cntr,
133
+ sim_state_record, # simulator state
134
+ ode_step, # simulation time step
135
+ int(ode_steps_in_savetimestep), # number of ODE integration steps between each recording
136
+ args)
137
+
138
+ # initalise the simulator state: (t, x) - x initialised with initial conditions
139
+ sim_state_record = {'ts': ts, 'xs': jnp.tile(jnp.array(x0), (ts.shape[0], 1))}
140
+
141
+ # simulate
142
+ sim_state_rec_final = jax.lax.fori_loop(1, ts.shape[0], loop_step, sim_state_record)
143
+ xs = sim_state_rec_final['xs']
144
+
145
+ # check for simulation success (i.e. no nans or infs in x)
146
+ success = not bool(jnp.any(jnp.isnan(xs)) or jnp.any(jnp.isinf(xs)))
147
+
148
+ # return numpy or jax.numpy arrays
149
+ if(return_numpy):
150
+ return np.array(ts), np.array(xs), success
151
+ else:
152
+ return ts, xs, success
153
+
154
+ elif (simulator == 'rk4'):
155
+ # define custom fourth-order Runge-Kutta parameters if not specified
156
+ ## number of ODE integration steps between each record point
157
+ ode_steps_in_savetimestep = kwargs.get('ode_steps_in_savetimestep', 1e4)
158
+
159
+ # define the time points at which we save the solution
160
+ ts = jnp.concatenate((jnp.arange(tf[0], tf[1], savetimestep), jnp.array([tf[1]])), dtype=jnp.float64)
161
+
162
+ # calculate the time step for the fourth-order Runge-Kutta integration
163
+ ode_step = savetimestep / ode_steps_in_savetimestep
164
+
165
+ # make the model ode function return a jnp.array
166
+ model_ode_jnp = lambda t, x, args: jnp.array(model_ode(t, x, args))
167
+
168
+ # make the retrieval of next x a lambda-function for jax.lax.fori_loop
169
+ loop_step = lambda save_cntr, sim_state_record: loopy_rk4(model_ode_jnp,
170
+ save_cntr,
171
+ sim_state_record, # simulator state
172
+ ode_step, # simulation time step
173
+ int(ode_steps_in_savetimestep), # number of ODE integration steps between each recording
174
+ args)
175
+
176
+ # initalise the simulator state: (t, x) - x initialised with initial conditions
177
+ sim_state_record = {'ts': ts, 'xs': jnp.tile(jnp.array(x0), (ts.shape[0], 1))}
178
+
179
+ # simulate
180
+ sim_state_rec_final = jax.lax.fori_loop(1, ts.shape[0], loop_step, sim_state_record)
181
+ xs = sim_state_rec_final['xs']
182
+
183
+ # check for simulation success (i.e. no nans or infs in x)
184
+ success = not bool(jnp.any(jnp.isnan(xs)) or jnp.any(jnp.isinf(xs)))
185
+
186
+ # return numpy or jax.numpy arrays
187
+ if (return_numpy):
188
+ return np.array(ts), np.array(xs), success
189
+ else:
190
+ return ts, xs, success
191
+
192
+ elif (simulator == 'scipy'):
193
+ # define ODE integration term
194
+ term = lambda t, x: model_ode(t, x, args)
195
+
196
+ # define the time points at which we save the solution
197
+ ts = np.concatenate((np.arange(tf[0], tf[1], savetimestep), np.array([tf[1]])))
198
+
199
+ # define diffrax parameters if not specified
200
+ ## ODE solver
201
+ solver = kwargs.get('solver', 'LSODA')
202
+ ## ODE integration tolerances to specify the step size controller
203
+ tols = kwargs.get('tols', {'rtol': 1e-6, 'atol': 1e-9})
204
+ ## initial time step
205
+ dt0 = kwargs.get('dt0', 0.1)
206
+
207
+ # solve the ODE
208
+ result = scipy.integrate.solve_ivp(term,
209
+ t_span=(tf[0], tf[-1]),
210
+ y0=x0,
211
+ t_eval=ts,
212
+ method=solver,
213
+ rtol=tols['rtol'], atol=tols['atol'],
214
+ first_step=dt0)
215
+ xs = (result.y).T
216
+
217
+ # return numpy or jax.numpy arrays
218
+ if (return_numpy):
219
+ return ts, xs, result.success
220
+ else:
221
+ return jnp.array(ts), jnp.array(xs), result.success
222
+
223
+ else:
224
+ raise ValueError("Unknown simulator: {}".format(simulator))
@@ -0,0 +1,29 @@
1
+ import numpy as np
2
+ import jax.numpy as jnp
3
+ import pytest
4
+
5
+ import jax.numpy as jnp
6
+ from jabsim import sim
7
+
8
+
9
+ def decay(t, x, args):
10
+ par, = args
11
+ for i in range(0,len(x)):
12
+ x0_sq = jnp.square(x[i])
13
+ return [par['rate'] * x0_sq]
14
+
15
+
16
+ @pytest.mark.parametrize('simulator', ['scipy', 'euler', 'rk4'])
17
+ def test_simulates_exponential_decay(simulator):
18
+ kwargs = {'ode_steps_in_savetimestep': 100} if simulator != 'scipy' else {}
19
+ ts, xs, success = sim(decay, ({'rate': 0.5},), np.array([1.0]), (0.0, 0.5), 0.05,
20
+ simulator=simulator, **kwargs)
21
+
22
+ np.testing.assert_allclose(ts, np.linspace(0.0, 0.5, 11))
23
+ np.testing.assert_allclose(np.asarray(xs[:, 0]), 1/(1-0.5*np.asarray(ts)), rtol=2e-2, atol=1e-4)
24
+ assert(success)
25
+
26
+
27
+ def test_rejects_unknown_simulator():
28
+ with pytest.raises(ValueError, match='Unknown simulator: unknown'):
29
+ sim({}, decay, np.array([1.0]), (0.0, 1.0), 0.25, simulator='unknown')