From 482951f064fa0700d7d42308e0e311d53df690dc Mon Sep 17 00:00:00 2001 From: Riccardo Foffi Date: Thu, 21 Dec 2023 22:28:41 +0100 Subject: [PATCH] Simplify motility types (#62) * remove `AbstractMotility` from type definitions and box motility field with `motilepattern` calls * remove `AbstractMotility` from type definitions and box motility field with `motilepattern` calls * rewrite api and simplify motility types * introduce `cwbias`, various fixes * `position` must extend function with same name from Base * fixes * update examples and tests --- .../Analysis/velocity_autocorrelations.jl | 15 +- examples/Chemotaxis/xie_response-function.jl | 3 +- .../randomwalk2D_motilepatterns.jl | 2 +- examples/RandomWalks/randomwalk3D.jl | 2 +- src/MicrobeAgents.jl | 3 +- src/api.jl | 30 ++ src/bodies/spheres.jl | 4 +- src/chemotaxis/brown-berg.jl | 9 +- src/chemotaxis/brumley.jl | 11 +- src/chemotaxis/celani.jl | 9 +- src/chemotaxis/xie.jl | 49 ++- src/microbe_step.jl | 7 +- src/microbes.jl | 24 +- src/motility.jl | 284 ++++-------------- src/rotations.jl | 14 +- src/utils.jl | 36 +-- test/model-creation.jl | 32 +- test/model-stepping.jl | 4 +- test/motility.jl | 56 ++-- 19 files changed, 207 insertions(+), 387 deletions(-) create mode 100644 src/api.jl diff --git a/examples/Analysis/velocity_autocorrelations.jl b/examples/Analysis/velocity_autocorrelations.jl index 77ba5e3..b9ac103 100644 --- a/examples/Analysis/velocity_autocorrelations.jl +++ b/examples/Analysis/velocity_autocorrelations.jl @@ -10,19 +10,20 @@ space = ContinuousSpace((L,L,L)) model = StandardABM(Microbe{3}, space, Δt; container=Vector) n = 200 -for i in 1:n - add_agent!(model; turn_rate, motility=RunTumble(speed=[U])) - add_agent!(model; turn_rate, motility=RunReverse(speed_forward=[U])) - add_agent!(model; turn_rate, motility=RunReverseFlick(speed_forward=[U])) +for Motility in (RunTumble, RunReverse, RunReverseFlick), i in 1:n + add_agent!(model; turn_rate, motility=Motility(speed=[U])) end +ids_runtumble = 1:n +ids_runreverse = (1:n) .+ n +ids_runrevflick = (1:n) .+ 2n nsteps = round(Int, 100τ_run / Δt) adata = [:vel] adf, = run!(model, nsteps; adata) -adf_runtumble = filter(:id => id -> model[id].motility isa RunTumble, adf; view=true) -adf_runrev = filter(:id => id -> model[id].motility isa RunReverse, adf; view=true) -adf_runrevflick = filter(:id => id -> model[id].motility isa RunReverseFlick, adf; view=true) +adf_runtumble = filter(:id => id -> id in ids_runtumble, adf; view=true) +adf_runrev = filter(:id => id -> id in ids_runreverse, adf; view=true) +adf_runrevflick = filter(:id => id -> id in ids_runrevflick, adf; view=true) adfs = [adf_runtumble, adf_runrev, adf_runrevflick] t = range(0, (nsteps-1)*Δt; step=Δt) diff --git a/examples/Chemotaxis/xie_response-function.jl b/examples/Chemotaxis/xie_response-function.jl index 3fd9df0..ae1cdf7 100644 --- a/examples/Chemotaxis/xie_response-function.jl +++ b/examples/Chemotaxis/xie_response-function.jl @@ -28,8 +28,7 @@ properties = Dict( :t₂ => t₂, ) -model_step!(model) = model.t += 1 -model = StandardABM(Xie{3}, space, timestep; properties, model_step!) +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))) diff --git a/examples/RandomWalks/randomwalk2D_motilepatterns.jl b/examples/RandomWalks/randomwalk2D_motilepatterns.jl index 09b3650..d5e1d92 100644 --- a/examples/RandomWalks/randomwalk2D_motilepatterns.jl +++ b/examples/RandomWalks/randomwalk2D_motilepatterns.jl @@ -11,7 +11,7 @@ nsteps = 600 ## abm setup model = StandardABM(Microbe{2}, space, dt) # add bacteria with different motile properties -add_agent!(model; motility=RunReverse(speed_forward=[55]), rotational_diffusivity=0.2) +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) diff --git a/examples/RandomWalks/randomwalk3D.jl b/examples/RandomWalks/randomwalk3D.jl index cf4ef11..d9fc491 100644 --- a/examples/RandomWalks/randomwalk3D.jl +++ b/examples/RandomWalks/randomwalk3D.jl @@ -11,7 +11,7 @@ nsteps = 600 ## abm setup model = StandardABM(Microbe{3}, space, dt) # add bacteria with different motile properties -add_agent!(model; motility=RunReverse(speed_forward=[55]), rotational_diffusivity=0.2) +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) diff --git a/src/MicrobeAgents.jl b/src/MicrobeAgents.jl index 946d110..2854b55 100644 --- a/src/MicrobeAgents.jl +++ b/src/MicrobeAgents.jl @@ -36,8 +36,9 @@ All microbe types *must* have at least the following fields: - `radius::Real` equivalent spherical radius of the microbe - `state::Real` generic variable for a scalar internal state """ -abstract type AbstractMicrobe{D} <: AbstractAgent where D end +abstract type AbstractMicrobe{D} <: AbstractAgent where {D} end +include("api.jl") include("utils.jl") include("motility.jl") include("rotations.jl") diff --git a/src/api.jl b/src/api.jl new file mode 100644 index 0000000..42bf784 --- /dev/null +++ b/src/api.jl @@ -0,0 +1,30 @@ +export position, direction, speed, velocity, motilepattern, + turnrate, rotational_diffusivity, radius, state, + cwbias, + distance, distancevector + +Base.position(m::AbstractMicrobe) = m.pos +direction(m::AbstractMicrobe) = m.vel +speed(m::AbstractMicrobe) = m.speed +velocity(m::AbstractMicrobe) = direction(m) .* speed(m) +motilepattern(m::AbstractMicrobe) = m.motility +turnrate(m::AbstractMicrobe) = m.turn_rate +rotational_diffusivity(m::AbstractMicrobe) = m.rotational_diffusivity +radius(m::AbstractMicrobe) = m.radius +state(m::AbstractMicrobe) = m.state + +distance(a, b, model) = euclidean_distance(position(a), position(b), model) +distancevector(a, b, model) = distancevector(position(a), position(b), model) +function distancevector(a::SVector{D}, b::SVector{D}, model) where D + extent = spacesize(model) + SVector{D}(wrapcoord(a[i], b[i], extent[i]) for i in 1:D) +end + +Base.position(a::SVector{D}) where D = a +Base.position(a::NTuple{D}) where D = SVector{D}(a) + +## utils +function wrapcoord(x1, x2, d) + a = (x2 - x1) / d + (a - round(a)) * d +end diff --git a/src/bodies/spheres.jl b/src/bodies/spheres.jl index d54cc80..4ad2ce0 100644 --- a/src/bodies/spheres.jl +++ b/src/bodies/spheres.jl @@ -3,8 +3,8 @@ export HyperSphere, contact, is_encounter # dispatch hides call to Point HyperSphere(center::SVector{D}, radius::Real) where D = HyperSphere(Point(Float64.(center)), Float64(radius)) -@inline position(a::HyperSphere{D}) where D = SVector{D}(a.center) -@inline radius(a::AbstractMicrobe) = a.radius +@inline Base.position(a::HyperSphere{D}) where D = SVector{D}(a.center) +#@inline radius(a::AbstractMicrobe) = a.radius @inline radius(a::HyperSphere) = a.r @inline contact(a,b,model) = distance(a,b,model) ≤ radius(a) + radius(b) diff --git a/src/chemotaxis/brown-berg.jl b/src/chemotaxis/brown-berg.jl index bc73f72..04db08c 100644 --- a/src/chemotaxis/brown-berg.jl +++ b/src/chemotaxis/brown-berg.jl @@ -16,7 +16,7 @@ Default parameters: """ @agent struct BrownBerg{D}(ContinuousAgent{D,Float64}) <: AbstractMicrobe{D} speed::Float64 - motility::AbstractMotility = RunTumble() + motility = RunTumble() turn_rate::Float64 = 1 / 0.67 rotational_diffusivity::Float64 = 0.035 radius::Float64 = 0.5 @@ -45,9 +45,8 @@ function affect!(microbe::BrownBerg, model) chemotaxis!(microbe, model) end -function turnrate(microbe::BrownBerg, model) - ν₀ = microbe.turn_rate # unbiased +function cwbias(microbe::BrownBerg, model) g = microbe.gain S = microbe.state - return ν₀ * exp(-g * S) # modulated turn rate -end # function + return exp(-g*S) +end diff --git a/src/chemotaxis/brumley.jl b/src/chemotaxis/brumley.jl index 14a46f2..09310b0 100644 --- a/src/chemotaxis/brumley.jl +++ b/src/chemotaxis/brumley.jl @@ -7,7 +7,7 @@ The model is optimized for simulation of marine bacteria and accounts for the presence of (gaussian) sensing noise in the chemotactic pathway. Default parameters: -- `motility = RunReverseFlick(speed_forward = [46.5])` +- `motility = RunReverseFlick(speed = [46.5])` - `turn_rate = 2.22` Hz → '1/τ₀' - `state = 0.0` → 'S' - `rotational_diffusivity = 0.035` rad²/s @@ -19,7 +19,7 @@ Default parameters: """ @agent struct Brumley{D}(ContinuousAgent{D,Float64}) <: AbstractMicrobe{D} speed::Float64 - motility::AbstractMotility = RunReverseFlick(speed_forward = [46.5]) + motility = RunReverseFlick(speed = [46.5]) turn_rate::Float64 = 1 / 0.45 rotational_diffusivity::Float64 = 0.035 radius::Float64 = 0.5 @@ -55,9 +55,8 @@ function affect!(microbe::Brumley, model) chemotaxis!(microbe, model) end -function turnrate(microbe::Brumley, model) - ν₀ = microbe.turn_rate # unbiased +function cwbias(microbe::Brumley, model) Γ = microbe.gain S = microbe.state - return (1 + exp(-Γ * S)) * ν₀ / 2 # modulated turn rate -end # function + return (1 + exp(-Γ*S))/2 +end diff --git a/src/chemotaxis/celani.jl b/src/chemotaxis/celani.jl index d1f3812..71b6162 100644 --- a/src/chemotaxis/celani.jl +++ b/src/chemotaxis/celani.jl @@ -21,7 +21,7 @@ Default parameters: """ @agent struct Celani{D}(ContinuousAgent{D,Float64}) <: AbstractMicrobe{D} speed::Float64 - motility::AbstractMotility = RunTumble(speed = [30.0]) + motility = RunTumble(speed = [30.0]) turn_rate::Float64 = 1 / 0.67 rotational_diffusivity::Float64 = 0.26 radius::Float64 = 0.5 @@ -53,12 +53,11 @@ function affect!(microbe::Celani, model) chemotaxis!(microbe, model) end -function turnrate(microbe::Celani, model) - ν₀ = microbe.turn_rate # unbiased +function cwbias(microbe::Celani, model) β = microbe.gain S = microbe.state - return ν₀ * (1 - β * S) # modulated turn rate -end # function + return (1 - β*S) +end # Celani requires a custom add_agent! method # to initialize the markovian variables at steady state diff --git a/src/chemotaxis/xie.jl b/src/chemotaxis/xie.jl index fa04a6f..7e817ea 100644 --- a/src/chemotaxis/xie.jl +++ b/src/chemotaxis/xie.jl @@ -14,7 +14,7 @@ tuned through a `chemotactic_precision` factor inspired by 'Brumley et al. (2019) PNAS' (defaults to 0, i.e. no noise). Default parameters: -- `motility = RunReverseFlick(speed_forward = [46.5])` +- `motility = RunReverseFlick(speed = [46.5])` - `turn_rate_forward = 2.3` Hz - `turn_rate_backward = 1.9` Hz - `state = 0.0` s @@ -31,7 +31,7 @@ Default parameters: """ @agent struct Xie{D}(ContinuousAgent{D,Float64}) <: AbstractMicrobe{D} speed::Float64 - motility::AbstractMotility = RunReverseFlick(speed_forward = [46.5]) + motility = RunReverseFlick(speed = [46.5]) turn_rate_forward::Float64 = 2.3 turn_rate_backward::Float64 = 1.9 rotational_diffusivity::Float64 = 0.26 @@ -56,33 +56,6 @@ function Base.show(io::IO, ::MIME"text/plain", m::Xie{D}) where {D} print(io, "other properties: " * join(s, ", ")) end -# Xie requires its own turnrate functions -# since it has different parameters for fw and bw states -function turnrate(microbe::Xie, model) - if microbe.motility isa AbstractMotilityTwoStep - return turnrate_twostep(microbe, model) - else - return turnrate_onestep(microbe, model) - end -end -function turnrate_twostep(microbe::Xie, model) - S = microbe.state - if microbe.motility.state == Forward - ν₀ = microbe.turn_rate_forward - β = microbe.gain_forward - else - ν₀ = microbe.turn_rate_backward - β = microbe.gain_backward - end - return ν₀ * (1 + β * S) -end -function turnrate_onestep(microbe::Xie, model) - S = microbe.state - ν₀ = microbe.turn_rate_forward - β = microbe.gain_forward - return ν₀ * (1 + β * S) -end - function chemotaxis!(microbe::Xie, model; ε=1e-16) Δt = model.timestep Dc = model.compound_diffusivity @@ -110,3 +83,21 @@ end function affect!(microbe::Xie, model) chemotaxis!(microbe, model) end + +function cwbias(microbe::Xie, model) + S = state(microbe) + if state(motilepattern(microbe)) == Forward + β = microbe.gain_forward + else + β = microbe.gain_backward + end + return (1 + β*S) +end + +function turnrate(microbe::Xie, model) + if state(motilepattern(microbe)) == Forward + return microbe.turn_rate_forward + else + return microbe.turn_rate_backward + end +end diff --git a/src/microbe_step.jl b/src/microbe_step.jl index da7e33e..9849586 100644 --- a/src/microbe_step.jl +++ b/src/microbe_step.jl @@ -12,7 +12,7 @@ Perform an integration step for `microbe`. In order: function microbe_step!(microbe::AbstractMicrobe, model) dt = model.timestep # integration timestep # update microbe position - move_agent!(microbe, model, microbe.speed * dt) + move_agent!(microbe, model, speed(microbe)*dt) # reorient through rotational diffusion rotational_diffusion!(microbe, model) # update microbe state @@ -46,12 +46,13 @@ function microbe_pathfinder_step!(microbe::AbstractMicrobe, model) nothing end -# exposed to allow overload and customization """ turnrate(microbe, model) Evaluate instantaneous turn rate of `microbe`. """ -turnrate(microbe::AbstractMicrobe, model) = microbe.turn_rate +turnrate(microbe::AbstractMicrobe, model) = turnrate(microbe) * cwbias(microbe, model) +# no CW bias for generic non-chemotactic microbe +cwbias(microbe::AbstractMicrobe, model) = 1.0 """ affect!(microbe, model) Can be used to arbitrarily update `microbe` state. diff --git a/src/microbes.jl b/src/microbes.jl index 5172e2b..8de04b1 100644 --- a/src/microbes.jl +++ b/src/microbes.jl @@ -4,22 +4,22 @@ export Microbe Microbe{D} <: AbstractMicrobe{D} Base microbe type for simple simulations. -Default parameters: -- `motility = RunTumble()` motile pattern -- `turn_rate::Float64 = 1.0` frequency of reorientations -- `rotational_diffusivity::Real` coefficient of brownian rotational diffusion -- `radius::Float64 = 0.0` equivalent spherical radius of the microbe -- `state::Float64 = 0.0` generic variable for a scalar internal state - -`Microbe` has the additional required fields +`Microbe` has the required fields - `id::Int` an identifier used internally - `pos::SVector{D,Float64}` spatial position - `vel::SVector{D,Float64}` unit velocity vector - `speed::Float64` magnitude of the velocity vector + +and the default parameters +- `motility::AbstractMotility = RunTumble()` motile pattern of the microbe +- `turn_rate::Float64 = 1.0` frequency of reorientations +- `rotational_diffusivity::Float64 = 0.0` coefficient of brownian rotational diffusion +- `radius::Float64 = 0.0` equivalent spherical radius of the microbe +- `state::Float64 = 0.0` generic variable for a scalar internal state """ @agent struct Microbe{D}(ContinuousAgent{D,Float64}) <: AbstractMicrobe{D} speed::Float64 - motility::AbstractMotility = RunTumble() + motility = RunTumble() turn_rate::Float64 = 1.0 rotational_diffusivity::Float64 = 0.0 radius::Float64 = 0.0 @@ -28,9 +28,9 @@ end r2dig(x) = round(x, digits=2) function Base.show(io::IO, ::MIME"text/plain", m::AbstractMicrobe{D}) where D - println(io, "$(typeof(m)) with $(typeof(m.motility)) motility") - println(io, "position (μm): $(r2dig.(m.pos)); velocity (μm/s): $(r2dig.(m.vel.*m.speed))") - println(io, "average unbiased turn rate (Hz): $(r2dig(m.turn_rate))") + println(io, "$(typeof(m)) with $(M)") + println(io, "position (μm): $(r2dig.(position(m))); velocity (μm/s): $(r2dig.(velocity(m)))") + println(io, "average unbiased turn rate (Hz): $(r2dig(turn_rate(m)))") s = setdiff(fieldnames(typeof(m)), [:id, :pos, :motility, :vel, :turn_rate]) print(io, "other properties: " * join(s, ", ")) end diff --git a/src/motility.jl b/src/motility.jl index 226da86..db380af 100644 --- a/src/motility.jl +++ b/src/motility.jl @@ -1,9 +1,8 @@ export - AbstractMotility, AbstractMotilityOneStep, AbstractMotilityTwoStep, - MotilityOneStep, MotilityTwoStep, + AbstractMotility, MotilityOneStep, MotilityTwoStep, RunTumble, RunReverse, RunReverseFlick, MotileState, TwoState, Forward, Backward, switch!, - random_polar, random_azimuthal + state, speed, polar, azimuthal export Arccos # from Agents @@ -13,226 +12,88 @@ General abstract interface for motility patterns. """ abstract type AbstractMotility end - - -# Copied and adapted from the @agent macro of Agents.jl -macro motility(new_name, base_type, extra_fields) - # This macro was generated with the guidance of @rdeits on Discourse: - # https://discourse.julialang.org/t/ - # metaprogramming-obtain-actual-type-from-symbol-for-field-inheritance/84912 - - # We start with a quote. All macros return a quote to be evaluated - quote - let - # Here we collect the field names and types from the base type - # Because the base type already exists, we escape the symbols to obtain it - base_fieldnames = fieldnames($(esc(base_type))) - base_fieldtypes = [t for t in getproperty($(esc(base_type)), :types)] - base_fields = [:($f::$T) for (f, T) in zip(base_fieldnames, base_fieldtypes)] - # Then, we prime the additional name and fields into QuoteNodes - # We have to do this to be able to interpolate them into an inner quote. - name = $(QuoteNode(new_name)) - additional_fields = $(QuoteNode(extra_fields.args)) - # Now we start an inner quote. This is because our macro needs to call `eval` - # However, this should never happen inside the main body of a macro - # There are several reasons for that, see the cited discussion at the top - expr = quote - mutable struct $name <: AbstractMotility - $(base_fields...) - $(additional_fields...) - end - end - # @show expr # uncomment this to see that the final expression looks as desired - # It is important to evaluate the macro in the module that it was called at - Core.eval($(__module__), expr) - end - # allow attaching docstrings to the new struct, issue #715 - Core.@__doc__($(esc(Docs.namify(new_name)))) - nothing - end -end -# There should be away that only the 4-argument version is used -# and the 3-argument version just passes `AbstractAgent` to the 4-argument. -macro motility(new_name, base_type, super_type, extra_fields) - # This macro was generated with the guidance of @rdeits on Discourse: - # https://discourse.julialang.org/t/ - # metaprogramming-obtain-actual-type-from-symbol-for-field-inheritance/84912 - - # We start with a quote. All macros return a quote to be evaluated - quote - let - # Here we collect the field names and types from the base type - # Because the base type already exists, we escape the symbols to obtain it - base_fieldnames = fieldnames($(esc(base_type))) - base_fieldtypes = [t for t in getproperty($(esc(base_type)), :types)] - base_fields = [:($f::$T) for (f, T) in zip(base_fieldnames, base_fieldtypes)] - # Then, we prime the additional name and fields into QuoteNodes - # We have to do this to be able to interpolate them into an inner quote. - name = $(QuoteNode(new_name)) - additional_fields = $(QuoteNode(extra_fields.args)) - # Now we start an inner quote. This is because our macro needs to call `eval` - # However, this should never happen inside the main body of a macro - # There are several reasons for that, see the cited discussion at the top - expr = quote - # Also notice that we escape supertype and interpolate it twice - # because this is expected to already be defined in the calling module - mutable struct $name <: $$(esc(super_type)) - $(base_fields...) - $(additional_fields...) - end - end - # @show expr # uncomment this to see that the final expression looks as desired - # It is important to evaluate the macro in the module that it was called at - Core.eval($(__module__), expr) - end - # allow attaching docstrings to the new struct, issue #715 - Core.@__doc__($(esc(Docs.namify(new_name)))) - nothing - end -end - - - - """ - AbstractMotilityOneStep -One-step motility patterns (`RunTumble`). -Subtypes have the following fields: + MotilityOneStep +Type for one-step motility patterns (e.g. `RunTumble`). + +A `MotilityOneStep` has the fields - `speed`: distribution of microbe speed, new values extracted after each turn - `polar`: distribution of polar angles - `azimuthal`: distribution azimuthal angles For 2-dimensional microbe types, only `polar` defines reorientations and `azimuthal` is ignored. - -New one-step motility patterns can be created as -``` -MicrobeAgents.@motility NewMotilityType MotilityOneStep AbstractMotilityOneStep begin - # some extra fields if needed -end -``` -The necessary fields and subtyping will be added automatically, only new extra fields -need to be specified in the definition. -If default values have to be specified, a constructor needs to be defined explicitly. """ -abstract type AbstractMotilityOneStep <: AbstractMotility end -# only used to define the base fields -struct MotilityOneStep +struct MotilityOneStep <: AbstractMotility speed polar azimuthal end -@motility RunTumble MotilityOneStep AbstractMotilityOneStep begin; end """ RunTumble(; speed=(30.0,), polar=Uniform(-π,π), azimuthal=Arccos()) -Run-tumble motility. -The kwargs `speed`, `polar` and `azimuthal` must be sampleable objects -(ranges, arrays, tuples, distributions...), _not scalars_. - -With default values, the reorientations are uniform on the sphere. +Constructor for a `MotilityOneStep` with default values associated to run-and-tumble motion. """ -RunTumble(; speed=(30.0,), polar=Uniform(-π,π), azimuthal=Arccos()) = RunTumble(speed, polar, azimuthal) +RunTumble(; speed=(30.0,), polar=Uniform(-π,π), azimuthal=Arccos()) = + MotilityOneStep(speed, polar, azimuthal) """ - AbstractMotilityTwoStep -Two-step motility patterns (`RunReverse` and `RunReverseFlick`), with different -properties between forward and backward state of motion. -Subtypes have the following fields: -- `speed_forward`: distribution of microbe speed, new values extracted after each turn -- `polar_forward`: distribution of in-plane reorientations for motile state fw -- `azimuthal_forward`: distribution of out-of-plane reorientations for motile state fw + MotilityTwoStep +Type for two-step motility patterns (e.g. `RunReverse`, `RunReverseFlick`) +In two-step motility patterns, the two "steps" can have different properties. + +A `MotilityTwoStep` has the fields +- `speed`: distribution of microbe speed, new values extracted after each turn +- `polar`: distribution of in-plane reorientations for motile state fw +- `azimuthal`: distribution of out-of-plane reorientations for motile state fw - `speed_backward`: distribution of microbe speed, new values extracted after each turn - `polar_backward`: distribution of in-plane reorientations for motile state bw - `azimuthal_backward`: distribution of out-of-plane reorientations for motile state bw - `motile_state`: defines current motile state (e.g. `Forward` or `Backward` for a `TwoState`) For 2-dimensional microbe types, only polar distributions define reorientations while azimuthal ones are ignored. - -New two-step motility patterns can be created as -``` -MicrobeAgents.@motility NewMotilityType MotilityTwoStep AbstractMotilityTwoStep begin - # some extra fields if needed -end -``` -The necessary fields and subtyping will be added automatically, only new extra fields -need to be specified in the definition. -If default values have to be specified, a constructor needs to be defined explicitly. """ -abstract type AbstractMotilityTwoStep <: AbstractMotility end -# only used to define the base fields -struct MotilityTwoStep - speed_forward - polar_forward - azimuthal_forward +struct MotilityTwoStep <: AbstractMotility + speed + polar + azimuthal speed_backward polar_backward azimuthal_backward motile_state end -@motility RunReverse MotilityTwoStep AbstractMotilityTwoStep begin; end """ - RunReverse(; - speed_forward = (30.0,), - polar_forward = (π,), - azimuthal_forward = Arccos(), - speed_backward = speed_forward, - polar_backward = polar_forward, - azimuthal_backward = azimuthal_forward - ) -Run-reverse motility, with possibility to have different properties between -the forward (run) and backward (reverse) stages. -All the fields must be sampleable objects (ranges, arrays, tuples, distributions...), -_not scalars_. - -With default values, reorientations are always perfect reversals -and the speed is identical between forward and backward runs. + RunReverse(; speed=(30.0,), polar=(π,), azimuthal=Arccos(), speed_backward=speed, polar_backward=polar, azimuthal_backward=azimuthal) +Constructor for a `MotilityTwoStep` with default values associated to run-reverse motion. """ RunReverse(; - speed_forward = (30.0,), - polar_forward = (π,), - azimuthal_forward = Arccos(), - speed_backward = speed_forward, - polar_backward = polar_forward, - azimuthal_backward = azimuthal_forward, + speed = (30.0,), + polar = (π,), + azimuthal = Arccos(), + speed_backward = speed, + polar_backward = polar, + azimuthal_backward = azimuthal, motile_state = MotileState() -) = RunReverse( - speed_forward, polar_forward, azimuthal_forward, - speed_backward, polar_backward, azimuthal_backward, - motile_state) +) = MotilityTwoStep(speed, polar, azimuthal, + speed_backward, polar_backward, azimuthal_backward, + motile_state) - -@motility RunReverseFlick MotilityTwoStep AbstractMotilityTwoStep begin; end """ - RunReverseFlick(; - speed_forward = (30.0,), - polar_forward = (π,), - azimuthal_forward = Arccos(), - speed_backward = speed_forward, - polar_backward = (-π/2, π/2), - azimuthal_backward = azimuthal_forward - ) -Run-reverse-flick motility, with possibility to have different properties between -the forward (run) and backward (reverse) stages. -All the fields must be sampleable objects (ranges, arrays, tuples, distributions...), -_not scalars_. - -With default values, reorientations after forward runs are perfect reversals, -while reorientations after backward runs are uniformly distributed on the circle -normal to the run direction; speed is identical between forward and backward runs. + RunReverseFlick(; speed=(30.0,), polar=(π,), azimuthal=Arccos(), speed_backward=speed, polar_backward=(-π/2,π/2), azimuthal_backward=azimuthal) +Constructor for a `MotilityTwoStep` with default values associated to run-reverse-flick motion. """ RunReverseFlick(; - speed_forward = (30.0,), - polar_forward = (π,), - azimuthal_forward = Arccos(), - speed_backward = speed_forward, + speed = (30.0,), + polar = (π,), + azimuthal = Arccos(), + speed_backward = speed, polar_backward = (-π/2, π/2), - azimuthal_backward = azimuthal_forward, + azimuthal_backward = azimuthal, motile_state = MotileState() -) = RunReverseFlick( - speed_forward, polar_forward, azimuthal_forward, - speed_backward, polar_backward, azimuthal_backward, - motile_state) +) = MotilityTwoStep(speed, polar, azimuthal, + speed_backward, polar_backward, azimuthal_backward, + motile_state) + # just a wrapper to allow state to be mutable @@ -251,14 +112,14 @@ Base.show(io::IO, ::MIME"text/plain", x::TwoState) = # initialize to Forward if not specified TwoState() = Forward # overload getproperty and setproperty! for convenient access to state -function Base.getproperty(obj::AbstractMotilityTwoStep, sym::Symbol) +function Base.getproperty(obj::MotilityTwoStep, sym::Symbol) if sym === :state return obj.motile_state.state else return getfield(obj, sym) end end -function Base.setproperty!(obj::AbstractMotilityTwoStep, sym::Symbol, x) +function Base.setproperty!(obj::MotilityTwoStep, sym::Symbol, x) if sym === :state return setfield!(obj.motile_state, :state, x) else @@ -266,52 +127,27 @@ function Base.setproperty!(obj::AbstractMotilityTwoStep, sym::Symbol, x) end end # define rules for switching motile state -switch!(::AbstractMotilityOneStep) = nothing +switch!(::MotilityOneStep) = nothing """ - switch!(m::AbstractMotilityTwoStep) + switch!(m::MotilityTwoStep) Switch the state of a two-step motility pattern (`m.state`) from `Forward` to `Backward` and viceversa. """ -switch!(m::AbstractMotilityTwoStep) = (m.state = ~m.state; nothing) +switch!(m::MotilityTwoStep) = (m.state = ~m.state; nothing) Base.:~(x::TwoState) = TwoState(~Bool(x)) # convenient wrapper to sample new motile state Base.rand(rng::AbstractRNG, motility::AbstractMotility) = ( - random_speed(rng, motility), - random_polar(rng, motility), - random_azimuthal(rng, motility) + rand(rng, speed(motility)), + rand(rng, polar(motility)), + rand(rng, azimuthal(motility)) ) -random_speed(rng::AbstractRNG, motility::AbstractMotilityOneStep) = rand(rng, motility.speed) -function random_speed(rng::AbstractRNG, motility::AbstractMotilityTwoStep) - if motility.state == Forward - return rand(rng, motility.speed_forward) - else - return rand(rng, motility.speed_backward) - end -end - -""" - random_polar(rng, motility) -Sample random value from the distribution of polar reorientations of `motility`. -""" -random_polar(rng::AbstractRNG, m::AbstractMotilityOneStep) = rand(rng, m.polar) -""" - random_azimuthal(rng, motility) -Sample random value from the distribution of azimuthal reorientations of `motility`. -""" -random_azimuthal(rng::AbstractRNG, m::AbstractMotilityOneStep) = rand(rng, m.azimuthal) -function random_polar(rng::AbstractRNG, m::AbstractMotilityTwoStep) - if m.state == Forward - return rand(rng, m.polar_forward) - else - return rand(rng, m.polar_backward) - end -end -function random_azimuthal(rng::AbstractRNG, m::AbstractMotilityTwoStep) - if m.state == Forward - return rand(rng, m.azimuthal_forward) - else - return rand(rng, m.azimuthal_backward) - end -end +state(m::MotilityOneStep) = Forward +state(m::MotilityTwoStep) = m.state +speed(m::MotilityOneStep) = m.speed +polar(m::MotilityOneStep) = m.polar +azimuthal(m::MotilityOneStep) = m.azimuthal +speed(m::MotilityTwoStep) = state(m) == Forward ? m.speed : m.speed_backward +polar(m::MotilityTwoStep) = state(m) == Forward ? m.polar : m.polar_backward +azimuthal(m::MotilityTwoStep) = state(m) == Forward ? m.azimuthal : m.azimuthal_backward diff --git a/src/rotations.jl b/src/rotations.jl index d2b3735..a7344b3 100644 --- a/src/rotations.jl +++ b/src/rotations.jl @@ -1,12 +1,12 @@ function turn!(microbe::AbstractMicrobe, model) - v = microbe.vel + e = direction(microbe) # extract new speed and rotation angles - U₁, θ, ϕ = rand(abmrng(model), microbe.motility) - # reorient and update speed - microbe.vel = rotate(v, θ, ϕ) + U₁, θ, ϕ = rand(abmrng(model), motilepattern(microbe)) + # reorient and update direction + microbe.vel = rotate(e, θ, ϕ) microbe.speed = U₁ # switch motile state (does nothing if motility is one-step) - switch!(microbe.motility) + switch!(motilepattern(microbe)) end """ @@ -17,11 +17,11 @@ In 1-dimensional models, this functions does nothing. rotational_diffusion!(microbe::AbstractMicrobe{1}, model) = nothing function rotational_diffusion!(microbe::AbstractMicrobe, model) dt = model.timestep - D_rot = microbe.rotational_diffusivity + D_rot = rotational_diffusivity(microbe) σ = sqrt(2*D_rot*dt) θ = rand(abmrng(model), Normal(0,σ)) ϕ = rand(abmrng(model), Arccos()) - microbe.vel = rotate(microbe.vel, θ, ϕ) + microbe.vel = rotate(direction(microbe), θ, ϕ) nothing end diff --git a/src/utils.jl b/src/utils.jl index 7bccf68..d68d2c2 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,4 +1,4 @@ -export random_velocity, random_speed, ChainedFunction, →, distance, distancevector +export random_velocity, random_speed, ChainedFunction, → """ random_velocity(model) @@ -19,16 +19,9 @@ end Generate a random speed from the motile pattern of `microbe`. """ function random_speed(microbe::AbstractMicrobe, model::AgentBasedModel) - random_speed(abmrng(model), motilepattern(microbe)) + rand(abmrng(model), speed(motilepattern(microbe))) end -""" - motilepattern(microbe) -Get the motility pattern of `microbe` -""" -motilepattern(microbe::AbstractMicrobe) = microbe.motility - - struct ChainedFunction{H,T} <: Function head::H tail::T @@ -47,28 +40,3 @@ end funlist(f::ChainedFunction{<:ChainedFunction,<:ChainedFunction}) = (funlist(f.head)..., funlist(f.tail)...) funlist(f::ChainedFunction{<:Function,<:ChainedFunction}) = (f.head, funlist(f.tail)...) funlist(f::ChainedFunction{<:Function,<:Function}) = (f.head, f.tail) - -""" - distance(a, b, model) -Evaluate the euclidean distance between `a` and `b` respecting the spatial -properties of `model`. -""" -@inline distance(a, b, model) = euclidean_distance(position(a), position(b), model) -""" - distancevector(a, b, model) -Evaluate the distance vector from `a` to `b` respecting the spatial -properties of `model`. -""" -@inline distancevector(a, b, model) = distancevector(position(a), position(b), model) -@inline function distancevector(a::SVector{D}, b::SVector{D}, model) where D - extent = spacesize(model) - SVector{D}(wrapcoord(a[i], b[i], extent[i]) for i in 1:D) -end -function wrapcoord(x₁, x₂, d) - α = (x₂-x₁)/d - (α-round(α))*d -end - -@inline position(a::AbstractMicrobe) = a.pos -@inline position(a::SVector{D}) where D = a -@inline position(a::NTuple{D}) where D = SVector{D}(a) diff --git a/test/model-creation.jl b/test/model-creation.jl index 56f4925..00c6a58 100644 --- a/test/model-creation.jl +++ b/test/model-creation.jl @@ -27,16 +27,16 @@ using LinearAlgebra: norm rng = Xoshiro(123) pos = rand(rng, SVector{D}) vel = random_velocity(rng, D) - speed = random_speed(rng, RunTumble()) + #speed = random_speed(rng, RunTumble()) + spd = rand(rng, speed(RunTumble())) @test model[1] isa Microbe{D} - @test model[1].pos == pos - @test model[1].vel == vel - @test model[1].speed == speed - @test model[1].motility isa RunTumble - @test model[1].turn_rate == 1.0 - @test model[1].radius == 0.0 - @test model[1].rotational_diffusivity == 0.0 - @test model[1].state == 0.0 + @test position(model[1]) == pos + @test direction(model[1]) == vel + @test speed(model[1]) == spd + @test turnrate(model[1]) == 1.0 + @test radius(model[1]) == 0.0 + @test rotational_diffusivity(model[1]) == 0.0 + @test state(model[1]) == 0.0 # add agent with kwproperties motility = RunReverse( speed_backward = [24.0], @@ -44,7 +44,6 @@ using LinearAlgebra: norm ) add_agent!(model; turn_rate = 0.55, motility) @test model[2].turn_rate == 0.55 - @test model[2].motility isa RunReverse @test model[2].speed == 24.0 # add agent with predefined position pos = SVector{D}(i/2D for i in 1:D) @@ -63,21 +62,22 @@ using LinearAlgebra: norm add_agent!(model) m = model[1] @test m isa T{D} - @test m.pos == rand(rng, SVector{D}) - @test m.vel == random_velocity(rng, D) - @test m.speed == random_speed(rng, m.motility) + @test position(m) == rand(rng, SVector{D}) + @test direction(m) == random_velocity(rng, D) + @test speed(m) == rand(rng, speed(motilepattern(m))) @test issubset( (:id, :pos, :vel, :speed, :motility, :rotational_diffusivity, :radius, :state), fieldnames(T) ) + φ(microbe, model) = turnrate(microbe) * cwbias(microbe, model) if T == BrownBerg - @test turnrate(m, model) == m.turn_rate * exp(-m.gain*m.state) + @test φ(m, model) == m.turn_rate * exp(-m.gain*m.state) elseif T == Brumley - @test turnrate(m, model) == (1+exp(-m.gain*m.state))*m.turn_rate/2 + @test φ(m, model) == (1+exp(-m.gain*m.state))*m.turn_rate/2 elseif T == Celani - @test turnrate(m, model) == m.turn_rate * (1 - m.gain*m.state) + @test φ(m, model) == m.turn_rate * (1 - m.gain*m.state) # when no concentration field is set, markovian variables are zero @test m.markovian_variables == zeros(3) diff --git a/test/model-stepping.jl b/test/model-stepping.jl index b7a3400..e82314c 100644 --- a/test/model-stepping.jl +++ b/test/model-stepping.jl @@ -11,10 +11,10 @@ using LinearAlgebra: norm model = StandardABM(Microbe{D}, space, dt; rng, container) pos = extent ./ 2 vel1 = random_velocity(model) - speed1 = random_speed(rng, RunTumble()) + speed1 = rand(rng, speed(RunTumble())) add_agent!(pos, model; vel=vel1, speed=speed1, turn_rate=0) vel2 = random_velocity(model) - speed2 = random_speed(rng, RunReverse()) + speed2 = rand(rng, speed(RunReverse())) add_agent!(pos, model; vel=vel2, speed=speed2, turn_rate=Inf, motility=RunReverse()) run!(model, 1) # performs 1 microbe_step! # x₁ = x₀ + vΔt diff --git a/test/motility.jl b/test/motility.jl index e12afa4..aa74789 100644 --- a/test/motility.jl +++ b/test/motility.jl @@ -3,14 +3,14 @@ using Distributions using LinearAlgebra: norm @testset "Motility" begin - @test AbstractMotilityOneStep <: AbstractMotility - @test AbstractMotilityTwoStep <: AbstractMotility - @test ~(AbstractMotilityOneStep <: AbstractMotilityTwoStep) + @test MotilityOneStep <: AbstractMotility + @test MotilityTwoStep <: AbstractMotility + @test !(MotilityOneStep <: MotilityTwoStep) - @test fieldnames(RunTumble) == (:speed, :polar, :azimuthal) + @test fieldnames(MotilityOneStep) == (:speed, :polar, :azimuthal) rt = RunTumble() # type hierarchy - @test rt isa AbstractMotilityOneStep + @test rt isa MotilityOneStep # default values @test rt.speed == (30.0,) @test rt.polar == Uniform(-π, π) @@ -20,24 +20,21 @@ using LinearAlgebra: norm @test rt.speed == Normal(5, 0.1) @test rt.polar == [-0.1, 0.1] @test rt.azimuthal == (π,) - # base constructor without keywords - rt2 = RunTumble(Normal(5,0.1), [-0.1, 0.1], (π,)) - @test rt2.speed == rt.speed && rt2.polar == rt.polar && rt2.azimuthal == rt.azimuthal - @test fieldnames(RunReverse) == ( - :speed_forward, :polar_forward, :azimuthal_forward, + @test fieldnames(MotilityTwoStep) == ( + :speed, :polar, :azimuthal, :speed_backward, :polar_backward, :azimuthal_backward, :motile_state ) # default values rr = RunReverse() - @test rr isa AbstractMotilityTwoStep - @test rr.speed_forward == (30.0,) - @test rr.polar_forward == (π,) - @test rr.azimuthal_forward == Arccos() - @test rr.speed_backward == rr.speed_forward - @test rr.polar_backward == rr.polar_forward - @test rr.azimuthal_backward == rr.azimuthal_forward + @test rr isa MotilityTwoStep + @test rr.speed == (30.0,) + @test rr.polar == (π,) + @test rr.azimuthal == Arccos() + @test rr.speed_backward == rr.speed + @test rr.polar_backward == rr.polar + @test rr.azimuthal_backward == rr.azimuthal @test rr.motile_state.state == Forward # field overload @test rr.state == Forward @@ -45,19 +42,18 @@ using LinearAlgebra: norm rr = RunReverse(; azimuthal_backward = (-π/4,0,π/4)) @test rr.azimuthal_backward == (-π/4,0,π/4) # backward distributions follow forward if unspecified - rr = RunReverse(; speed_forward = (45,)) - @test rr.speed_backward == rr.speed_forward == (45,) + rr = RunReverse(; speed = (45,)) + @test rr.speed_backward == rr.speed == (45,) - @test fieldnames(RunReverseFlick) == fieldnames(RunReverse) # default values rrf = RunReverseFlick() - @test rrf isa AbstractMotilityTwoStep - @test rrf.speed_forward == (30.0,) - @test rrf.polar_forward == (π,) - @test rrf.azimuthal_forward == Arccos() - @test rrf.speed_backward == rrf.speed_forward + @test rrf isa MotilityTwoStep + @test rrf.speed == (30.0,) + @test rrf.polar == (π,) + @test rrf.azimuthal == Arccos() + @test rrf.speed_backward == rrf.speed @test rrf.polar_backward == (-π/2, π/2) - @test rrf.azimuthal_backward == rrf.azimuthal_forward + @test rrf.azimuthal_backward == rrf.azimuthal @test rrf.motile_state.state == Forward # field overload @test rrf.state == Forward @@ -65,9 +61,9 @@ using LinearAlgebra: norm rrf = RunReverseFlick(; azimuthal_backward = (-π/4,0,π/4)) @test rrf.azimuthal_backward == (-π/4,0,π/4) # polar distributions are independent in run reverse flick - rrf = RunReverseFlick(; speed_forward=(45,), polar_forward=(3π,)) - @test rrf.speed_backward == rrf.speed_forward == (45,) - @test rrf.polar_forward == (3π,) && rrf.polar_backward == (-π/2,π/2) + rrf = RunReverseFlick(; speed=(45,), polar=(3π,)) + @test rrf.speed_backward == rrf.speed == (45,) + @test rrf.polar == (3π,) && rrf.polar_backward == (-π/2,π/2) @testset "Motile state" begin @test instances(TwoState) == (Forward, Backward) @@ -105,7 +101,7 @@ using LinearAlgebra: norm @test u₁ == u₂ model = StandardABM(Microbe{2}, ContinuousSpace((1,1))) - rr = RunReverse(speed_forward=[45], speed_backward=[35]) # initialized to Forward + rr = RunReverse(speed=[45], speed_backward=[35]) # initialized to Forward add_agent!(model; motility = rr) @test random_speed(model[1], model) == 45 switch!(model[1].motility) # now Backward