From e853f0db22609670ebb85197f37ab15e1752969f Mon Sep 17 00:00:00 2001 From: Riccardo Foffi Date: Thu, 28 Dec 2023 01:43:46 +0100 Subject: [PATCH 1/6] update docs with Literate.jl --- docs/Project.toml | 16 ++- docs/make.jl | 78 +++++++++-- docs/src/api.md | 29 ++++- docs/src/examples/Chemotaxis/linear_ramp.md | 123 ++++++++++++++++++ docs/src/examples/Chemotaxis/xie_2D.md | 46 +++++++ .../Chemotaxis/xie_response-function.md | 90 +++++++++++++ docs/src/examples/RandomWalks/randomwalk1D.md | 79 +++++++++++ .../randomwalk2D_motilepatterns.md | 78 +++++++++++ docs/src/examples/RandomWalks/randomwalk3D.md | 37 ++++++ examples/Chemotaxis/linear_ramp.jl | 79 +++++++++-- examples/RandomWalks/randomwalk1D.jl | 76 +++++++++-- .../randomwalk2D_motilepatterns.jl | 69 +++++++--- examples/RandomWalks/randomwalk3D.jl | 22 ++-- 13 files changed, 754 insertions(+), 68 deletions(-) create mode 100644 docs/src/examples/Chemotaxis/linear_ramp.md create mode 100644 docs/src/examples/Chemotaxis/xie_2D.md create mode 100644 docs/src/examples/Chemotaxis/xie_response-function.md create mode 100644 docs/src/examples/RandomWalks/randomwalk1D.md create mode 100644 docs/src/examples/RandomWalks/randomwalk2D_motilepatterns.md create mode 100644 docs/src/examples/RandomWalks/randomwalk3D.md diff --git a/docs/Project.toml b/docs/Project.toml index de08706..c21c3f7 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,6 +1,20 @@ [deps] +Agents = "46ada45e-f475-11e8-01d0-f70cc89e6671" +AxisArrays = "39de3d68-74b9-583c-8d2d-e117c070f3a9" +CellListMap = "69e1c6dd-3888-40e6-b3c8-31ac5f578864" +DSP = "717857b8-e6f2-59f4-9121-6e50c889abd2" +DisplayAs = "0b91fe84-8a4c-11e9-3e1d-67c38462b6d6" +Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DocumenterTools = "35a29f4d-8980-5a13-9543-d66fff28ecb8" +GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" +OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Rotations = "6038ab10-8711-5258-84ad-4b1120ba62dc" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [compat] -Documenter = "0.27" +Documenter = "1" diff --git a/docs/make.jl b/docs/make.jl index cfe9415..779347a 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,20 +1,72 @@ -push!(LOAD_PATH, "../src/") -using Documenter, MicrobeAgents +cd(@__DIR__) +using MicrobeAgents +using Documenter +ENV["JULIA_DEBUG"] = "Documenter" +CI = get(ENV, "CI", nothing) == "true" || get(ENV, "GITHUB_TOKEN", nothing) !== nothing +import Literate +using Plots + +indir_base = joinpath(@__DIR__, "..", "examples") +toskip = ("Encounters", "Pathfinder", "Analysis") +sections = filter(s -> !(s in toskip), readdir(indir_base)) +outdir_base = joinpath(@__DIR__, "src", "examples") +indir = Dict( + section => joinpath(indir_base, section) + for section in sections +) +outdir = Dict( + section => joinpath(outdir_base, section) + for section in sections +) +rm(outdir_base; force=true, recursive=true) # clean up previous examples +mkpath(outdir_base) +for section in sections + mkpath(outdir[section]) + for file in readdir(indir[section]) + Literate.markdown(joinpath(indir[section], file), outdir[section]; credit=false) + end +end + + +# convert camelcase directory names to space-separated section names +function namify(s) + indices = findall(isuppercase, s) + if length(indices) <= 1 + return s + else + s1 = s[indices[1] : indices[2]-1] + s2 = lowercase.(s[indices[2] : end]) + return join((s1, namify(s2)), " ") + end +end + +pages = [ + "Home" => "index.md", + "Tutorial" => ["firststeps.md", "randomwalks.md", "chemotaxis.md"], + "Validation" => "validation.md", + "Examples" => [ + [namify(section) => [joinpath.("examples", section, readdir(outdir[section]))...] + for section in sections]... + ], + "API" => "api.md" +] makedocs( sitename = "MicrobeAgents.jl", + authors = "Riccardo Foffi", modules = [MicrobeAgents], - pages = [ - "Home" => "index.md", - "Tutorial" => ["firststeps.md", "randomwalks.md", "chemotaxis.md"], - "Validation" => "validation.md", - "API" => "api.md" - ], + pages = pages, + expandfirst = ["index.md"], format = Documenter.HTML( - prettyurls = get(ENV, "CI", nothing) == "true" - ) + prettyurls = CI, + ), + warnonly = true, ) -deploydocs(; - repo = "github.com/mastrof/MicrobeAgents.jl" -) \ No newline at end of file +if CI + deploydocs(; + repo = "github.com/mastrof/MicrobeAgents.jl.git", + target = "build", + push_preview = true + ) +end diff --git a/docs/src/api.md b/docs/src/api.md index c2bae34..55d6fc6 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -1,5 +1,32 @@ # API +## [Microbes](@id Microbes) +```@docs +AbstractMicrobe +Microbe +BrownBerg +Brumley +Celani +Xie +``` + +## [Motility](@id Motility) +```@docs +AbstractMotility +MotilityOneStep +MotilityTwoStep +RunTumble +RunReverse +RunReverseFlick +``` + +## [Utils](@id Utils) +```@docs +vectorize_adf_measurement +random_speed +random_velocity +``` + ## [Data analysis](@id Analysis) ```@docs msd @@ -10,4 +37,4 @@ detect_turns rundurations mean_runduration mean_turnrate -``` \ No newline at end of file +``` diff --git a/docs/src/examples/Chemotaxis/linear_ramp.md b/docs/src/examples/Chemotaxis/linear_ramp.md new file mode 100644 index 0000000..c734d04 --- /dev/null +++ b/docs/src/examples/Chemotaxis/linear_ramp.md @@ -0,0 +1,123 @@ +```@meta +EditURL = "../../../../examples/Chemotaxis/linear_ramp.jl" +``` + +# Linear concentration ramp + +In this example we will setup an in-silico version of a typical laboratory assay, +with chemotactic bacteria moving in a linear concentration ramp, i.e. a concentration +field of the form +```math +C(x) = C_0 + (C_1 - C_0)\dfrac{x}{L_x}, \qquad x \in [0,L_x]. +``` + +We will create a closed two-dimensional domain, mimicking a thin microfluidic chamber, +with a length of 3 mm along the `x` direction, and 1.5 mm along the `y` direction. +We will then setup the concentration field along the `x` direction and observe +chemotactic microbes as they drift towards the high-concentration region of the chamber. + +The first thing we have to do is define two functions for the `concentration_field` and the +`concentration_gradient`. They must take as arguments the position of a single microbe, +and the model (from which we can access other properties of the system). +Of course, for our convenience we can dispatch these functions on whatever arguments we want, +as long as they have a method whose signature matches the MicrobeAgents API. + +Importantly, the `concentration_field` must return a scalar, non-negative value. +Since the gradient is a vector quantity, the `concentration_gradient` should instead return +an iterable with length equal to the system dimensionality; a `SVector` is the recommended +choice, but `NTuple`s, `Vector`s, etc.. work just fine. + +All the parameters that we need to evaluate the concentration field and gradient, in our case +the two concentration values `C₀` and `C₁` and the chamber length `Lx`, should be extracted +from the `model`. + +````@example linear_ramp +using MicrobeAgents +using Plots + +@inline function concentration_field(pos, model) + C₀ = model.C₀ + C₁ = model.C₁ + Lx = first(spacesize(model)) + concentration_field(pos,Lx,C₀,C₁) +end +@inline concentration_field(pos,Lx,C₀,C₁) = C₀ + (C₁-C₀)*pos[1]/Lx + +@inline function concentration_gradient(pos, model) + C₀ = model.C₀ + C₁ = model.C₁ + Lx = first(spacesize(model)) + concentration_gradient(pos,Lx,C₀,C₁) +end +@inline concentration_gradient(pos,Lx,C₀,C₁) = SVector{length(pos)}(i==1 ? (C₁-C₀)/Lx : 0.0 for i in eachindex(pos)) +```` + +Now as usual we define the simulation domain and the integration timestep, but we also define +a `properties` dictionary, which we pass as a keyword argument to `StandardABM`. +This dictionary will contain all the information regarding our concentration field. + +Note that the `:C₀` and `:C₁` keys have been defined by us; we could have chosen different +names for them. +The `concentration_field` and `concentration_gradient` functions instead **must** be assigned +to the `:concentration_field` and `:concentration_gradient` keys respectively; this is required +by MicrobeAgents and assigning these functions to any other key will not produce the desired results. + +To observe chemotaxis, we must use a microbe type for which chemotactic behavior is implemented. +If we used the base `Microbe`, no matter what field we define, we would only observe a random walk +since no chemotactic behavior is implemented. +The most classic model of chemotaxis is implemented in the `BrownBerg` type; +we will not modify its parameters here and just stick to the default values. + +````@example linear_ramp +Lx, Ly = 3000, 1500 # domain size (μm) +periodic = false +space = ContinuousSpace((Lx,Ly); periodic) +Δt = 0.1 # timestep (s) + +# model setup +C₀, C₁ = 0.0, 20.0 # μM +properties = Dict( + :C₀ => C₀, + :C₁ => C₁, + :concentration_field => concentration_field, + :concentration_gradient => concentration_gradient +) +model = StandardABM(BrownBerg{2}, space, Δt; properties) +n = 100 # number of microbes +for i in 1:n + add_agent!(model) # add at random positions +end +model +```` + +Now that the model is created, we just run it as usual, collecting the position +of the microbes at each timestep. +The visualization is slightly more involved since we want to plot +the microbe trajectories on top of the concentration field shown as a heatmap, +but there is really no difference from what we have seen in the random walk examples. + +In the figure, we will see that all the microbes drift towards the right, +where the concentration of the attractant is higher. + +````@example linear_ramp +T = 120 # simulation time (s) +nsteps = round(Int, T/Δt) +adata = [position] +adf, mdf = run!(model, nsteps; adata) + +traj = vectorize_adf_measurement(adf, :position) +x = first.(traj) +y = last.(traj) + +ts = unique(adf.step) .* Δt +lw = eachindex(ts) ./ length(ts) .* 3 +xmesh = range(0,Lx,length=100) +ymesh = range(0,Ly,length=100) +xn = @view x[:,1:10] +yn = @view y[:,1:10] +c = concentration_field.(Iterators.product(xmesh,ymesh),Lx,C₀,C₁) +heatmap(xmesh, ymesh, c', cbar=false, ratio=1, axis=false, c=:bone) +plot!(xn, yn, lab=false, lw=lw, lc=(1:n)') +scatter!(xn[end,:], yn[end,:], lab=false, m=:c, mc=1:n, msw=0.5, ms=8) +```` + diff --git a/docs/src/examples/Chemotaxis/xie_2D.md b/docs/src/examples/Chemotaxis/xie_2D.md new file mode 100644 index 0000000..7e31cf4 --- /dev/null +++ b/docs/src/examples/Chemotaxis/xie_2D.md @@ -0,0 +1,46 @@ +```@meta +EditURL = "../../../../examples/Chemotaxis/xie_2D.jl" +``` + +````@example xie_2D +using MicrobeAgents +using Plots +using Random + +function concentration_field(pos, model) + C = model.C + σ = model.σ + p₀ = model.p₀ + concentration_field(pos, p₀, C, σ) +end +concentration_field(pos, p₀, C, σ) = C * exp(-sum(abs2.(pos.-p₀))/(2*σ^2)) + +timestep = 0.1 # s +extent = ntuple(_ -> 1000.0, 2) # μm +space = ContinuousSpace(extent; periodic=false) +p₀ = extent./2 # μm +C = 500.0 # μM +σ = 25.0 # μm +properties = Dict( + :concentration_field => concentration_field, + :C => C, + :σ => σ, + :p₀ => p₀, +) + +rng = MersenneTwister(12) +model = StandardABM(Xie{2}, space, timestep; rng, properties) +foreach(_ -> add_agent!(model; chemotactic_precision=6.0), 1:300) + +nsteps = 5000 +adata = [:pos] +adf, = run!(model, nsteps; adata) + +traj = vectorize_adf_measurement(adf, :pos) +plot( + first.(traj)[end-100:end,:], + last.(traj)[end-100:end,:], + lab=false, lims=(0,1000) +) +```` + diff --git a/docs/src/examples/Chemotaxis/xie_response-function.md b/docs/src/examples/Chemotaxis/xie_response-function.md new file mode 100644 index 0000000..5ffa716 --- /dev/null +++ b/docs/src/examples/Chemotaxis/xie_response-function.md @@ -0,0 +1,90 @@ +```@meta +EditURL = "../../../../examples/Chemotaxis/xie_response-function.jl" +``` + +````@example xie_response-function +using MicrobeAgents +using Plots + +θ(a,b) = a>b ? 1.0 : 0.0 +function concentration_field(pos, model) + C₀ = model.C₀ + C₁ = model.C₁ + t₁ = model.t₁ + t₂ = model.t₂ + dt = model.timestep + t = abmtime(model) * dt + concentration_field(t, C₀, C₁, t₁, t₂) +end +concentration_field(t,C₀,C₁,t₁,t₂) = C₀+C₁*θ(t,t₁)*(1-θ(t,t₂)) + +timestep = 0.1 # s +space = ContinuousSpace(ntuple(_ -> 500.0, 3)) # μm +C₀ = 0.01 # μM +C₁ = 5.0-C₀ # μM +T = 60.0 # s +t₁ = 20.0 # s +t₂ = 40.0 # s +properties = Dict( + :concentration_field => concentration_field, + :C₀ => C₀, + :C₁ => C₁, + :t₁ => t₁, + :t₂ => t₂, +) + +model = StandardABM(Xie{3}, space, timestep; properties) +add_agent!(model; turn_rate_forward=0, motility=RunReverseFlick(motile_state=MotileState(Forward))) +add_agent!(model; turn_rate_backward=0, motility=RunReverseFlick(motile_state=MotileState(Backward))) + +nsteps = round(Int, T/timestep) +β(a) = a.motility.state == Forward ? a.gain_forward : a.gain_backward +state(a::Xie) = max(1 + β(a)*a.state, 0) +adata = [state, :state_m, :state_z] +adf, = run!(model, nsteps; adata) + +S = vectorize_adf_measurement(adf, :state) +m = (vectorize_adf_measurement(adf, :state_m))[:,1] # take only fw +z = (vectorize_adf_measurement(adf, :state_z))[:,1] # take only fw +```` + +response vs time for fw and bw modes + +````@example xie_response-function +begin + _green = palette(:default)[3] + plot() + x = (0:timestep:T) .- t₁ + plot!( + x, S, + lw=1.5, lab=["Forward" "Backward"] + ) + plot!(ylims=(-0.1,4.5), ylab="Response", xlab="time (s)") + plot!(twinx(), + x, t -> concentration_field(t.+t₁,C₀,C₁,t₁,t₂), + ls=:dash, lw=1.5, lc=_green, lab=false, + tickfontcolor=_green, + ylab="C (μM)", guidefontcolor=_green + ) +end +```` + +methylation and dephosphorylation + +````@example xie_response-function +begin + x = (0:timestep:T) .- t₁ + τ_m = model[1].adaptation_time_m + τ_z = model[1].adaptation_time_z + M = m ./ τ_m + Z = z ./ τ_z + R = M .- Z + plot( + x, [M Z R], + lw=2, + lab=["m/τ_m" "z/τ_z" "m/τ_m - z/τ_z"], + xlab="time (s)" + ) +end +```` + diff --git a/docs/src/examples/RandomWalks/randomwalk1D.md b/docs/src/examples/RandomWalks/randomwalk1D.md new file mode 100644 index 0000000..eadbe3d --- /dev/null +++ b/docs/src/examples/RandomWalks/randomwalk1D.md @@ -0,0 +1,79 @@ +```@meta +EditURL = "../../../../examples/RandomWalks/randomwalk1D.jl" +``` + +# 1D Random walk + +Here we simulate a population of one-dimensional random walkers. + +First we shall set up the model: we need to define the microbe type, +the space and the integration timestep. + +For the microbe type, we will choose the base type `Microbe{1}`. + +For the space, we need to set the domain size and whether it is periodic or not. +We will use a periodic box with an extent of 1000 μm; +if unspecified, `ContinuousSpace` will default to `periodic=true`. + +For the integration timestep, we choose a value of 0.1 s. + +Remember that lengths are always assumed to be in units of microns, times in seconds. + +````@example randomwalk1D +using MicrobeAgents + +L = 1000 # space size in μm +space = ContinuousSpace((L,)) +dt = 0.1 # integration timestep in s +model = StandardABM(Microbe{1}, space, dt) +```` + +Now that the model is initialized, we will add 10 microbes. +If we don't provide any argument on creation, default values from the constructor +will be used, i.e., an isotropic `RunTumble` motility, with speed 30 μm/s, an +unbiased tumbling rate of 1 Hz... + +With the first (optional) argument to `add_agent!`, we can define the starting +position of the microbe. For convenience, we will initialize all of them +from position `(0,)`. + +````@example randomwalk1D +n = 10 # number of microbes to add +foreach(_ -> add_agent!((0,), model), 1:n) +```` + +We can now run the simulation. +We just need to define how many timesteps we want to simulate +and what kind of data we want to store during the simulation. +In this simulation, we only want to collect the microbe positions +at each timestep; we then set the `adata` vector (agent data) +to collect the position of the microbes. + +The `run!` function will return a dataframe for the agent data (`adf`) +and one for the model data (`mdf`) collected during the simulation. +Here we are only collecting agent data and no model data. + +````@example randomwalk1D +nsteps = 600 +adata = [position] +adf, _ = run!(model, nsteps; adata); +nothing #hide +```` + +We now want to visualize our results. +One last thing we need to is to "unfold" the trajectories of our microbes. +In fact, since we used a periodic domain, if we just plotted the trajectories +we would see them crossing between the two sides of the box, which is not what we want. + +With the unfolding, the trajectories are expanded as if they were simulated in an +infinite system. + +````@example randomwalk1D +traj = MicrobeAgents.unfold(vectorize_adf_measurement(adf,:position), L) +x = first.(traj) +t = axes(x,1) .* dt + +using Plots +plot(t, x, leg=false, xlab="time", ylab="displacement") +```` + diff --git a/docs/src/examples/RandomWalks/randomwalk2D_motilepatterns.md b/docs/src/examples/RandomWalks/randomwalk2D_motilepatterns.md new file mode 100644 index 0000000..ce8ea74 --- /dev/null +++ b/docs/src/examples/RandomWalks/randomwalk2D_motilepatterns.md @@ -0,0 +1,78 @@ +```@meta +EditURL = "../../../../examples/RandomWalks/randomwalk2D_motilepatterns.jl" +``` + +# 2D Random walk and motile patterns + +Here we will simulate two dimensional random walk with different motile patterns. + +As usual we start by setting up the model. + +````@example randomwalk2D_motilepatterns +using MicrobeAgents +using Distributions +using Plots + +L = 500 # space size in μm +space = ContinuousSpace((L,L)) # defaults to periodic +dt = 0.1 # integration timestep in s +model = StandardABM(Microbe{2}, space, dt) +```` + +We will now add microbes individually, choosing different properties for each. +The motile pattern can be customized through the `motility` keyword. + +The `RunTumble` motility consists of straight runs interspersed with isotropic +reorientations (tumbles). We can define the `speed` of follow a a `Normal` distribution +(from Distributions.jl) with mean 30 μm/s and standard deviation 6 μm/s. +This means that, after every tumble, the microbe will change its speed following +this distribution. +Further, we reduce the unbiased tumbling rate (`turn_rate`) of the microbe +from the default value of 1 Hz to 0.5 Hz. + +````@example randomwalk2D_motilepatterns +add_agent!(model; motility=RunTumble(speed=Normal(30,6)), turn_rate=0.5) +```` + +The `RunReverse` motility consists of alternating straight runs and 180-degree reversals. +Differently from the `RunTumble`, the `RunReverse` can be considered as a two-step motility +pattern and we can assign different properties to the "forward" and the "backward" state of motion. +If no properties are explicitly specified for the "backward" state, it will inherit those of +the "forward" state. +Here we just set the speed to the constant value of 55 μm/s. +Further, we set the `rotational_diffusivity` of the microbe to 0.2 rad²/s; in absence of +rotational diffusion, the run reverse motility is pathologically incapable of exploring space efficiently. + +````@example randomwalk2D_motilepatterns +add_agent!(model; motility=RunReverse(speed=[55]), rotational_diffusivity=0.2) +```` + +The `RunReverseFlick` motility consists of a straight run, a 180-degree reversal, then another +straight run followed by a 90-degree reorientation (the flick). +Like the `RunReverse`, this is a two-step motility pattern; indeed, we can imagine it as +a `RunReverse` where the reorientation in the "backward" motile state is 90 instead of 180 degrees. +We set the `speed_backward` to 6 μm/s, while the speed in the forward mode will keep its default +value (30 μm/s). We also set the rotational diffusivity to 0.1 rad²/s. + +````@example randomwalk2D_motilepatterns +add_agent!(model; motility=RunReverseFlick(speed_backward=[6]), rotational_diffusivity=0.1) +```` + +Now we can run (collecting the microbe positions at each timestep), unfold the trajectories, +and visualize them. + +````@example randomwalk2D_motilepatterns +nsteps = 600 +adata = [position] +adf, _ = run!(model, nsteps; adata) + +traj = MicrobeAgents.unfold(vectorize_adf_measurement(adf,:position), L) +x = first.(traj) +y = last.(traj) +t = axes(x,1) .* dt + +plot(x, y, xlab="x", ylab="y", ratio=1, + lab=["RunTumble" "RunReverse" "RunReverseFlick"] +) +```` + diff --git a/docs/src/examples/RandomWalks/randomwalk3D.md b/docs/src/examples/RandomWalks/randomwalk3D.md new file mode 100644 index 0000000..7c98f0b --- /dev/null +++ b/docs/src/examples/RandomWalks/randomwalk3D.md @@ -0,0 +1,37 @@ +```@meta +EditURL = "../../../../examples/RandomWalks/randomwalk3D.jl" +``` + +# 3D Random walk + +Without any significant difference, we can also simulate three-dimensional random walks. + +````@example randomwalk3D +using MicrobeAgents +using Distributions +using Plots + +L = 500 +space = ContinuousSpace((L,L,L)) +dt = 0.1 +model = StandardABM(Microbe{3}, space, dt) + +add_agent!(model; motility=RunReverse(speed=[55]), rotational_diffusivity=0.2) +add_agent!(model; motility=RunTumble(speed=Normal(30,6)), turn_rate=0.5) +add_agent!(model; motility=RunReverseFlick(speed_backward=[6]), rotational_diffusivity=0.1) + +nsteps = 600 +adata = [position] +adf, _ = run!(model, nsteps; adata) + +traj = MicrobeAgents.unfold(vectorize_adf_measurement(adf,:position), L) +x = first.(traj) +y = getindex.(traj, 2) +z = last.(traj) +t = axes(x,1) .* dt + +plot(x, y, z, xlab="x", ylab="y", zlab="z", lw=2, ratio=1, + lab=["RunReverse" "RunTumble" "RunReverseFlick"] +) +```` + diff --git a/examples/Chemotaxis/linear_ramp.jl b/examples/Chemotaxis/linear_ramp.jl index 7265a50..330f156 100644 --- a/examples/Chemotaxis/linear_ramp.jl +++ b/examples/Chemotaxis/linear_ramp.jl @@ -1,7 +1,37 @@ +# # Linear concentration ramp + +#= +In this example we will setup an in-silico version of a typical laboratory assay, +with chemotactic bacteria moving in a linear concentration ramp, i.e. a concentration +field of the form +```math +C(x) = C_0 + (C_1 - C_0)\dfrac{x}{L_x}, \qquad x \in [0,L_x]. +``` + +We will create a closed two-dimensional domain, mimicking a thin microfluidic chamber, +with a length of 3 mm along the `x` direction, and 1.5 mm along the `y` direction. +We will then setup the concentration field along the `x` direction and observe +chemotactic microbes as they drift towards the high-concentration region of the chamber. + +The first thing we have to do is define two functions for the `concentration_field` and the +`concentration_gradient`. They must take as arguments the position of a single microbe, +and the model (from which we can access other properties of the system). +Of course, for our convenience we can dispatch these functions on whatever arguments we want, +as long as they have a method whose signature matches the MicrobeAgents API. + +Importantly, the `concentration_field` must return a scalar, non-negative value. +Since the gradient is a vector quantity, the `concentration_gradient` should instead return +an iterable with length equal to the system dimensionality; a `SVector` is the recommended +choice, but `NTuple`s, `Vector`s, etc.. work just fine. + +All the parameters that we need to evaluate the concentration field and gradient, in our case +the two concentration values `C₀` and `C₁` and the chamber length `Lx`, should be extracted +from the `model`. +=# + using MicrobeAgents using Plots -## define concentration field @inline function concentration_field(pos, model) C₀ = model.C₀ C₁ = model.C₁ @@ -16,16 +46,29 @@ end Lx = first(spacesize(model)) concentration_gradient(pos,Lx,C₀,C₁) end -@inline concentration_gradient(pos,Lx,C₀,C₁) = ntuple(i->i==1 ? (C₁-C₀)/Lx : 0.0, length(pos)) +@inline concentration_gradient(pos,Lx,C₀,C₁) = SVector{length(pos)}(i==1 ? (C₁-C₀)/Lx : 0.0 for i in eachindex(pos)) + +#= +Now as usual we define the simulation domain and the integration timestep, but we also define +a `properties` dictionary, which we pass as a keyword argument to `StandardABM`. +This dictionary will contain all the information regarding our concentration field. -## simulation parameters +Note that the `:C₀` and `:C₁` keys have been defined by us; we could have chosen different +names for them. +The `concentration_field` and `concentration_gradient` functions instead **must** be assigned +to the `:concentration_field` and `:concentration_gradient` keys respectively; this is required +by MicrobeAgents and assigning these functions to any other key will not produce the desired results. + +To observe chemotaxis, we must use a microbe type for which chemotactic behavior is implemented. +If we used the base `Microbe`, no matter what field we define, we would only observe a random walk +since no chemotactic behavior is implemented. +The most classic model of chemotaxis is implemented in the `BrownBerg` type; +we will not modify its parameters here and just stick to the default values. +=# Lx, Ly = 3000, 1500 # domain size (μm) periodic = false space = ContinuousSpace((Lx,Ly); periodic) Δt = 0.1 # timestep (s) -T = 120 # simulation time (s) -nsteps = round(Int, T/Δt) -n = 100 ## model setup C₀, C₁ = 0.0, 20.0 # μM @@ -36,20 +79,31 @@ properties = Dict( :concentration_gradient => concentration_gradient ) model = StandardABM(BrownBerg{2}, space, Δt; properties) +n = 100 # number of microbes for i in 1:n - add_agent!(model) + add_agent!(model) # add at random positions end +model -## run -adata = [:pos, :vel] +#= +Now that the model is created, we just run it as usual, collecting the position +of the microbes at each timestep. +The visualization is slightly more involved since we want to plot +the microbe trajectories on top of the concentration field shown as a heatmap, +but there is really no difference from what we have seen in the random walk examples. + +In the figure, we will see that all the microbes drift towards the right, +where the concentration of the attractant is higher. +=# +T = 120 # simulation time (s) +nsteps = round(Int, T/Δt) +adata = [position] adf, mdf = run!(model, nsteps; adata) -## postprocessing -traj = vectorize_adf_measurement(adf, :pos) +traj = vectorize_adf_measurement(adf, :position) x = first.(traj) y = last.(traj) -## plotting ts = unique(adf.step) .* Δt lw = eachindex(ts) ./ length(ts) .* 3 xmesh = range(0,Lx,length=100) @@ -60,4 +114,3 @@ c = concentration_field.(Iterators.product(xmesh,ymesh),Lx,C₀,C₁) heatmap(xmesh, ymesh, c', cbar=false, ratio=1, axis=false, c=:bone) plot!(xn, yn, lab=false, lw=lw, lc=(1:n)') scatter!(xn[end,:], yn[end,:], lab=false, m=:c, mc=1:n, msw=0.5, ms=8) -plot!(size=(600,300), margin=-30Plots.px) diff --git a/examples/RandomWalks/randomwalk1D.jl b/examples/RandomWalks/randomwalk1D.jl index 39bef03..e9facfc 100644 --- a/examples/RandomWalks/randomwalk1D.jl +++ b/examples/RandomWalks/randomwalk1D.jl @@ -1,25 +1,73 @@ +# # 1D Random walk + +#= +Here we simulate a population of one-dimensional random walkers. + +First we shall set up the model: we need to define the microbe type, +the space and the integration timestep. + +For the microbe type, we will choose the base type `Microbe{1}`. + +For the space, we need to set the domain size and whether it is periodic or not. +We will use a periodic box with an extent of 1000 μm; +if unspecified, `ContinuousSpace` will default to `periodic=true`. + +For the integration timestep, we choose a value of 0.1 s. + +Remember that lengths are always assumed to be in units of microns, times in seconds. +=# + using MicrobeAgents -using Plots -## model parameters -L = 1000 +L = 1000 # space size in μm space = ContinuousSpace((L,)) -dt = 0.1 -n = 10 -nsteps = 600 - -## abm setup +dt = 0.1 # integration timestep in s model = StandardABM(Microbe{1}, space, dt) + +#= +Now that the model is initialized, we will add 10 microbes. +If we don't provide any argument on creation, default values from the constructor +will be used, i.e., an isotropic `RunTumble` motility, with speed 30 μm/s, an +unbiased tumbling rate of 1 Hz... + +With the first (optional) argument to `add_agent!`, we can define the starting +position of the microbe. For convenience, we will initialize all of them +from position `(0,)`. +=# + +n = 10 # number of microbes to add foreach(_ -> add_agent!((0,), model), 1:n) -## simulation -adata = [:pos] -adf, _ = run!(model, nsteps; adata) +#= +We can now run the simulation. +We just need to define how many timesteps we want to simulate +and what kind of data we want to store during the simulation. +In this simulation, we only want to collect the microbe positions +at each timestep; we then set the `adata` vector (agent data) +to collect the position of the microbes. -## postprocessing -traj = MicrobeAgents.unfold(vectorize_adf_measurement(adf,:pos), L) +The `run!` function will return a dataframe for the agent data (`adf`) +and one for the model data (`mdf`) collected during the simulation. +Here we are only collecting agent data and no model data. +=# + +nsteps = 600 +adata = [position] +adf, _ = run!(model, nsteps; adata); + +#= +We now want to visualize our results. +One last thing we need to is to "unfold" the trajectories of our microbes. +In fact, since we used a periodic domain, if we just plotted the trajectories +we would see them crossing between the two sides of the box, which is not what we want. + +With the unfolding, the trajectories are expanded as if they were simulated in an +infinite system. +=# + +traj = MicrobeAgents.unfold(vectorize_adf_measurement(adf,:position), L) x = first.(traj) t = axes(x,1) .* dt -## visualization +using Plots plot(t, x, leg=false, xlab="time", ylab="displacement") diff --git a/examples/RandomWalks/randomwalk2D_motilepatterns.jl b/examples/RandomWalks/randomwalk2D_motilepatterns.jl index d5e1d92..6b954ba 100644 --- a/examples/RandomWalks/randomwalk2D_motilepatterns.jl +++ b/examples/RandomWalks/randomwalk2D_motilepatterns.jl @@ -1,31 +1,70 @@ +# # 2D Random walk and motile patterns + +#= +Here we will simulate two dimensional random walk with different motile patterns. + +As usual we start by setting up the model. +=# + using MicrobeAgents using Distributions using Plots -## model parameters -L = 500 -space = ContinuousSpace((L,L)) -dt = 0.1 -nsteps = 600 - -## abm setup +L = 500 # space size in μm +space = ContinuousSpace((L,L)) # defaults to periodic +dt = 0.1 # integration timestep in s model = StandardABM(Microbe{2}, space, dt) -# add bacteria with different motile properties -add_agent!(model; motility=RunReverse(speed=[55]), rotational_diffusivity=0.2) + +#= +We will now add microbes individually, choosing different properties for each. +The motile pattern can be customized through the `motility` keyword. + +The `RunTumble` motility consists of straight runs interspersed with isotropic +reorientations (tumbles). We can define the `speed` of follow a a `Normal` distribution +(from Distributions.jl) with mean 30 μm/s and standard deviation 6 μm/s. +This means that, after every tumble, the microbe will change its speed following +this distribution. +Further, we reduce the unbiased tumbling rate (`turn_rate`) of the microbe +from the default value of 1 Hz to 0.5 Hz. +=# add_agent!(model; motility=RunTumble(speed=Normal(30,6)), turn_rate=0.5) + +#= +The `RunReverse` motility consists of alternating straight runs and 180-degree reversals. +Differently from the `RunTumble`, the `RunReverse` can be considered as a two-step motility +pattern and we can assign different properties to the "forward" and the "backward" state of motion. +If no properties are explicitly specified for the "backward" state, it will inherit those of +the "forward" state. +Here we just set the speed to the constant value of 55 μm/s. +Further, we set the `rotational_diffusivity` of the microbe to 0.2 rad²/s; in absence of +rotational diffusion, the run reverse motility is pathologically incapable of exploring space efficiently. +=# +add_agent!(model; motility=RunReverse(speed=[55]), rotational_diffusivity=0.2) + +#= +The `RunReverseFlick` motility consists of a straight run, a 180-degree reversal, then another +straight run followed by a 90-degree reorientation (the flick). +Like the `RunReverse`, this is a two-step motility pattern; indeed, we can imagine it as +a `RunReverse` where the reorientation in the "backward" motile state is 90 instead of 180 degrees. +We set the `speed_backward` to 6 μm/s, while the speed in the forward mode will keep its default +value (30 μm/s). We also set the rotational diffusivity to 0.1 rad²/s. +=# add_agent!(model; motility=RunReverseFlick(speed_backward=[6]), rotational_diffusivity=0.1) -## simulation -adata = [:pos] +#= +Now we can run (collecting the microbe positions at each timestep), unfold the trajectories, +and visualize them. +=# + +nsteps = 600 +adata = [position] adf, _ = run!(model, nsteps; adata) -## postprocessing -traj = MicrobeAgents.unfold(vectorize_adf_measurement(adf,:pos), L) +traj = MicrobeAgents.unfold(vectorize_adf_measurement(adf,:position), L) x = first.(traj) y = last.(traj) t = axes(x,1) .* dt -## visualization plot(x, y, xlab="x", ylab="y", ratio=1, - lab=["RunReverse" "RunTumble" "RunReverseFlick"] + lab=["RunTumble" "RunReverse" "RunReverseFlick"] ) diff --git a/examples/RandomWalks/randomwalk3D.jl b/examples/RandomWalks/randomwalk3D.jl index d9fc491..5b035d5 100644 --- a/examples/RandomWalks/randomwalk3D.jl +++ b/examples/RandomWalks/randomwalk3D.jl @@ -1,32 +1,32 @@ +# # 3D Random walk + +#= +Without any significant difference, we can also simulate three-dimensional random walks. +=# + using MicrobeAgents using Distributions using Plots -## model parameters L = 500 space = ContinuousSpace((L,L,L)) dt = 0.1 -nsteps = 600 - -## abm setup model = StandardABM(Microbe{3}, space, dt) -# add bacteria with different motile properties + add_agent!(model; motility=RunReverse(speed=[55]), rotational_diffusivity=0.2) add_agent!(model; motility=RunTumble(speed=Normal(30,6)), turn_rate=0.5) add_agent!(model; motility=RunReverseFlick(speed_backward=[6]), rotational_diffusivity=0.1) -## simulation -adata = [:pos] +nsteps = 600 +adata = [position] adf, _ = run!(model, nsteps; adata) -## postprocessing -traj = MicrobeAgents.unfold(vectorize_adf_measurement(adf,:pos), L) +traj = MicrobeAgents.unfold(vectorize_adf_measurement(adf,:position), L) x = first.(traj) -y = map(s -> s[2], traj) +y = getindex.(traj, 2) z = last.(traj) t = axes(x,1) .* dt -## visualization plot(x, y, z, xlab="x", ylab="y", zlab="z", lw=2, ratio=1, lab=["RunReverse" "RunTumble" "RunReverseFlick"] ) From 04965bb28d63b215b6d7e61aedb2c623e2ffa67c Mon Sep 17 00:00:00 2001 From: Riccardo Foffi Date: Thu, 28 Dec 2023 14:44:32 +0100 Subject: [PATCH 2/6] reorganize sections --- docs/make.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 779347a..79ab6a8 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -7,8 +7,7 @@ import Literate using Plots indir_base = joinpath(@__DIR__, "..", "examples") -toskip = ("Encounters", "Pathfinder", "Analysis") -sections = filter(s -> !(s in toskip), readdir(indir_base)) +sections = ("RandomWalks", "Chemotaxis") outdir_base = joinpath(@__DIR__, "src", "examples") indir = Dict( section => joinpath(indir_base, section) From 4fb582a8deed7decfb78c7aff9dccad749592f75 Mon Sep 17 00:00:00 2001 From: Riccardo Foffi Date: Thu, 28 Dec 2023 18:42:03 +0100 Subject: [PATCH 3/6] update examples --- examples/Chemotaxis/celani_gauss2D.jl | 51 +++++++ examples/Chemotaxis/response_functions.jl | 137 +++++++++++++++++++ examples/Chemotaxis/xie_2D.jl | 39 ------ examples/Chemotaxis/xie_response-function.jl | 109 ++++++++------- 4 files changed, 247 insertions(+), 89 deletions(-) create mode 100644 examples/Chemotaxis/celani_gauss2D.jl create mode 100644 examples/Chemotaxis/response_functions.jl delete mode 100644 examples/Chemotaxis/xie_2D.jl diff --git a/examples/Chemotaxis/celani_gauss2D.jl b/examples/Chemotaxis/celani_gauss2D.jl new file mode 100644 index 0000000..3d0f020 --- /dev/null +++ b/examples/Chemotaxis/celani_gauss2D.jl @@ -0,0 +1,51 @@ +# # Noisy chemotaxis towards Gaussian source + +#= +In this example we set up a static Gaussian source and observe the chemotactic behavior +of the `Celani` model, in the presence of sensing noise (via the `chemotactic_precision`). +Playing with the `chemotactic_precision`, it can be seen that the clustering of bacteria +at the source becomes stronger with decreasing noise (decreasing chemotactic precision). +=# + +using MicrobeAgents +using Plots + +function concentration_field(pos, model) + C = model.C + σ = model.σ + p₀ = model.p₀ + concentration_field(pos, p₀, C, σ) +end +concentration_field(pos, p₀, C, σ) = C * exp(-sum(abs2.(pos.-p₀))/(2*σ^2)) + +timestep = 0.1 # s +extent = ntuple(_ -> 1000.0, 2) # μm +space = ContinuousSpace(extent; periodic=false) +p₀ = extent./2 # μm +C = 1.0 # μM +σ = 100.0 # μm +properties = Dict( + :concentration_field => concentration_field, + :C => C, + :σ => σ, + :p₀ => p₀, +) + +model = StandardABM(Celani{2}, space, timestep; properties) +foreach(_ -> add_agent!(model; chemotactic_precision=6.0), 1:300) + +nsteps = 5000 +adata = [position] +adf, = run!(model, nsteps; adata) + +traj = vectorize_adf_measurement(adf, :position) +xmesh = range(0, first(spacesize(model)); length=100) +ymesh = range(0, last(spacesize(model)); length=100) +c = [concentration_field(p, p₀, C, σ) for p in Iterators.product(xmesh, ymesh)] +heatmap(xmesh, ymesh, c', cbar=false, ratio=1, c=:bone, axis=false) +x = getindex.(traj,1)[end-100:4:end, :] +y = getindex.(traj,2)[end-100:4:end, :] +a = axes(x,1) ./ size(x,1) +plot!(x, y, + lab=false, lims=(0,1000), lw=1, alpha=a +) diff --git a/examples/Chemotaxis/response_functions.jl b/examples/Chemotaxis/response_functions.jl new file mode 100644 index 0000000..bebe6ef --- /dev/null +++ b/examples/Chemotaxis/response_functions.jl @@ -0,0 +1,137 @@ +# # Response function (Xie) + +#= +In this example we will probe the response function implemented +in the `Xie` model of chemotaxis. +The impulse response function is the "output" of the bacterial chemotaxis +pathway when presented with an input signal`. + +To do this, we will emulate another classical laboratory assay, where the +bacterium is tethered to a wall, and it is exposed to a temporal change +in the concentration of a chemoattractant. +The response to the stimulus can be measured by observing modulations +in the instantaneous tumbling rate. +For each of the implemented microbe types, MicrobeAgents provides a +`tumblebias` function which returns the instantaneous bias +in the tumbling rate, evaluated from the internal state of the microbe. +Monitoring the time evolution of the tumble bias under teporal stimuli +then allows us to access the response function of the microbe. + +In the `Xie` model, chemotaxis is implemented by direct samplings of the +`concentration_field`, thus we don't need to explicitly define neither +a `concentration_gradient` nor a `concentration_time_derivative`. +We will represent our temporal stimuli in the form of square waves which +instantaneously switch from a baseline value `C₀` to a peak value `C₁+C₀` +homogeneously over space. +The excitation will occur at a time `t₁` and go back to baseline levels +at a time `t₂`. +=# +using MicrobeAgents +using Plots + +θ(a,b) = a>b ? 1.0 : 0.0 # heaviside theta function +function concentration_field(pos, model) + C₀ = model.C₀ + C₁ = model.C₁ + t₁ = model.t₁ + t₂ = model.t₂ + dt = model.timestep + t = abmtime(model) * dt + ## notice the time dependence! + concentration_field(t, C₀, C₁, t₁, t₂) +end +concentration_field(t,C₀,C₁,t₁,t₂) = C₀+C₁*θ(t,t₁)*(1-θ(t,t₂)) + +space = ContinuousSpace(ntuple(_ -> 500.0, 3)) # μm +C₀ = 0.01 # μM +C₁ = 5.0-C₀ # μM +T = 60.0 # s +t₁ = 20.0 # s +t₂ = 40.0 # s +properties = Dict( + :concentration_field => concentration_field, + :C₀ => C₀, + :C₁ => C₁, + :t₁ => t₁, + :t₂ => t₂, +) + +dt = 0.1 # s +model = StandardABM(Xie{3}, space, dt; properties) + +#= +A peculiarity of the `Xie` model is that the chemotactic properties of the +microbe differ between the forward and backward motile states, so we can +probe the response function in both the forward and backward motile state +by initializing two distinct microbes in the two states. +To keep the microbes in these motile states for the entire experiment duration, +we suppress their tumbles, and (just for total consistency with experiments) +we also set their speed to 0. +=# +add_agent!(model; turn_rate_forward=0, + motility=RunReverseFlick(motile_state=MotileState(Forward), speed=[0]) +) +add_agent!(model; turn_rate_backward=0, + motility=RunReverseFlick(motile_state=MotileState(Backward), speed=[0]) +) + +#= +In addition to the `tumblebias`, we will also monitor two other quantities +`state_m` and `state_z` which are internal variables of the `Xie` model +which represent the methylation and dephosphorylation processes which +together control the chemotactic response of the bacterium. +=# + +nsteps = round(Int, T/dt) +#state(a::Xie) = max(tumblebias(a)*a.state, 0) +adata = [tumblebias, :state_m, :state_z] +adf, = run!(model, nsteps; adata) + +S = vectorize_adf_measurement(adf, :tumblebias) +m = (vectorize_adf_measurement(adf, :state_m))[:,1] # take only fw +z = (vectorize_adf_measurement(adf, :state_z))[:,1] # take only fw + +#= +We first look at the response function in the forward and backward +motile state: when the concentration increases we have a sharp negative +response (the tumble bias decreases), then the bacterium adapts to the new +concentration level, and when it drops back to the basal level we observe +a sharp positive response (the tumble bisa increases) before adapting +again to the new concentration level. +=# +_green = palette(:default)[3] +plot() +x = (0:dt:T) .- t₁ +plot!( + x, S, + lw=1.5, lab=["Forward" "Backward"] +) +plot!(ylims=(-0.1,4.5), ylab="Response", xlab="time (s)") +plot!(twinx(), + x, t -> concentration_field(t.+t₁,C₀,C₁,t₁,t₂), + ls=:dash, lw=1.5, lc=_green, lab=false, + tickfontcolor=_green, + ylab="C (μM)", guidefontcolor=_green +) + +#= +By analysing the methylation and dephosphorylation processes, we can +understand how the chemotactic response arises. +First, when the concentration increases, both `m` and `z` increase and converge +to a new steady-state value, but since they respond on different timescales, +the response (defined by the difference between these two quantities), shows +a sharp decrease followed by a slower relaxation. +The same occurs for the negative stimulus. +=# +x = (0:dt:T) .- t₁ +τ_m = model[1].adaptation_time_m +τ_z = model[1].adaptation_time_z +M = m ./ τ_m +Z = z ./ τ_z +R = M .- Z +plot( + x, [M Z R], + lw=2, + lab=["m/τ_m" "z/τ_z" "m/τ_m - z/τ_z"], + xlab="time (s)" +) diff --git a/examples/Chemotaxis/xie_2D.jl b/examples/Chemotaxis/xie_2D.jl deleted file mode 100644 index 3cc80c8..0000000 --- a/examples/Chemotaxis/xie_2D.jl +++ /dev/null @@ -1,39 +0,0 @@ -using MicrobeAgents -using Plots -using Random - -function concentration_field(pos, model) - C = model.C - σ = model.σ - p₀ = model.p₀ - concentration_field(pos, p₀, C, σ) -end -concentration_field(pos, p₀, C, σ) = C * exp(-sum(abs2.(pos.-p₀))/(2*σ^2)) - -timestep = 0.1 # s -extent = ntuple(_ -> 1000.0, 2) # μm -space = ContinuousSpace(extent; periodic=false) -p₀ = extent./2 # μm -C = 500.0 # μM -σ = 25.0 # μm -properties = Dict( - :concentration_field => concentration_field, - :C => C, - :σ => σ, - :p₀ => p₀, -) - -rng = MersenneTwister(12) -model = StandardABM(Xie{2}, space, timestep; rng, properties) -foreach(_ -> add_agent!(model; chemotactic_precision=6.0), 1:300) - -nsteps = 5000 -adata = [:pos] -adf, = run!(model, nsteps; adata) - -traj = vectorize_adf_measurement(adf, :pos) -plot( - first.(traj)[end-100:end,:], - last.(traj)[end-100:end,:], - lab=false, lims=(0,1000) -) diff --git a/examples/Chemotaxis/xie_response-function.jl b/examples/Chemotaxis/xie_response-function.jl index ae1cdf7..50b5e9e 100644 --- a/examples/Chemotaxis/xie_response-function.jl +++ b/examples/Chemotaxis/xie_response-function.jl @@ -1,7 +1,18 @@ +# # Comparison of chemotactic response functions + +#= +Here we will compare the chemotactic response function of the `Celani` +and `BrownBerg` model to an impulse stimulus of chemoattractant. + +While `Celani` only needs the `concentration_field` to determine the +chemotactic response, `BrownBerg` also needs the `concentration_time_derivative` +to be defined explicitly (also the `concentration_gradient` but it's +not relevant in this specific study). +=# using MicrobeAgents using Plots -θ(a,b) = a>b ? 1.0 : 0.0 +θ(a,b) = a>b ? 1.0 : 0.0 # Heaviside theta function function concentration_field(pos, model) C₀ = model.C₀ C₁ = model.C₁ @@ -13,65 +24,63 @@ function concentration_field(pos, model) end concentration_field(t,C₀,C₁,t₁,t₂) = C₀+C₁*θ(t,t₁)*(1-θ(t,t₂)) -timestep = 0.1 # s +δ(t,dt) = 0 <= t <= dt ? 1.0/dt : 0.0 # discrete approximation to Dirac delta +function concentration_time_derivative(pos, model) + C₀ = model.C₀ + C₁ = model.C₁ + t₁ = model.t₁ + t₂ = model.t₂ + dt = model.timestep + t = abmtime(model) * dt + concentration_time_derivative(t, C₀, C₁, t₁, t₂, dt) +end +function concentration_time_derivative(t, C₀, C₁, t₁, t₂, dt) + C₁*(δ(t-t₁, dt) - δ(t-t₂, dt)) +end + space = ContinuousSpace(ntuple(_ -> 500.0, 3)) # μm -C₀ = 0.01 # μM -C₁ = 5.0-C₀ # μM -T = 60.0 # s -t₁ = 20.0 # s -t₂ = 40.0 # s +C₀ = 1.0 # μM +C₁ = 2.0-C₀ # μM +T = 50.0 # s +dt = 0.1 # s +t₁ = 10.0 # s +t₂ = 30.0 # s properties = Dict( :concentration_field => concentration_field, + :concentration_time_derivative => concentration_time_derivative, :C₀ => C₀, :C₁ => C₁, :t₁ => t₁, :t₂ => t₂, ) -model = StandardABM(Xie{3}, space, timestep; properties) -add_agent!(model; turn_rate_forward=0, motility=RunReverseFlick(motile_state=MotileState(Forward))) -add_agent!(model; turn_rate_backward=0, motility=RunReverseFlick(motile_state=MotileState(Backward))) +model = StandardABM(Union{BrownBerg{3},Celani{3}}, space, dt; properties) -nsteps = round(Int, T/timestep) -β(a) = a.motility.state == Forward ? a.gain_forward : a.gain_backward -state(a::Xie) = max(1 + β(a)*a.state, 0) -adata = [state, :state_m, :state_z] +add_agent!(BrownBerg{3}, model; turn_rate=0, motility=RunTumble(speed=[0]), + memory=1, +) +add_agent!(Celani{3}, model; turn_rate=0, motility=RunTumble(speed=[0]), gain=4) +add_agent!(Celani{3}, model; turn_rate=0, motility=RunTumble(speed=[0]), gain=4, + chemotactic_precision=50.0 +) + +nsteps = round(Int, T/dt) +adata = [tumblebias] adf, = run!(model, nsteps; adata) -S = vectorize_adf_measurement(adf, :state) -m = (vectorize_adf_measurement(adf, :state_m))[:,1] # take only fw -z = (vectorize_adf_measurement(adf, :state_z))[:,1] # take only fw +S = vectorize_adf_measurement(adf, :tumblebias) -# response vs time for fw and bw modes -begin - _green = palette(:default)[3] - plot() - x = (0:timestep:T) .- t₁ - plot!( - x, S, - lw=1.5, lab=["Forward" "Backward"] - ) - plot!(ylims=(-0.1,4.5), ylab="Response", xlab="time (s)") - plot!(twinx(), - x, t -> concentration_field(t.+t₁,C₀,C₁,t₁,t₂), - ls=:dash, lw=1.5, lc=_green, lab=false, - tickfontcolor=_green, - ylab="C (μM)", guidefontcolor=_green - ) -end - -# methylation and dephosphorylation -begin - x = (0:timestep:T) .- t₁ - τ_m = model[1].adaptation_time_m - τ_z = model[1].adaptation_time_z - M = m ./ τ_m - Z = z ./ τ_z - R = M .- Z - plot( - x, [M Z R], - lw=2, - lab=["m/τ_m" "z/τ_z" "m/τ_m - z/τ_z"], - xlab="time (s)" - ) -end +_pink = palette(:default)[4] +plot() +x = (0:dt:T) .- t₁ +plot!( + x, S, + lw=1.5, lab=["BrownBerg" "Celani" "Celani + Noise"] +) +plot!(ylims=(-0.1,2.1), ylab="Response", xlab="time (s)") +plot!(twinx(), + x, t -> concentration_field(t.+t₁,C₀,C₁,t₁,t₂), + ls=:dash, lw=1.5, lc=_pink, lab=false, + tickfontcolor=_pink, + ylab="C (μM)", guidefontcolor=_pink +) From 9976ae7faa7da899eab06dc577e1cf4ac4b1f9ab Mon Sep 17 00:00:00 2001 From: Riccardo Foffi Date: Thu, 28 Dec 2023 18:43:07 +0100 Subject: [PATCH 4/6] update docs --- docs/make.jl | 4 +- .../src/examples/Chemotaxis/celani_gauss2D.md | 56 +++++++ .../examples/Chemotaxis/response_functions.md | 146 ++++++++++++++++++ docs/src/examples/Chemotaxis/xie_2D.md | 46 ------ .../Chemotaxis/xie_response-function.md | 114 +++++++------- docs/src/firststeps.md | 119 +++++++------- 6 files changed, 318 insertions(+), 167 deletions(-) create mode 100644 docs/src/examples/Chemotaxis/celani_gauss2D.md create mode 100644 docs/src/examples/Chemotaxis/response_functions.md delete mode 100644 docs/src/examples/Chemotaxis/xie_2D.md diff --git a/docs/make.jl b/docs/make.jl index 79ab6a8..a019d84 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -41,12 +41,12 @@ end pages = [ "Home" => "index.md", - "Tutorial" => ["firststeps.md", "randomwalks.md", "chemotaxis.md"], - "Validation" => "validation.md", + "Introduction" => ["firststeps.md", "randomwalks.md", "chemotaxis.md"], "Examples" => [ [namify(section) => [joinpath.("examples", section, readdir(outdir[section]))...] for section in sections]... ], + "Validation" => "validation.md", "API" => "api.md" ] diff --git a/docs/src/examples/Chemotaxis/celani_gauss2D.md b/docs/src/examples/Chemotaxis/celani_gauss2D.md new file mode 100644 index 0000000..0aad720 --- /dev/null +++ b/docs/src/examples/Chemotaxis/celani_gauss2D.md @@ -0,0 +1,56 @@ +```@meta +EditURL = "../../../../examples/Chemotaxis/celani_gauss2D.jl" +``` + +# Noisy chemotaxis towards Gaussian source + +In this example we set up a static Gaussian source and observe the chemotactic behavior +of the `Celani` model, in the presence of sensing noise (via the `chemotactic_precision`). +Playing with the `chemotactic_precision`, it can be seen that the clustering of bacteria +at the source becomes stronger with decreasing noise (decreasing chemotactic precision). + +````@example celani_gauss2D +using MicrobeAgents +using Plots + +function concentration_field(pos, model) + C = model.C + σ = model.σ + p₀ = model.p₀ + concentration_field(pos, p₀, C, σ) +end +concentration_field(pos, p₀, C, σ) = C * exp(-sum(abs2.(pos.-p₀))/(2*σ^2)) + +timestep = 0.1 # s +extent = ntuple(_ -> 1000.0, 2) # μm +space = ContinuousSpace(extent; periodic=false) +p₀ = extent./2 # μm +C = 1.0 # μM +σ = 100.0 # μm +properties = Dict( + :concentration_field => concentration_field, + :C => C, + :σ => σ, + :p₀ => p₀, +) + +model = StandardABM(Celani{2}, space, timestep; properties) +foreach(_ -> add_agent!(model; chemotactic_precision=6.0), 1:300) + +nsteps = 5000 +adata = [position] +adf, = run!(model, nsteps; adata) + +traj = vectorize_adf_measurement(adf, :position) +xmesh = range(0, first(spacesize(model)); length=100) +ymesh = range(0, last(spacesize(model)); length=100) +c = [concentration_field(p, p₀, C, σ) for p in Iterators.product(xmesh, ymesh)] +heatmap(xmesh, ymesh, c', cbar=false, ratio=1, c=:bone, axis=false) +x = getindex.(traj,1)[end-100:4:end, :] +y = getindex.(traj,2)[end-100:4:end, :] +a = axes(x,1) ./ size(x,1) +plot!(x, y, + lab=false, lims=(0,1000), lw=1, alpha=a +) +```` + diff --git a/docs/src/examples/Chemotaxis/response_functions.md b/docs/src/examples/Chemotaxis/response_functions.md new file mode 100644 index 0000000..e9d2656 --- /dev/null +++ b/docs/src/examples/Chemotaxis/response_functions.md @@ -0,0 +1,146 @@ +```@meta +EditURL = "../../../../examples/Chemotaxis/response_functions.jl" +``` + +# Response function (Xie) + +In this example we will probe the response function implemented +in the `Xie` model of chemotaxis. +The impulse response function is the "output" of the bacterial chemotaxis +pathway when presented with an input signal`. + +To do this, we will emulate another classical laboratory assay, where the +bacterium is tethered to a wall, and it is exposed to a temporal change +in the concentration of a chemoattractant. +The response to the stimulus can be measured by observing modulations +in the instantaneous tumbling rate. +For each of the implemented microbe types, MicrobeAgents provides a +`tumblebias` function which returns the instantaneous bias +in the tumbling rate, evaluated from the internal state of the microbe. +Monitoring the time evolution of the tumble bias under teporal stimuli +then allows us to access the response function of the microbe. + +In the `Xie` model, chemotaxis is implemented by direct samplings of the +`concentration_field`, thus we don't need to explicitly define neither +a `concentration_gradient` nor a `concentration_time_derivative`. +We will represent our temporal stimuli in the form of square waves which +instantaneously switch from a baseline value `C₀` to a peak value `C₁+C₀` +homogeneously over space. +The excitation will occur at a time `t₁` and go back to baseline levels +at a time `t₂`. + +````@example response_functions +using MicrobeAgents +using Plots + +θ(a,b) = a>b ? 1.0 : 0.0 # heaviside theta function +function concentration_field(pos, model) + C₀ = model.C₀ + C₁ = model.C₁ + t₁ = model.t₁ + t₂ = model.t₂ + dt = model.timestep + t = abmtime(model) * dt + # notice the time dependence! + concentration_field(t, C₀, C₁, t₁, t₂) +end +concentration_field(t,C₀,C₁,t₁,t₂) = C₀+C₁*θ(t,t₁)*(1-θ(t,t₂)) + +space = ContinuousSpace(ntuple(_ -> 500.0, 3)) # μm +C₀ = 0.01 # μM +C₁ = 5.0-C₀ # μM +T = 60.0 # s +t₁ = 20.0 # s +t₂ = 40.0 # s +properties = Dict( + :concentration_field => concentration_field, + :C₀ => C₀, + :C₁ => C₁, + :t₁ => t₁, + :t₂ => t₂, +) + +dt = 0.1 # s +model = StandardABM(Xie{3}, space, dt; properties) +```` + +A peculiarity of the `Xie` model is that the chemotactic properties of the +microbe differ between the forward and backward motile states, so we can +probe the response function in both the forward and backward motile state +by initializing two distinct microbes in the two states. +To keep the microbes in these motile states for the entire experiment duration, +we suppress their tumbles, and (just for total consistency with experiments) +we also set their speed to 0. + +````@example response_functions +add_agent!(model; turn_rate_forward=0, + motility=RunReverseFlick(motile_state=MotileState(Forward), speed=[0]) +) +add_agent!(model; turn_rate_backward=0, + motility=RunReverseFlick(motile_state=MotileState(Backward), speed=[0]) +) +```` + +In addition to the `tumblebias`, we will also monitor two other quantities +`state_m` and `state_z` which are internal variables of the `Xie` model +which represent the methylation and dephosphorylation processes which +together control the chemotactic response of the bacterium. + +````@example response_functions +nsteps = round(Int, T/dt) +#state(a::Xie) = max(tumblebias(a)*a.state, 0) +adata = [tumblebias, :state_m, :state_z] +adf, = run!(model, nsteps; adata) + +S = vectorize_adf_measurement(adf, :tumblebias) +m = (vectorize_adf_measurement(adf, :state_m))[:,1] # take only fw +z = (vectorize_adf_measurement(adf, :state_z))[:,1] # take only fw +```` + +We first look at the response function in the forward and backward +motile state: when the concentration increases we have a sharp negative +response (the tumble bias decreases), then the bacterium adapts to the new +concentration level, and when it drops back to the basal level we observe +a sharp positive response (the tumble bisa increases) before adapting +again to the new concentration level. + +````@example response_functions +_green = palette(:default)[3] +plot() +x = (0:dt:T) .- t₁ +plot!( + x, S, + lw=1.5, lab=["Forward" "Backward"] +) +plot!(ylims=(-0.1,4.5), ylab="Response", xlab="time (s)") +plot!(twinx(), + x, t -> concentration_field(t.+t₁,C₀,C₁,t₁,t₂), + ls=:dash, lw=1.5, lc=_green, lab=false, + tickfontcolor=_green, + ylab="C (μM)", guidefontcolor=_green +) +```` + +By analysing the methylation and dephosphorylation processes, we can +understand how the chemotactic response arises. +First, when the concentration increases, both `m` and `z` increase and converge +to a new steady-state value, but since they respond on different timescales, +the response (defined by the difference between these two quantities), shows +a sharp decrease followed by a slower relaxation. +The same occurs for the negative stimulus. + +````@example response_functions +x = (0:dt:T) .- t₁ +τ_m = model[1].adaptation_time_m +τ_z = model[1].adaptation_time_z +M = m ./ τ_m +Z = z ./ τ_z +R = M .- Z +plot( + x, [M Z R], + lw=2, + lab=["m/τ_m" "z/τ_z" "m/τ_m - z/τ_z"], + xlab="time (s)" +) +```` + diff --git a/docs/src/examples/Chemotaxis/xie_2D.md b/docs/src/examples/Chemotaxis/xie_2D.md deleted file mode 100644 index 7e31cf4..0000000 --- a/docs/src/examples/Chemotaxis/xie_2D.md +++ /dev/null @@ -1,46 +0,0 @@ -```@meta -EditURL = "../../../../examples/Chemotaxis/xie_2D.jl" -``` - -````@example xie_2D -using MicrobeAgents -using Plots -using Random - -function concentration_field(pos, model) - C = model.C - σ = model.σ - p₀ = model.p₀ - concentration_field(pos, p₀, C, σ) -end -concentration_field(pos, p₀, C, σ) = C * exp(-sum(abs2.(pos.-p₀))/(2*σ^2)) - -timestep = 0.1 # s -extent = ntuple(_ -> 1000.0, 2) # μm -space = ContinuousSpace(extent; periodic=false) -p₀ = extent./2 # μm -C = 500.0 # μM -σ = 25.0 # μm -properties = Dict( - :concentration_field => concentration_field, - :C => C, - :σ => σ, - :p₀ => p₀, -) - -rng = MersenneTwister(12) -model = StandardABM(Xie{2}, space, timestep; rng, properties) -foreach(_ -> add_agent!(model; chemotactic_precision=6.0), 1:300) - -nsteps = 5000 -adata = [:pos] -adf, = run!(model, nsteps; adata) - -traj = vectorize_adf_measurement(adf, :pos) -plot( - first.(traj)[end-100:end,:], - last.(traj)[end-100:end,:], - lab=false, lims=(0,1000) -) -```` - diff --git a/docs/src/examples/Chemotaxis/xie_response-function.md b/docs/src/examples/Chemotaxis/xie_response-function.md index 5ffa716..e45c633 100644 --- a/docs/src/examples/Chemotaxis/xie_response-function.md +++ b/docs/src/examples/Chemotaxis/xie_response-function.md @@ -2,11 +2,21 @@ EditURL = "../../../../examples/Chemotaxis/xie_response-function.jl" ``` +# Comparison of chemotactic response functions + +Here we will compare the chemotactic response function of the `Celani` +and `BrownBerg` model to an impulse stimulus of chemoattractant. + +While `Celani` only needs the `concentration_field` to determine the +chemotactic response, `BrownBerg` also needs the `concentration_time_derivative` +to be defined explicitly (also the `concentration_gradient` but it's +not relevant in this specific study). + ````@example xie_response-function using MicrobeAgents using Plots -θ(a,b) = a>b ? 1.0 : 0.0 +θ(a,b) = a>b ? 1.0 : 0.0 # Heaviside theta function function concentration_field(pos, model) C₀ = model.C₀ C₁ = model.C₁ @@ -18,73 +28,65 @@ function concentration_field(pos, model) end concentration_field(t,C₀,C₁,t₁,t₂) = C₀+C₁*θ(t,t₁)*(1-θ(t,t₂)) -timestep = 0.1 # s +δ(t,dt) = 0 <= t <= dt ? 1.0/dt : 0.0 # discrete approximation to Dirac delta +function concentration_time_derivative(pos, model) + C₀ = model.C₀ + C₁ = model.C₁ + t₁ = model.t₁ + t₂ = model.t₂ + dt = model.timestep + t = abmtime(model) * dt + concentration_time_derivative(t, C₀, C₁, t₁, t₂, dt) +end +function concentration_time_derivative(t, C₀, C₁, t₁, t₂, dt) + C₁*(δ(t-t₁, dt) - δ(t-t₂, dt)) +end + space = ContinuousSpace(ntuple(_ -> 500.0, 3)) # μm -C₀ = 0.01 # μM -C₁ = 5.0-C₀ # μM -T = 60.0 # s -t₁ = 20.0 # s -t₂ = 40.0 # s +C₀ = 1.0 # μM +C₁ = 2.0-C₀ # μM +T = 50.0 # s +dt = 0.1 # s +t₁ = 10.0 # s +t₂ = 30.0 # s properties = Dict( :concentration_field => concentration_field, + :concentration_time_derivative => concentration_time_derivative, :C₀ => C₀, :C₁ => C₁, :t₁ => t₁, :t₂ => t₂, ) -model = StandardABM(Xie{3}, space, timestep; properties) -add_agent!(model; turn_rate_forward=0, motility=RunReverseFlick(motile_state=MotileState(Forward))) -add_agent!(model; turn_rate_backward=0, motility=RunReverseFlick(motile_state=MotileState(Backward))) - -nsteps = round(Int, T/timestep) -β(a) = a.motility.state == Forward ? a.gain_forward : a.gain_backward -state(a::Xie) = max(1 + β(a)*a.state, 0) -adata = [state, :state_m, :state_z] -adf, = run!(model, nsteps; adata) - -S = vectorize_adf_measurement(adf, :state) -m = (vectorize_adf_measurement(adf, :state_m))[:,1] # take only fw -z = (vectorize_adf_measurement(adf, :state_z))[:,1] # take only fw -```` +model = StandardABM(Union{BrownBerg{3},Celani{3}}, space, dt; properties) -response vs time for fw and bw modes +add_agent!(BrownBerg{3}, model; turn_rate=0, motility=RunTumble(speed=[0]), + memory=1, +) +add_agent!(Celani{3}, model; turn_rate=0, motility=RunTumble(speed=[0]), gain=4) +add_agent!(Celani{3}, model; turn_rate=0, motility=RunTumble(speed=[0]), gain=4, + chemotactic_precision=50.0 +) -````@example xie_response-function -begin - _green = palette(:default)[3] - plot() - x = (0:timestep:T) .- t₁ - plot!( - x, S, - lw=1.5, lab=["Forward" "Backward"] - ) - plot!(ylims=(-0.1,4.5), ylab="Response", xlab="time (s)") - plot!(twinx(), - x, t -> concentration_field(t.+t₁,C₀,C₁,t₁,t₂), - ls=:dash, lw=1.5, lc=_green, lab=false, - tickfontcolor=_green, - ylab="C (μM)", guidefontcolor=_green - ) -end -```` +nsteps = round(Int, T/dt) +adata = [tumblebias] +adf, = run!(model, nsteps; adata) -methylation and dephosphorylation +S = vectorize_adf_measurement(adf, :tumblebias) -````@example xie_response-function -begin - x = (0:timestep:T) .- t₁ - τ_m = model[1].adaptation_time_m - τ_z = model[1].adaptation_time_z - M = m ./ τ_m - Z = z ./ τ_z - R = M .- Z - plot( - x, [M Z R], - lw=2, - lab=["m/τ_m" "z/τ_z" "m/τ_m - z/τ_z"], - xlab="time (s)" - ) -end +_pink = palette(:default)[4] +plot() +x = (0:dt:T) .- t₁ +plot!( + x, S, + lw=1.5, lab=["BrownBerg" "Celani" "Celani + Noise"] +) +plot!(ylims=(-0.1,2.1), ylab="Response", xlab="time (s)") +plot!(twinx(), + x, t -> concentration_field(t.+t₁,C₀,C₁,t₁,t₂), + ls=:dash, lw=1.5, lc=_pink, lab=false, + tickfontcolor=_pink, + ylab="C (μM)", guidefontcolor=_pink +) ```` diff --git a/docs/src/firststeps.md b/docs/src/firststeps.md index 77b6fec..bfcee0a 100644 --- a/docs/src/firststeps.md +++ b/docs/src/firststeps.md @@ -4,7 +4,7 @@ An `AgentBasedModel` object embeds all the properties of the system to be simulated and maps unique IDs to microbe instances. During the simulation, the model is evolved in discrete time steps, with -each microbe's "state" being updated according to specified rules. +each microbe's position, velocity and "state" being updated according to specified rules. Standard rules for motion, reorientations and chemotaxis are available by default, but custom behaviors can be implemented via user-defined functions. @@ -13,7 +13,7 @@ The typical workflow to run a simulation in MicrobeAgents.jl goes as follows: 2. Choose an appropriate microbe type to represent the desired behavior, or define a new one. 3. Initialize an `AgentBasedModel` object with the desired space, microbe type, integration time step, and any extra property needed for the simulation. 4. Populated the ABM with microbe instances. -5. Run the model (defining custom stepping functions if required) and collect data. +5. Choose the observables to collect during production and run the model. MicrobeAgents.jl re-exports and extends various function from Agents.jl in order to work as a standalone, but it is generally recommended to use it in combination with @@ -22,7 +22,7 @@ Agents.jl for extra goodies. ## Space MicrobAgents.jl only supports continuous spaces with dimensions 1, 2 or 3. Spaces can be created with the `ContinuousSpace` function (reexported from Agents.jl). -The extent of the space must be given as a tuple, and periodicity is set with +The extent of the space must be given as a tuple or `SVector`, and periodicity is set with the `periodic` kwarg (defaults to true). ``` # one-dimensional periodic space @@ -32,6 +32,10 @@ ContinuousSpace(extent) # two-dimensional non-periodic space extent = (1.0, 2.0) ContinuousSpace(extent; periodic=false) + +# three-dimensional space periodic only along the x direction +extent = (100.0, 20.0, 20.0) +ContinuousSpace(extent; periodic=(true,false,false)) ``` @@ -42,6 +46,7 @@ AbstractMicrobe ``` MicrobeAgents provides different `AbstractMicrobe` subtypes representing different models of bacterial behavior from the literature. +The list of implemented models can be obtained with `subtypes(AbstractMicrobe)`. A basic type, which is typically sufficient for simple motility simulations and does not include chemotaxis, is the `Microbe` type. ```@docs @@ -49,31 +54,39 @@ Microbe ``` The dimensionality of `Microbe` *must* always be specified on creation. All the fields are instead optional, and if not specified will be assigned default values. +Microbe instances should only be created within an `AgentBasedModel`. -To create a `Microbe` living in a 1-dimensional space, with default parameters -(`RunTumble` motility, average turn rate ``\nu=1\;s^{-1}``, and no rotational diffusivity), -it is therefore sufficient to run -``` -Microbe{1}() +In MicrobeAgents.jl, models are created through the `StandardABM` function. +```@docs +StandardABM ``` -Similarly, for two and three dimensions: +To initialize a model we must provide the microbe type, the simulation space, and the +integration timestep of the simulation. All other parameters are optional. +To setup a model for `Microbe`s living in a 1-dimensional space we can therefore run ``` -Microbe{2}() -Microbe{3}() +space = ContinuousSpace((100.0,); periodic=false) +dt = 0.1 +model = StandardABM(Microbe{1}, space, dt) ``` -Any custom parameter can be set via kwargs: +Now, calling `add_agent!(model)` will populate the model with microbes of +the specified type (`Microbe{1}`) using the default values of the constructor, +and automatically generating a random position and a random velocity vector. +To select a position, it can be passed as the first argument to the `add_agent!` call, +and any other bacterial parameter can be defined via keyword arguments. +All of the following are valid calls ``` -Microbe{3}( - turn_rate = 0.6, - rotational_diffusivity = 0.1 -) +# a Microbe with large radius and low tumble rate +add_agent!(model; radius=10.0, turn_rate=0.17) +# a Microbe with custom position and high coefficient of rotational diffusion +add_agent!((53.2,), model; rotational_diffusivity=0.5) +# a Microbe initialized with velocity to the right +add_agent!(model; vel=(1.0,)) ``` - All the other subtypes of `AbstractMicrobe` work in a similar way, although they will have distinct default values and extra fields. -Default values are typically assigned following the original implementation in the literature. +When possible, default values are typically assigned following the original implementation in the literature. ```@docs BrownBerg @@ -83,40 +96,32 @@ Xie ``` -## Creating a model +## More about models MicrobeAgents.jl exploits the `AgentBasedModel` interface from Agents.jl. While the standard Agents.jl syntax will always work, it is typically more convenient to use the method extensions provided by MicrobeAgents.jl, which also includes some default parameters required by the simulations. -Whenever removal of microbes during the simulation is not needed, -it is recommended to call `StandardABM` with the `container=Vector` -keyword argument to improve performance. -```@docs -StandardABM -``` - -To create a simple model, we just need to choose a microbe type, the size of -the simulation domain and the integration timestep. -The properties of the simulation domain are wrapped in the `ContinuousSpace` -object. -``` -extent = (1000.0, 500.0) # size of 2D simulation domain -space = ContinuousSpace(extent) -dt = 0.1 # integration timestep -model = StandardABM(Microbe{2}, space, dt) -``` - -Now bacteria can be added with the `add_agent!` function. -```@docs -add_agent! -``` - -We need not specify anything if we want the microbe to be added at a random -position with the default values from the constructor. -``` -add_agent!(model) -``` -The microbe will be now accessible as `model[1]`. +If the simulation requires removal/addition of microbes, it is recommended +to call `StandardABM` with the `container=Dict` keyword argument, +otherwise MicrobeAgents.jl defaults to `container=Vector` which provides +better performance. + +In addition to the microbe instances, the model should also wrap all +the other information required to perform the simulation. + +MicrobeAgents.jl defines default timestepping functions which are used +to evolve the microbes and the model, and are accessible through the +`microbe_step!` and `model_step!` keywords in `StandardABM`. +By default, the `microbe_step!` function performs, in order: +- update microbe position according to current velocity +- randomize the microbe orientation through rotational diffusion (if present) +- update internal state of the microbe (e.g. chemotaxis or other user-defined behavior) +- perform reorientation events following Poissonian statistics +The `model_step!` function instead defaults to a dummy function which does nothing. +Any custom behavior can be implemented by simply modifying these two functions. + +Any type of external parameter that should be used during the simulation should be +passed to `StandardABM` through the `properties` dictionary. ## Running a model After the model has been created and populated with the desired number of microbes, @@ -139,27 +144,15 @@ y = last.(adf.pos) plot(x, y) ``` -Notice that we did not specify at all how the timestepping is performed. -MicrobAgents.jl implements a default timestepper which is applied to all -`AbstractMicrobe` instances, which takes care of motion, rotational diffusion -and reorientations. -Each subtype is then equipped with its own `affect!` and `turnrate` functions -(explained later) which determine extra behavioral features (such as chemotaxis). - -If different behavior is desired, the integration can be customized by -passing custom timestepping functions to `run!`: -``` -run!(model, my_agent_step!, my_model_step!, nsteps) -``` ## Motility patterns In MicrobeAgents.jl, motility patterns are represented as instances of `AbstractMotility`. In particular, currently available patterns are distinguished into two further categories: `AbstractMotilityOneStep` or `AbstractMotilityTwoStep`. -```@docs -AbstractMotilityOneStep -AbstractMotilityTwoStep +```@docs; canonical=false +MotilityOneStep +MotilityTwoStep ``` One-step motility pattern are characterized by a single swimming stage. Two-step motility patterns instead have two stages which can have distinct properties; From cf3fe984a36a1b29041aa441e72a57cd0c321f41 Mon Sep 17 00:00:00 2001 From: Riccardo Foffi Date: Thu, 28 Dec 2023 18:48:49 +0100 Subject: [PATCH 5/6] temporary fix --- docs/Project.toml | 12 ------------ docs/make.jl | 5 +++++ 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index c21c3f7..646a639 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,20 +1,8 @@ [deps] -Agents = "46ada45e-f475-11e8-01d0-f70cc89e6671" -AxisArrays = "39de3d68-74b9-583c-8d2d-e117c070f3a9" -CellListMap = "69e1c6dd-3888-40e6-b3c8-31ac5f578864" -DSP = "717857b8-e6f2-59f4-9121-6e50c889abd2" -DisplayAs = "0b91fe84-8a4c-11e9-3e1d-67c38462b6d6" -Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DocumenterTools = "35a29f4d-8980-5a13-9543-d66fff28ecb8" -GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" -LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" -OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" -Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -Rotations = "6038ab10-8711-5258-84ad-4b1120ba62dc" -StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [compat] Documenter = "1" diff --git a/docs/make.jl b/docs/make.jl index a019d84..f0b4640 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,6 +1,11 @@ +#= cd(@__DIR__) using MicrobeAgents using Documenter +=# +# temporary +push!(LOAD_PATH, "../src/") +using Documenter, MicrobeAgents ENV["JULIA_DEBUG"] = "Documenter" CI = get(ENV, "CI", nothing) == "true" || get(ENV, "GITHUB_TOKEN", nothing) !== nothing import Literate From e0edaa18be1941d9d0c51e5f8c7a5bfbc4bac12c Mon Sep 17 00:00:00 2001 From: Riccardo Foffi Date: Thu, 28 Dec 2023 18:58:52 +0100 Subject: [PATCH 6/6] try another fix --- docs/Project.toml | 11 +++++++++++ docs/make.jl | 5 +---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index 646a639..c53e0e5 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,8 +1,19 @@ [deps] +Agents = "46ada45e-f475-11e8-01d0-f70cc89e6671" +AxisArrays = "39de3d68-74b9-583c-8d2d-e117c070f3a9" +CellListMap = "69e1c6dd-3888-40e6-b3c8-31ac5f578864" +DSP = "717857b8-e6f2-59f4-9121-6e50c889abd2" +Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DocumenterTools = "35a29f4d-8980-5a13-9543-d66fff28ecb8" +GeometryBasics = "5c1252a2-5f33-56bf-86c9-59e7332b4326" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" +OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Rotations = "6038ab10-8711-5258-84ad-4b1120ba62dc" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [compat] Documenter = "1" diff --git a/docs/make.jl b/docs/make.jl index f0b4640..2083b72 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,10 +1,7 @@ -#= cd(@__DIR__) +import Pkg; Pkg.add(name="Agents", rev="main") using MicrobeAgents using Documenter -=# -# temporary -push!(LOAD_PATH, "../src/") using Documenter, MicrobeAgents ENV["JULIA_DEBUG"] = "Documenter" CI = get(ENV, "CI", nothing) == "true" || get(ENV, "GITHUB_TOKEN", nothing) !== nothing