Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .JuliaFormatter.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
style = "sciml"
format_markdown = true
format_docstrings = true
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
Finite State Projection [[1]](#1) algorithms for chemical reaction networks based on [Catalyst.jl](https://github.com/SciML/Catalyst.jl) and [ModelingToolkit.jl](https://github.com/SciML/ModelingToolkit.jl). Converts descriptions of reaction networks into `ODEProblem`s and `SteadyStateProblem`s for use with [DifferentialEquations.jl](https://github.com/SciML/DifferentialEquations.jl).

## Features:
- Built on top of [Catalyst.jl](https://github.com/SciML/Catalyst.jl)
- FSP equations are generated as `ODEFunction`/`ODEProblem`s and can be solved with [DifferentialEquations.jl](https://github.com/SciML/DifferentialEquations.jl), with on-the-fly generation of targeted functions for improved performance
- The Chemical Master Equation can be represented as a `SparseMatrixCSC`

- Built on top of [Catalyst.jl](https://github.com/SciML/Catalyst.jl)
- FSP equations are generated as `ODEFunction`/`ODEProblem`s and can be solved with [DifferentialEquations.jl](https://github.com/SciML/DifferentialEquations.jl), with on-the-fly generation of targeted functions for improved performance
- The Chemical Master Equation can be represented as a `SparseMatrixCSC`

More information is available in the [documentation](https://kaandocal.github.io/FiniteStateProjection.jl/dev/). Please feel free to open issues and submit pull requests!

## Examples

### Birth-Death System

```julia
using FiniteStateProjection
using OrdinaryDiffEq
Expand All @@ -25,7 +28,7 @@ end
sys = FSPSystem(rn)

# Parameters for our system
ps = [ :σ => 10.0, :d => 1.0 ]
ps = [:σ => 10.0, :d => 1.0]

# Initial distribution (over 1 species)
# Here we start with 0 copies of A
Expand All @@ -35,9 +38,11 @@ u0[1] = 1.0
prob = ODEProblem(sys, u0, (0, 10.0), ps)
sol = solve(prob, Vern7())
```

![Visualisation](docs/src/assets/birth_death.png)

### Telegraph Model

```julia
using FiniteStateProjection
using OrdinaryDiffEq
Expand All @@ -52,16 +57,17 @@ end
sys = FSPSystem(rn)

# Parameters for our system
ps = [ :σ_on => 0.25, :σ_off => 0.15, :ρ => 15.0, :d => 1.0 ]
ps = [:σ_on => 0.25, :σ_off => 0.15, :ρ => 15.0, :d => 1.0]

# Initial distribution (over two species)
# Here we start with 0 copies of G_on and M
u0 = zeros(2, 50)
u0[1,1] = 1.0
u0[1, 1] = 1.0

prob = ODEProblem(sys, u0, (0, 10.0), ps)
sol = solve(prob, Vern7())
```

![Visualisation](docs/src/assets/telegraph.png)

## References
Expand Down
6 changes: 3 additions & 3 deletions demo/birth_death.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ end
sys = FSPSystem(rs)

# Parameters for our system
ps = [ :r1 => 10.0, :r2 => 1.0 ]
ps = [:r1 => 10.0, :r2 => 1.0]

# Initial values
u0 = zeros(50)
Expand All @@ -26,12 +26,12 @@ prob = ODEProblem(sys, u0, 10.0, ps)

##

sol = solve(prob, Vern7(), dense=false, save_everystep=false, abstol=1e-6)
sol = solve(prob, Vern7(), dense = false, save_everystep = false, abstol = 1e-6)

##

plt.suptitle(L"Distribution at $t = 10$")
plt.bar(0:49, sol.u[end], width=1);
plt.bar(0:49, sol.u[end], width = 1);
plt.xlabel("# of Molecules")
plt.ylabel("Probability")
plt.xlim(-0.5, 49.5)
Expand Down
8 changes: 4 additions & 4 deletions demo/telegraph.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ end
sys = FSPSystem(rs)

# Parameters for our system
ps = [ :r1 => 0.25, :r2 => 0.15, :r3 => 15.0, :r4 => 1.0 ]
ps = [:r1 => 0.25, :r2 => 0.15, :r3 => 15.0, :r4 => 1.0]

# Initial values
# Since G_on + G_off = const. we do not have to model the two
Expand All @@ -29,20 +29,20 @@ ps = [ :r1 => 0.25, :r2 => 0.15, :r3 => 15.0, :r4 => 1.0 ]
# 2
#
u0 = zeros(2, 50)
u0[1,1] = 1.0
u0[1, 1] = 1.0

##

prob = ODEProblem(sys, u0, 10.0, ps)

##

sol = solve(prob, Vern7(), dense=false, save_everystep=false, abstol=1e-6)
sol = solve(prob, Vern7(), dense = false, save_everystep = false, abstol = 1e-6)

##

plt.suptitle(L"Distribution at $t = 10$")
plt.bar(0:49, sol.u[end][1,:] + sol.u[end][2,:], width=1);
plt.bar(0:49, sol.u[end][1, :] + sol.u[end][2, :], width = 1);
plt.xlabel("# of mRNA")
plt.ylabel("Probability")
plt.xlim(-0.5, 49.5)
Expand Down
29 changes: 14 additions & 15 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@ using Documenter
using FiniteStateProjection
using SparseArrays

makedocs(sitename="FiniteStateProjection.jl",
modules=[FiniteStateProjection],
format = Documenter.HTML(prettyurls = false),
pages = [
"Home" => "index.md",
"Main API" => "mainapi.md",
"Index Handlers" => "indexhandlers.md",
"Matrix Conversions" => "matrix.md",
"Internal API" => "internal.md",
"Tips & Tricks" => "tips.md",
"Troubleshooting" => "troubleshoot.md",
"Examples" => "examples.md"
])

makedocs(sitename = "FiniteStateProjection.jl",
modules = [FiniteStateProjection],
format = Documenter.HTML(prettyurls = false),
pages = [
"Home" => "index.md",
"Main API" => "mainapi.md",
"Index Handlers" => "indexhandlers.md",
"Matrix Conversions" => "matrix.md",
"Internal API" => "internal.md",
"Tips & Tricks" => "tips.md",
"Troubleshooting" => "troubleshoot.md",
"Examples" => "examples.md"
])

deploydocs(repo = "github.com/kaandocal/FiniteStateProjection.jl.git", devbranch="main")
deploydocs(repo = "github.com/kaandocal/FiniteStateProjection.jl.git", devbranch = "main")
10 changes: 6 additions & 4 deletions docs/src/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ end
sys = FSPSystem(rn)

# Parameters for our system
ps = [ 10.0, 1.0 ]
ps = [10.0, 1.0]

# Initial distribution (over 1 species)
# Here we start with 0 copies of A
Expand All @@ -26,6 +26,7 @@ u0[1] = 1.0
prob = ODEProblem(sys, u0, (0, 10.0), ps)
sol = solve(prob, Vern7())
```

![Visualisation](assets/birth_death.png)

## Telegraph Model
Expand All @@ -35,7 +36,7 @@ Here we showcase the telegraph model, a simplistic description of mRNA transcrip
The most straightforward description of this system includes three species: two gene states, `G_on` and `G_off`, and mRNA `M`. The state space for this system is 3-dimensional. We know, however, that `G_on` and `G_off` never occur at the same time, indeed the conservation law `[G_on] + [G_off] = 1` allows us to express the state of the system in terms of `G_on` and `M` only. The state space of this reduced system is 2-dimensional.If we use an mRNA cutoff of 100, the state space for the original model has size ``2 \times 2 \times 100 = 400``, while the reduced state space has size ``2 \times 100 = 200``, a two-fold saving. Since the FSP get computationally more expensive for each species in a system, eliminating redundant species as above is recommended for improved performance.

!!! note

The class `ReducingIndexHandler`, which performed such reduction on the fly, has been deprecated and will be moved into [Catalyst.jl](https://github.com/SciML/Catalyst.jl).

```julia
Expand All @@ -52,14 +53,15 @@ end
sys = FSPSystem(rn)

# Parameters for our system
ps = [ 0.25, 0.15, 15.0, 1.0 ]
ps = [0.25, 0.15, 15.0, 1.0]

# Initial distribution (over two species)
# Here we start with 0 copies of G_on and M
u0 = zeros(2, 50)
u0[1,1] = 1.0
u0[1, 1] = 1.0

prob = ODEProblem(sys, u0, (0, 10.0), ps)
sol = solve(prob, Vern7())
```

![Visualisation](assets/telegraph.png)
15 changes: 8 additions & 7 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ CurrentModule=FiniteStateProjection

FiniteStateProjection.jl is a package that implements Finite State Projection algorithms for chemical reaction networks based on [Catalyst.jl](https://github.com/SciML/Catalyst.jl) and [ModelingToolkit](https://github.com/SciML/ModelingToolkit.jl). FiniteStateProjection.jl converts descriptions of reaction networks into `ODEProblem`s that can be used to compute approximate solutions of the Chemical Master Equation with packages such as [DifferentialEquations.jl](https://github.com/SciML/DifferentialEquations.jl).

The Chemical Master Equation allows us to compute the probability of a reaction system to be in a given state ``\vec n`` at a time ``t``. The state of a system with ``s`` species is described by a vector ``\vec n = (n_1, n_2, \ldots, n_s)`` of length ``s``, where ``n_1, n_2, \ldots`` are the abundances of the different species. The reaction system itself can be represented by an ``s``-dimensional array ``P[n_1,\ldots,n_s]`` that describes the probability of the system being in state ``(n_1,\ldots,n_s)``. Here ``P`` depends on time as the system evolves. Given initial conditions ``P(0)``, the time evolution of ``P`` is given by the Chemical Master Equation, which is a set of linear ODEs that can be solved numerically to predict the probability distribution of the system in the future.
The Chemical Master Equation allows us to compute the probability of a reaction system to be in a given state ``\vec n`` at a time ``t``. The state of a system with ``s`` species is described by a vector ``\vec n = (n_1, n_2, \ldots, n_s)`` of length ``s``, where ``n_1, n_2, \ldots`` are the abundances of the different species. The reaction system itself can be represented by an ``s``-dimensional array ``P[n_1,\ldots,n_s]`` that describes the probability of the system being in state ``(n_1,\ldots,n_s)``. Here ``P`` depends on time as the system evolves. Given initial conditions ``P(0)``, the time evolution of ``P`` is given by the Chemical Master Equation, which is a set of linear ODEs that can be solved numerically to predict the probability distribution of the system in the future.

Since the number of states for a system can be infinite, the Finite State Projection approximates the Chemical Master Equation by only considering a finite number of states, namely those which are most probable. There are various ways to truncate the state space, but the most common is to provide an upper limit for each species, that is, to require ``n_1 < M_1``, ``n_2 < M_2``, etc. for fixed thresholds ``M_1, M_2, \ldots``. The number of states considered will be ``M_1 \times M_2 \times \ldots \times M_s``, and ``P`` can be represented as an array with those dimensions. While truncating the CME allows us to solve the equations numerically, the amount of space required to store ``P`` increases quickly as more species are added.
Since the number of states for a system can be infinite, the Finite State Projection approximates the Chemical Master Equation by only considering a finite number of states, namely those which are most probable. There are various ways to truncate the state space, but the most common is to provide an upper limit for each species, that is, to require ``n_1 < M_1``, ``n_2 < M_2``, etc. for fixed thresholds ``M_1, M_2, \ldots``. The number of states considered will be ``M_1 \times M_2 \times \ldots \times M_s``, and ``P`` can be represented as an array with those dimensions. While truncating the CME allows us to solve the equations numerically, the amount of space required to store ``P`` increases quickly as more species are added.

FiniteStateProjection.jl works by converting a `ReactionSystem` into a function that computes the right-hand side of the (truncated) Chemical Master Equation:

Expand All @@ -19,11 +19,12 @@ FiniteStateProjection.jl works by converting a `ReactionSystem` into a function
This function is generated dynamically as an `ODEFunction` for use with DifferentialEquations.jl and specialised for each `ReactionSystem`. Users can use their preferred array types and provide additional features by overloading the functions in this package. Alternatively the matrix `A` can be constructed as a `SparseMatrixCSC`. FiniteStateProjection.jl supports both the time-dependent Chemical Master Equation and its steady-state version.

## Features
- Built on top of [Catalyst.jl](https://github.com/SciML/Catalyst.jl)
- FSP equations are generated as `ODEFunction`/`ODEProblem`s and can be solved with [DifferentialEquations.jl](https://github.com/SciML/DifferentialEquations.jl), with on-the-fly generation of targeted functions for improved performance
- The Chemical Master Equation can be generated as a `SparseMatrixCSC`
- Flexible API for user-defined array types via `IndexHandler`s

- Built on top of [Catalyst.jl](https://github.com/SciML/Catalyst.jl)
- FSP equations are generated as `ODEFunction`/`ODEProblem`s and can be solved with [DifferentialEquations.jl](https://github.com/SciML/DifferentialEquations.jl), with on-the-fly generation of targeted functions for improved performance
- The Chemical Master Equation can be generated as a `SparseMatrixCSC`
- Flexible API for user-defined array types via `IndexHandler`s

## Acknowledgments

Special thanks to [Xiamong Fu](https://github.com/palmtree2013), [Brian Munsky](https://www.engr.colostate.edu/~munsky/) and [Huy Vo](https://github.com/voduchuy) for their examples and suggestions and for contributing most of the content in the [Tips & Tricks](@ref tips) page!
Special thanks to [Xiamong Fu](https://github.com/palmtree2013), [Brian Munsky](https://www.engr.colostate.edu/%7Emunsky/) and [Huy Vo](https://github.com/voduchuy) for their examples and suggestions and for contributing most of the content in the [Tips & Tricks](@ref tips) page!
2 changes: 1 addition & 1 deletion docs/src/indexhandlers.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Index Handlers

The task of an index handler is to provide a mapping between the system state and the way it is stored in memory, usually as a multidimensional array. The standard approach is to represent the states of a system with ``s`` reactions as an ``s``-dimensional array and have the index ``(i_1, \ldots, i_s)`` correspond to the state ``(n_1 = i_1, \ldots, n_s = i_s)``. This is implemented by the class [`DefaultIndexHandler`](@ref), which accepts an offset argument to deal with Julia's 1-based indexing (so the Julia index $(1,\ldots,1)$ corresponds to the state with no molecules).
The task of an index handler is to provide a mapping between the system state and the way it is stored in memory, usually as a multidimensional array. The standard approach is to represent the states of a system with ``s`` reactions as an ``s``-dimensional array and have the index ``(i_1, \ldots, i_s)`` correspond to the state ``(n_1 = i_1, \ldots, n_s = i_s)``. This is implemented by the class [`DefaultIndexHandler`](@ref), which accepts an offset argument to deal with Julia's 1-based indexing (so the Julia index $(1,\ldots,1)$ corresponds to the state with no molecules).

See the [internal API](@ref index_handlers_internal) on how to define your own `IndexHandler` type.

Expand Down
18 changes: 12 additions & 6 deletions docs/src/internal.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Internal API

```@meta
CurrentModule = FiniteStateProjection
```
Expand All @@ -8,14 +9,16 @@ CurrentModule = FiniteStateProjection
### Index Handler Interface

User-defined index handlers should inherit from `AbstractIndexHandler` and implement the following methods:
- [`getsubstitutions`](@ref getsubstitutions)
- [`build_rhs_header`](@ref build_rhs_header)
- [`singleindices`](@ref singleindices)
- [`pairedindices`](@ref pairedindices)

- [`getsubstitutions`](@ref getsubstitutions)
- [`build_rhs_header`](@ref build_rhs_header)
- [`singleindices`](@ref singleindices)
- [`pairedindices`](@ref pairedindices)

For matrix conversions they should additionally implement:
- [`LinearIndices`](@ref Base.LinearIndices)
- [`vec`](@ref Base.vec)

- [`LinearIndices`](@ref Base.LinearIndices)
- [`vec`](@ref Base.vec)

```@docs
singleindices
Expand All @@ -26,11 +29,13 @@ LinearIndices
```

### Built-in implementations

```@docs
getsubstitutions(::DefaultIndexHandler, ::ReactionSystem)
```

## Function Building

```@docs
build_rhs
unpackparams
Expand All @@ -40,6 +45,7 @@ build_rhs_secondpass
```

## Steady-State Functions

```@docs
build_rhs_ss
build_rhs_singlepass_ss
Expand Down
5 changes: 3 additions & 2 deletions docs/src/mainapi.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Main API

```@meta
CurrentModule = FiniteStateProjection
```
Expand All @@ -13,8 +14,8 @@ FSPSystem

## Creating ODE systems

The following methods convert a reaction network into a system of ODEs representing the time-dependent FSP. This package provides a flexible way to represent the FSP in memory via index handlers, see [Index Handlers] for more information.
The following methods convert a reaction network into a system of ODEs representing the time-dependent FSP. This package provides a flexible way to represent the FSP in memory via index handlers, see [Index Handlers] for more information.

```@docs
Base.convert(::Type{ODEFunction}, ::FSPSystem)
DiffEqBase.ODEProblem(::FSPSystem, u0, tmax, p)
Expand Down
10 changes: 5 additions & 5 deletions docs/src/troubleshoot.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ If your are solving an SIR model with three species, ``S``, ``I`` and ``R``, you
```julia
# correct
u0 = zeros(101, 101, 101)
u0[100,2,1] = 1.0 # start with 1 infected and 99 susceptible individuals
u0[100, 2, 1] = 1.0 # start with 1 infected and 99 susceptible individuals

# incorrect
u0 = [99,1,0] # wrong type (Int) and shape (1D)
u0 = [99, 1, 0] # wrong type (Int) and shape (1D)
```

## Ensure your state space is big enough
Expand Down Expand Up @@ -40,8 +40,8 @@ This point might seem obvious, but errors in the rate functions, or an incorrect

```julia
rn = @reaction_network begin
σ * (N - I), I --> 2I
ρ, I --> 0
σ * (N - I), I --> 2I
ρ, I --> 0
end

sys_fsp = FSPSystem(rn)
Expand All @@ -54,7 +54,7 @@ u0 = zeros(30)
u0[2] = 1

# N is too small for the state space!
prob_fsp = ODEProblem(sys_fsp, u0, (0, 100.), [ 1., 1., 20 ])
prob_fsp = ODEProblem(sys_fsp, u0, (0, 100.0), [1.0, 1.0, 20])
```

## Ensure you are using the right solver
Expand Down
17 changes: 9 additions & 8 deletions src/build_rhs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ See also: [`build_rhs_header`](@ref), [`build_rhs`](@ref)
"""
function unpackparams(sys::FSPSystem, psym::Symbol)
param_names = Expr(:tuple, map(par -> par.name, Catalyst.parameters(sys.rs))...)
quote

quote
$(param_names) = $(psym)
end
end
Expand All @@ -26,7 +26,7 @@ just unpacks parameters from `p`.
See also: [`unpackparams`](@ref), [`build_rhs`](@ref)
"""
function build_rhs_header(sys::FSPSystem)
quote
quote
ps = p
$(unpackparams(sys, :ps))
end
Expand Down Expand Up @@ -79,7 +79,8 @@ function build_rhs_secondpass(sys::FSPSystem)

for (i, rf) in enumerate(sys.rfs)
ex = quote
for (idx_in, idx_out) in pairedindices($(sys.ih), u, $(CartesianIndex(S[:,i]...)))
for (idx_in, idx_out) in
pairedindices($(sys.ih), u, $(CartesianIndex(S[:, i]...)))
du[idx_out] += u[idx_in] * $(rf.body)
end
end
Expand All @@ -92,7 +93,7 @@ end

##

function build_rhs_ex(sys::FSPSystem; striplines::Bool=false)
function build_rhs_ex(sys::FSPSystem; striplines::Bool = false)
header = build_rhs_header(sys)
first_pass = build_rhs_firstpass(sys)
second_pass = build_rhs_secondpass(sys)
Expand All @@ -115,7 +116,7 @@ Builds the function `f(du,u,p,t)` that defines the right-hand side of the CME
for use with DifferentialEquations.jl.
"""
function build_rhs(sys::FSPSystem)
@RuntimeGeneratedFunction(build_rhs_ex(sys; striplines=false))
@RuntimeGeneratedFunction(build_rhs_ex(sys; striplines = false))
end

##
Expand All @@ -136,9 +137,9 @@ Base.convert(::Type{ODEFunction}, sys::FSPSystem) = ODEFunction{true}(build_rhs(
DiffEqBase.ODEProblem(sys::FSPSystem, u0, tmax[, p])

Return an `ODEProblem` for use in `DifferentialEquations`. This function implicitly
calls `convert(ODEFunction, sys)`. It is usually more efficient to create an `ODEFunction`
calls `convert(ODEFunction, sys)`. It is usually more efficient to create an `ODEFunction`
first and then use that to create `ODEProblem`s.
"""
function DiffEqBase.ODEProblem(sys::FSPSystem, u0, tint, pmap=NullParameters())
function DiffEqBase.ODEProblem(sys::FSPSystem, u0, tint, pmap = NullParameters())
ODEProblem(convert(ODEFunction, sys), u0, tint, pmap_to_p(sys, pmap))
end
Loading
Loading