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 +21 -0
- jabsim-0.1.0/PKG-INFO +132 -0
- jabsim-0.1.0/README.md +117 -0
- jabsim-0.1.0/__init__.py +13 -0
- jabsim-0.1.0/jabsim.egg-info/PKG-INFO +132 -0
- jabsim-0.1.0/jabsim.egg-info/SOURCES.txt +13 -0
- jabsim-0.1.0/jabsim.egg-info/dependency_links.txt +1 -0
- jabsim-0.1.0/jabsim.egg-info/requires.txt +6 -0
- jabsim-0.1.0/jabsim.egg-info/top_level.txt +1 -0
- jabsim-0.1.0/pyproject.toml +23 -0
- jabsim-0.1.0/setup.cfg +4 -0
- jabsim-0.1.0/simulators.py +224 -0
- jabsim-0.1.0/tests/test_simulators.py +29 -0
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
|
+
```
|
jabsim-0.1.0/__init__.py
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
|
+
```
|
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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,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')
|